richardcocks opened a new issue, #4058:
URL: https://github.com/apache/iggy/issues/4058

   ### Bug description
   
   Note: There is no path traversal due to a filename prefix, so as per 
`SECURITY.md`, "missing hardening or defence-in-depth suggestions with no 
accompanying exploit" is out of scope for private disclosure.  I have been 
unable to find any exploit angle despite initially looking like it could be a 
path traversal exploit.
   
   ---
   
   ## Bug description
   
   The connectors runtime control API takes the connector `key` from the 
request path and uses it,
   unvalidated, to build a filesystem path for the connector's config file. A 
`key` containing path
   separators or `..` is written straight into the target path.
   
   **What I was doing:** reviewing the local config provider's write path for 
the connector control API
   (`POST /sources/{key}/configs` and the `/sinks/{key}/configs` equivalent).
   
   **What I expected:** the `key` is validated (or the resulting path is 
confined to `config_dir`) before
   any filesystem operation.
   
   **What actually happens:** there is no validation anywhere. The path 
parameter flows verbatim into the
   config filename:
   
   - `create_source_config(&key, …)` — 
`core/connectors/runtime/src/api/source.rs:158`
   - → `cmd.to_source_config(key, next_version)` sets `config.key = key`
   - → `ConnectorId::to_filename_key()` = `format!("{}_{}", self.key, 
self.version)` —
     `core/connectors/runtime/src/configs/connectors/local_provider.rs:41`
   - → `format!("{}/source_{}.toml", self.config_dir, 
connector_id.to_filename_key())` —
     `local_provider.rs:445`
   - → `std::fs::write(&path, …)` — `local_provider.rs:450` (sink equivalent at 
`:411`/`:415`)
   
   There appears to be no `..`/separator check, no path canonicalization, and 
no validation 
   on this path. The `key` is fully controlled by anyone who can post to the 
connectors API. 
   
   ( It is also unclear to me if that is only trusted users, which further 
reduces and removes any security impact. ) 
   
   ### Impact
   
   Attempting the classic traversal does not escape `config_dir`:
   
   1. **Filename-prefix fusion.** The template welds a literal `source_` (or 
`sink_`) onto the first
      token of the key: `source_` + `../../evil` → `source_../../evil`, whose 
first path component is
      `source_..` — an ordinary (nonexistent) directory name, not `..`. The 
leading traversal step is
      dead on arrival.
   2. **No intermediate-dir creation.** `std::fs::write` does not create parent 
directories, so any
      multi-level `..` climb needs a real, traversable directory under 
`config_dir` to stand on. There
      is none by default (config_dir holds only `*.toml` files), and I 
confirmed by sweeping every
      `create_dir*` call in the tree that no runtime code path creates a 
`key`-named directory under
      `config_dir`. The only directories the config/state planes create are 
`config_dir` and
      `state_path` themselves, both from operator config. `delete_*_config` 
removes a stored path from
      its in-memory map, not a key-derived path, so it is not a vector either.
   
   So the traversal has no working escape. This safety could be unintentionally 
removed in a refactor:
   It breaks if either (a) an operator places any subdirectory under 
`config_dir`, or (b) a refactor reorders the template (key-first, or a per-key 
subdir like `config_dir/{key}/config.toml`) or adds 
`create_dir_all(path.parent())` before the write.
   
   ### Threat model / scope
   
   - The control API is **unauthenticated by default** (`api_key = ""`, 
`core/connectors/runtime/config.toml:21`;
     the auth middleware early-returns when the key is empty, 
`core/connectors/runtime/src/api/auth.rs:41`).
   - Connector plugins are **fully trusted** by design: they are `dlopen`'d 
native code
     (`unsafe { Container::<SinkApi>::load(&path) }`, 
`core/connectors/runtime/src/sink.rs:98`) called
     in-process via FFI, with no sandbox (no seccomp/landlock/chroot/rlimit 
anywhere in the runtime). A
     malicious plugin already has full process privileges, so this issue is 
irrelevant to a
     plugin-deploying attacker.
   - The only adversary for whom this matters is one who can reach the control 
API but **cannot** deploy
     a plugin (e.g. control API exposed on the network, plugin directory locked 
down / plugins vetted).
   
   This is an input-validation gap with no demonstrated escape.
   
   ### Suggested fix
   
   Validate `key` at the API boundary (and defensively in `create_*_config`) 
before it touches the
   filesystem: reject any key that is not `^[A-Za-z0-9._-]+$` (equivalently: 
contains `/`, `\`, `..`, a
   path separator, or a NUL), returning `400 Bad Request`. This mirrors the 
input validation the local
   provider already performs elsewhere and removes the reliance on accidental 
filename-prefix fusion.
   
   ### Affected area / component
   
   Connectors
   
   ### Deployment
   
   Compiled from source
   
   ### Versions
   
   Server `master` @ `be2265c9e` (0.11 edge line). Component: connectors 
runtime, local config provider.
   
   ### Hardware / environment
   
   _No response_
   
   ### Sample code
   
   With a connectors runtime running its control API locally (default config, 
empty `api_key`):
   
   ```bash
   API=http://127.0.0.1:8081
   H='content-type: application/json'
   BODY='{"enabled":false,"name":"x","path":"/tmp/evil.so","streams":[]}'
   
   # key contains encoded "../" — captured as a single path segment, then 
percent-decoded into the key
   curl -s --path-as-is -o /dev/null -w '%{http_code}\n' \
     -X POST -H "$H" -d "$BODY" "$API/sources/..%2F..%2Fpwned/configs"
   
   # descend-then-climb variant
   curl -s --path-as-is -o /dev/null -w '%{http_code}\n' \
     -X POST -H "$H" -d "$BODY" 
"$API/sources/x%2F..%2F..%2F..%2F..%2Ftmp%2Fpwn/configs"
   ```
   
   The write target resolves to a nonexistent intermediate directory and fails 
(ENOENT → `500`).
   
   ### Logs
   
   _No response_
   
   ### Iggy server config
   
   _No response_
   
   ### Reproduction
   
   _No response_
   
   ### Contribution
   
   - [ ] I'm willing to submit a pull request to fix this bug
   
   ### Good first issue
   
   - [ ] I think this could be a good first issue for a new contributor


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to