CVE-2026-102422: the comment that commented out a quote
The command injection I reported in shell-quote's quote() — the function you call so an untrusted value cannot become shell syntax. A { comment } token emits a #, which comments out the opening quote of the next argument; a line terminator inside that argument ends the comment and the rest runs as a command. Reachable through parse() with no comment token in sight, and the half of an earlier fix that only covered the comment's own text. Fixed in 1.11.0.
Disclaimer. This is personal security research, carried out on my own equipment against a public open-source project. It has no connection to any current or former employer, and no client, employer or production environment was involved at any point. Nothing in it touched a network: every result is a call to a library function in a shell on my own machine. The issue was reported and fixed through the project’s security advisory process, and is written up here after the advisory and the fixed release were public.
shell-quote has two halves. parse() reads a shell command line into an array of tokens; quote() runs the other way, taking an array of tokens and producing a string the shell will read back as exactly those tokens and nothing else. quote() is the safe direction — the one an application reaches for precisely so that an untrusted value becomes one inert argument rather than shell syntax. CVE-2026-102422, which I reported to the project, is a way to make quote() produce a string that runs a command.
The comment that comments out a quote
shell-quote’s token vocabulary includes a comment: { comment: 'text' }, which quote() emits as #text. In a shell, # starts a comment that runs to the end of the line, so a comment token sitting in the middle of an argument array comments out everything quote() emits after it on that line — including the opening single quote of the next string token.
On its own that only loses arguments. What turns it into an injection is a line terminator. quote() wraps an ordinary string in single quotes to keep its contents literal; place a string token after the comment, and its opening quote is on the commented line, inert. Its closing quote is wherever the string’s own bytes put it. If the string contains a newline, the comment ends at that newline — and everything after it is no longer commented. It is live shell input, and the opening quote that was supposed to make it a literal was swallowed by the #:
quote(['echo', 'ok', { comment: 'x' }, 'a\nid;#']);
// echo ok #x 'a
// id;#'
The first line is a comment. The second line is id;#' — a command, followed by a # that comments out the orphaned closing quote so the shell never complains about it. Handed to sh, bash, dash, ksh or zsh, that string runs id. The single quote quote() added to make a\nid;# a safe literal is doing nothing on the line above; its partner is stranded on the line below, and the shell never sees the two as a pair. The function whose entire job is to keep the shell from misreading a string has assembled a command out of two tokens it was asked to hold apart.
It does not need a hand-built comment token
A caller does not have to construct { comment } tokens deliberately for this to be reachable. parse() emits a comment token for any # that appears in the middle of a word — the fragment in a URL like http://example.com/#frag, for instance. So the ordinary round-trip shape
quote(parse(untrustedCommand).concat(untrustedArg));
reaches the injection with no comment token anywhere in the caller’s own code: the # comes from one untrusted string, and the line terminator from another. A program that parses a command, appends an argument, and re-quotes the result — a routine thing to do — is exposed without ever naming a comment.
The half an earlier fix did not cover
This is the second line-terminator bug in quote()’s comment handling, and the first one is why the mechanism was already half-guarded. CVE-2026-9277 concerned newlines in a comment’s own text, and its fix — commit 4378a6e — added a check that rejects line terminators inside the comment. That check looked only at the comment’s own text. The tokens that come after the comment, where a newline does the same damage through the swallowed quote, were never examined. The guard was in the right function and one argument short of the defect it was written to prevent — so the same shape it closed for the comment stayed open for everything that followed the comment.
The fix
quote() now applies the same line-terminator set (\n, \r, U+2028 and U+2029) to any string that follows a { comment } token, throwing a TypeError rather than emitting the ambiguous line. The change is 6002b2e, and it shipped in 1.11.0, the current release on npm. The affected range is >= 1.8.4, < 1.11.0 — every version since 4378a6e added the comment-text check that this one completes. The advisory also notes the plainer half of the same shape: do not append your own shell text after quote() output that contains a comment, because the comment will swallow it.
If you build shell strings with quote(), especially through parse() output you then extend: 1.11.0, or anything later.
Advisory GHSA-pqg4-j6r4-53mv · CVE CVE-2026-102422 · Fix 6002b2e · Parent CVE-2026-9277 · Fixed in 1.11.0
Reported to shell-quote through its security advisory process; fixed in 1.11.0. CWE-78, 8.1 on CVSS 3.1 and 9.2 on CVSS 4.0; CVE-2026-102422 assigned by the harborist CNA on 29 September 2026, crediting me as finder.
Eurico Nicacio — @h3llh0und