shell-quote's `quote()` function did not validate object-token inputs against the operator model used by `parse()`. The `.op` field was…
harborist·CWE-77·Published 2026-05-22
shell-quote's `quote()` function did not validate object-token inputs against the operator model used by `parse()`. The `.op` field was backslash-escaped character by character using `/(.)/g`, which in JavaScript does not match line terminators (\n, \r, U+2028, U+2029). A line terminator in `.op` therefore passed through unescaped into the output; POSIX shells treat a literal newline as a command separator, so any content after it would execute as a second command. The vulnerable code path is reachable in two ways: (1) direct construction of `{ op: '...\n...' }` from external input, and (2) via `parse(cmd, envFn)` when `envFn` returns object tokens whose `.op` is attacker-influenced. Both are documented API surface. Fixed by replacing the per-character escape with strict shape validation: `.op` must match the parser's control-operator allowlist; `{ op: 'glob', pattern }` validates `pattern` and forbids line terminators; `{ comment }` validates `comment` and forbids line terminators; any other object shape throws `TypeError`.
shell-quote's `quote()` function did not validate object-token inputs against the operator model used by `parse()`. The `.op` field was backslash-escaped character by character using `/(.)/g`, which in JavaScript does not match line terminators (\n, \r, U+2028, U+2029). A line terminator in `.op` therefore passed through unescaped into the output; POSIX shells treat a literal newline as a command separator, so any content after it would execute as a second command. The vulnerable code path is reachable in two ways: (1) direct construction of `{ op: '...\n...' }` from external input, and (2) via `parse(cmd, envFn)` when `envFn` returns object tokens whose `.op` is attacker-influenced. Both are documented API surface. Fixed by replacing the per-character escape with strict shape validation: `.op` must match the parser's control-operator allowlist; `{ op: 'glob', pattern }` validates `pattern` and forbids line terminators; `{ comment }` validates `comment` and forbids line terminators; any other object shape throws `TypeError`.
### Summary `shell-quote`'s `quote()` function did not validate object-token inputs against the operator model used by `parse()`. The `.op` field was backslash-escaped character by character using `/(.)/g`, which in JavaScript does not match line terminators (`\n`, `\r`, U+2028, U+2029). A line terminator in `.op` therefore passed through unescaped into the output; POSIX shells treat a literal `\n` as a command separator, so any content after it would execute as a second command. The vulnerable code path is reachable in two ways. Neither requires the parser to misbehave — `parse()` only emits ops from a fixed control set — but both are documented API surface: 1. **Direct construction.** A caller builds `{ op: '...\n...' }` from external input (e.g. a deserialized argument array) and passes it to `quote()`. 2. **`envFn` return.** `parse(cmd, envFn)` is documented to splice the return value of `envFn` into the result array when it is an object. An attacker-influenced data source consulted by `envFn` can introduce an object token whose `.op` reaches `quote()`. ### Impact Shell command injection in callers that pass object tokens with attacker-influenced `.op` values to `quote()` and then hand the result to a shell. The preconditions are narrower than ordinary string injection — they require the caller to feed object tokens into `quote()` — but object tokens are a public, documented part of the API surface, and `quote()` is intended to be a shell-safety boundary. ### PoC ```js const { parse, quote } = require('shell-quote'); // Direct construction quote([{ op: ';\nid' }]); // → "\;\n\\i\\d" ← literal newline; second line executes as a command // Via parse() with an envFn returning attacker-shaped objects const tokens = parse('echo $X', () => ({ op: ';\nid' })); require('child_process').execSync(quote(tokens), { shell: true }); // Executes `id` after `echo \;`. ``` Confirmed under `sh`, `bash`, `dash`, and `zsh`. ### Patch Fixed by replacing the per-character escape with strict shape validation in `quote()`. The object-token branch now: - **`{ op }`** — `.op` must be a string from the same allowlist the parser emits (`||`, `&&`, `;;`, `|&`, `<(`, `<<<`, `>>`, `>&`, `<&`, `&`, `;`, `(`, `)`, `|`, `<`, `>`). Anything else throws `TypeError`. This is the direct fix for the reported issue and removes the entire class of `.op` injection. - **`{ op: 'glob', pattern }`** — `.pattern` must be a string with no line terminators. Glob metacharacters (`*`, `?`, `[`, `]`, `{`, `}`, `,`) pass through; all other shell-special characters are backslash-escaped. (Previously the pattern field was discarded entirely and the literal string `\g\l\o\b` was emitted — a latent bug, not security-relevant.) - **`{ comment }`** — `.comment` must be a string with no line terminators (line terminators would end the shell comment and resume command parsing — same injection shape). - **Any other object shape** — `TypeError`. The fix is allowlist-based rather than a targeted regex tweak, so it closes the reported vector and forecloses adjacent ones (U+2028 / U+2029 line separators in `.op`, line terminators in comments, unknown-shape objects coerced through `.replace`). ### Workarounds Prior to upgrading, callers that build object tokens from untrusted input should validate `.op` against the parser's operator set themselves, and never construct `{ op }` from attacker-controlled strings. ### Credits Reported by Akshat Sinha
### Summary `shell-quote`'s `quote()` function did not validate object-token inputs against the operator model used by `parse()`. The `.op` field was backslash-escaped character by character using `/(.)/g`, which in JavaScript does not match line terminators (`\n`, `\r`, U+2028, U+2029). A line terminator in `.op` therefore passed through unescaped into the output; POSIX shells treat a literal `\n` as a command separator, so any content after it would execute as a second command. The vulnerable code path is reachable in two ways. Neither requires the parser to misbehave — `parse()` only emits ops from a fixed control set — but both are documented API surface: 1. **Direct construction.** A caller builds `{ op: '...\n...' }` from external input (e.g. a deserialized argument array) and passes it to `quote()`. 2. **`envFn` return.** `parse(cmd, envFn)` is documented to splice the return value of `envFn` into the result array when it is an object. An attacker-influenced data source consulted by `envFn` can introduce an object token whose `.op` reaches `quote()`. ### Impact Shell command injection in callers that pass object tokens with attacker-influenced `.op` values to `quote()` and then hand the result to a shell. The preconditions are narrower than ordinary string injection — they require the caller to feed object tokens into `quote()` — but object tokens are a public, documented part of the API surface, and `quote()` is intended to be a shell-safety boundary. ### PoC ```js const { parse, quote } = require('shell-quote'); // Direct construction quote([{ op: ';\nid' }]); // → "\;\n\\i\\d" ← literal newline; second line executes as a command // Via parse() with an envFn returning attacker-shaped objects const tokens = parse('echo $X', () => ({ op: ';\nid' })); require('child_process').execSync(quote(tokens), { shell: true }); // Executes `id` after `echo \;`. ``` Confirmed under `sh`, `bash`, `dash`, and `zsh`. ### Patch Fixed by replacing the per-character escape with strict shape validation in `quote()`. The object-token branch now: - **`{ op }`** — `.op` must be a string from the same allowlist the parser emits (`||`, `&&`, `;;`, `|&`, `<(`, `<<<`, `>>`, `>&`, `<&`, `&`, `;`, `(`, `)`, `|`, `<`, `>`). Anything else throws `TypeError`. This is the direct fix for the reported issue and removes the entire class of `.op` injection. - **`{ op: 'glob', pattern }`** — `.pattern` must be a string with no line terminators. Glob metacharacters (`*`, `?`, `[`, `]`, `{`, `}`, `,`) pass through; all other shell-special characters are backslash-escaped. (Previously the pattern field was discarded entirely and the literal string `\g\l\o\b` was emitted — a latent bug, not security-relevant.) - **`{ comment }`** — `.comment` must be a string with no line terminators (line terminators would end the shell comment and resume command parsing — same injection shape). - **Any other object shape** — `TypeError`. The fix is allowlist-based rather than a targeted regex tweak, so it closes the reported vector and forecloses adjacent ones (U+2028 / U+2029 line separators in `.op`, line terminators in comments, unknown-shape objects coerced through `.replace`). ### Workarounds Prior to upgrading, callers that build object tokens from untrusted input should validate `.op` against the parser's operator set themselves, and never construct `{ op }` from attacker-controlled strings. ### Credits Reported by Akshat Sinha
La función 'quote()' de shell-quote no validaba las entradas de token de objeto contra el modelo de operador utilizado por 'parse()'. El campo '.op' se escapaba con barra invertida carácter por carácter usando '/(.)/g', lo que en JavaScript no coincide con los terminadores de línea ('\n', '\r', 'U+2028', 'U+2029'). Un terminador de línea en '.op' por lo tanto pasaba sin escapar a la salida; los shells POSIX tratan un salto de línea literal como un separador de comandos, por lo que cualquier contenido después de él se ejecutaría como un segundo comando. La ruta de código vulnerable es alcanzable de dos maneras: (1) construcción directa de '{ op: '...\n...' }' desde entrada externa, y (2) a través de 'parse(cmd, envFn)' cuando 'envFn' devuelve tokens de objeto cuyo '.op' está influenciado por el atacante. Ambas son superficies de API documentadas. Solucionado reemplazando el escape por carácter con validación estricta de forma: '.op' debe coincidir con la lista de permitidos de operadores de control del analizador; '{ op: 'glob', pattern }' valida 'pattern' y prohíbe los terminadores de línea; '{ comment }' valida 'comment' y prohíbe los terminadores de línea; cualquier otra forma de objeto lanza 'TypeError'.
| Version | Type | Source | Base | Exp | Impact | Vector |
|---|---|---|---|---|---|---|
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Primary | cve.org | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Secondary | NVD | 8.1 | 2.2 | 5.9 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Secondary | GHSA | 8.1 | — | — | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 3.1 | Secondary | NVD | 8.1 | 2.2 | 5.9 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 4.0 | Primary | cve.org | 9.2 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N |
| 4.0 | Secondary | ENISA EUVD | 9.2 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N |
| 4.0 | Secondary | NVD | 9.2 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X |
| 4.0 | Secondary | GHSA | 9.2 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N |