GHSA-3p24-9x7v-7789 Emissary has an OS Command Injection via Unvalidated IN_FILE_ENDING / OUT_FILE_ENDING in Executrix
Summary`Executrix.getCommand()` constructs shell commands by substituting temporary file paths directly into a `/bin/sh -c` string with no escaping. The `IN_FILE_ENDING` and `OUT_FILE_ENDING` configuration keys flow into those paths unmodified. A place author who sets either key to a shell metacharacter sequence achieves arbitrary OS command execution in the JVM's security context when the place processes any payload. No runtime privileges beyond place configuration authorship are required, and no API or network access is needed.This is a **framework-level defect** — `Executrix` provides no escaping mechanism and no validation on file ending values. Downstream implementors have no safe way to use the API as designed.Root CauseStep 1 — `IN_FILE_ENDING` flows into temp path construction without validation**[`TempFileNames.java:32-36`](src/main/java/emissary/util/shell/TempFileNames.java#L32-L36)**`inFileEnding` is concatenated directly onto a numeric base to produce `inputFilename`. No character class, no regex, no escaping.Step 2 — The injected path is substituted verbatim into a shell string**[`Executrix.java:1053-1065`](src/main/java/emissary/util/shell/Executrix.java#L1053-L1065)**The final array element is passed to `/bin/sh -c`. Shell metacharacters in any substituted value are interpreted by the shell.The identical pattern exists in the `TempFileNames` overload at **[`Executrix.java:1103-1115`](src/main/java/emissary/util/shell/Executrix.java#L1103-L1115)**.Step 3 — `setInFileEnding()` and `setOutFileEnding()` perform no validation**[`Executrix.java:1176-1196`](src/main/java/emissary/util/shell/Executrix.java#L1176-L1196)**The same absence of validation applies to the `IN_FILE_ENDING` and `OUT_FILE_ENDING` keys read from configuration at **[`Executrix.java:121-122`](src/main/java/emissary/util/shell/Executrix.java#L121-L122)**.Contrast: `placeName` is sanitized, file endings are notThe framework already sanitizes `placeName` using a strict allowlist:`placeName` ends up in `tmpNames[DIR]`, which is also embedded in the shell string. The sanitization of `placeName` demonstrates awareness that these values reach the shell — the omission of equivalent sanitization for `inFileEnding` and `outFileEnding` is the defect.Proof of ConceptTwo reproduction paths are provided: a Docker-based end-to-end attack against a live Emissary node (verified), and a unit-level test for CI integration.PoC 1 — Docker: end-to-end attack against a live node**Verified against Emissary 8.42.0-SNAPSHOT running in Docker on Alpine Linux.****Environment setup**Put the `Dockerfile.poc` to `contrib/docker/` folder**Step 1 — Confirm the marker file does not exist****Step 2 — Write the malicious place config**Write `emissary.place.UnixCommandPlace.cfg` into the server's config directory. The `EXEC_COMMAND` is a benign `cat`. The injection is entirely in `IN_FILE_ENDING` using backtick command substitution (POSIX-compatible, works on all target OS images):**Step 3 — Add UnixCommandPlace to places.cfg****Step 4 — Restart the server to load the config**Startup log confirms the place loaded:**Step 5 — Drop any file into the pickup directory to trigger processing**The Emissary pipeline picks up the file, routes it through `UnixFilePlace` → `ToLowerPlace` → **`UnixCommandPlace`** (cost 4000, lower than `ToUpperPlace` at 5010, so it wins the routing). The injected backtick expression runs during shell argument expansion inside `getCommand()` before `cat` is even called.**Step 6 — Confirm injection executed****Live output (verified):****Assembled shell string at execution time** (logged by Emissary at DEBUG level):The backtick expression fires as the shell expands the `cat` argument. The `cat` itself returns non-zero (no file at that path) but that is irrelevant — the injected command has already run.**Transform history from Emissary logs — confirms UnixCommandPlace ran:****Escalating the payload — reverse shell**Replace the `IN_FILE_ENDING` value. The content is passed verbatim to `/bin/sh -c`, so any POSIX shell construct works:Both fire on the first payload processed — no further attacker interaction required.PoC 2 — Unit test: isolated, no server requiredExercises the identical code path using only the public `Executrix` API. Suitable for inclusion in a CI security regression suite.**Assembled shell string:**The marker file is created by the backtick expression firing during shell argument expansion.**Note on `$()` vs backticks:** `String.replaceAll()` treats `$` in the replacement as a regex group reference, so a `$(...)` payload causes a `java.lang.IllegalArgumentException` before reaching the shell. The backtick form avoids this Java-layer error and confirms the shell injection path. Both forms are equivalent at the shell level; on a real deployment the attacker would use backticks or escape the `$` appropriately.**The same injection works via `OUT_FILE_ENDING` → `<OUTPUT_PATH>` / `<OUTPUT_NAME>`, and via the `String[]` overload of `getCommand()` used by `MultiFileUnixCommandPlace`.**Attack ScenariosEach scenario is a realistic, step-by-step attack path using only capabilities observable in the codebase.Scenario A — Insider / developer with config write access**Attacker's starting position:** Developer or operator who can commit to the config repository or write to the config directory directly. No special server access required beyond what their role already provides.**Why this is realistic:** Emissary deployments typically load `.cfg` files from a directory checked into version control or managed by a configuration management system (Ansible, Chef, Puppet). A developer who can merge a config change — even a code reviewer who can approve their own PR — can inject the payload.**Step 1 — Add the malicious config as a seemingly routine change**In a PR or direct push to the config repo:The injection lives in a string value inside a properties-style config file. It does not look like code to a reviewer who is not specifically aware of this vulnerability.**Step 2 — Wait for the next deploy**The next routine deploy or restart loads the config. The payload fires on the first payload processed — silently, with no error visible in normal log levels (the place logs a `WARN` for non-zero exit but does not surface the injected command's output).**Deniability:** The `.cfg` file looks like a misconfigured place. The log entry is `Bad execution of commands` — a common operational error, not an obvious security event.Scenario B — Cluster-wide propagation via the peers API**Attacker's starting position:** RCE on one node (from Scenario A).**Why this is dangerous:** Emissary clusters share config through the directory service. Once the attacker has shell on one node, they can use the cluster's own replication to propagate the malicious config to every peer.**Step 1 — Enumerate all cluster nodes**Response:**Step 2 — Push the malicious config to each peer via the Emissary API**From the compromised node, use the Emissary cluster API directly — no SSH required. All nodes authenticate each other using the same shared credentials, and the `CONFIG_DIR` path is disclosed by the `/api/peers` response metadata:If no config write API is available, the same result is achieved by dropping the payload into the peer's monitored pickup directory via the ingest endpoint, or by exploiting the fact that cluster nodes share a network-accessible config store (NFS, S3, git remote) — all of which are common Emissary deployment patterns.**Step 3 — Trigger restart on each peer via the cluster shutdown API****Outcome:** Every node in the cluster loads the malicious config on restart. Injection fires on all nodes simultaneously on the next payload, giving the attacker shell on the entire cluster from a single initial foothold.Impact| Dimension | Assessment |
|-----------|------------|
| **Confidentiality** | **Critical** — arbitrary read of files accessible to the Emissary process |
| **Integrity** | **Critical** — arbitrary file write, process state modification, persistence |
| **Availability** | **Critical** — process termination, resource exhaustion |
| **Blast radius** | Any place that uses `Executrix` and calls `getCommand()`; this includes all subclasses of `ExecPlace` and any custom place that follows the documented pattern |Recommended RemediationPrimary fix — validate `inFileEnding` and `outFileEnding` on assignmentApply the same allowlist pattern already used for `placeName`:Apply the same validation inside `configure()` where the values are read from the `Configurator`.Secondary fix (defence-in-depth) — shell-quote substituted values in `getCommand()`Even if validation is in place, the shell string construction should not rely on input cleanliness alone. Quote each substituted path component:Why this is a framework-level fixThe framework's `cleanPlaceName()` method already demonstrates the correct approach for values that reach the shell. Extending equivalent sanitization to `inFileEnding` and `outFileEnding` is a minimal, targeted change that requires no deployment configuration and no downstream implementor action. There is no architectural ambiguity about whether shell injection should be permitted: it should not.
CVSS v3.1 score: 8.8