Hello, I've prepared node-shell-quote 1.7.4+~1.7.1-1+deb12u2 for bookworm-security, and it's ready for review.
DSA-6548-1 shipped both fixes in node-shell-quote 1.7.4+~1.7.1-1+deb13u2 in Trixie. node-shell-quote 1.12.0-1 in Sid includes the same upstream commits, 7ff5488599 from 1.9.0 and 6002b2ed90 from 1.11.0. The debdiff against 1.7.4+~1.7.1-1+deb12u1 is attached, and so is the draft DLA. - CVE-2026-102422: https://github.com/ljharb/shell-quote/security/advisories/GHSA-pqg4-j6r4-53mv - CVE-2026-13311: https://github.com/ljharb/shell-quote/security/advisories/GHSA-395f-4hp3-45gv Bookworm's node-shell-quote 1.7.4+~1.7.1-1+deb12u1 includes comment quoting added by the CVE-2026-9277 fix in DSA-6300-1. Although upstream lists CVE-2026-102422 as affecting versions from 1.8.4, that earlier backport makes Bookworm's version vulnerable too. This update makes quote() throw a TypeError if a string after a comment contains a line break (\n, \r, U+2028, or U+2029). It also changes the parser to build its result in a single array, without changing the tokens it returns. I built and installed the update in a Bookworm schroot. With the archive package, passing echo, a comment, and a later string containing a newline to quote() produced a command that ran the upstream example's id command under sh, bash, and dash. The updated package rejects that input with a TypeError, while a newline before the comment stays inside a single quoted argument. Parsing 16,000 plain words took 425 ms with the archive package and 18 ms with the update; the update also handled 200,000 words in 124 ms. Both versions returned matching tokens for environment substitutions, comments, and globs. The archive package passed all 143 tests, and the update passed all 155, including under sbuild and autopkgtest. I also checked Browserify's use of the library. Bookworm's node-browserify 17.0.0+ds-5 uses it to parse --command strings before running them as source transforms. Bundling a one-line module through both cat and sed -n '1,999p' worked with the archive package and the update. node-tap 16.3.2+ds1+~cs50.8.16-1+deb12u1 also depends on node-shell-quote; in the installed Bookworm tree, the module is loaded by Browserify's command parser. Feedback is welcome. If nothing showstopping comes up, I'll upload this and issue the DLA on Thursday afternoon (November 12th, 2026). Best regards, Simon Quigley [email protected]
diff -Nru node-shell-quote-1.7.4+~1.7.1/debian/changelog node-shell-quote-1.7.4+~1.7.1/debian/changelog --- node-shell-quote-1.7.4+~1.7.1/debian/changelog 2026-05-26 08:45:25.000000000 -0500 +++ node-shell-quote-1.7.4+~1.7.1/debian/changelog 2026-11-08 16:08:27.000000000 -0600 @@ -1,3 +1,11 @@ +node-shell-quote (1.7.4+~1.7.1-1+deb12u2) bookworm-security; urgency=high + + * Team upload. + * CVE-2026-102422: Reject a line break in a string after a comment. + * CVE-2026-13311: Finalize parsed tokens in linear time. + + -- Simon Quigley <[email protected]> Sun, 08 Nov 2026 16:08:43 -0600 + node-shell-quote (1.7.4+~1.7.1-1+deb12u1) bookworm-security; urgency=medium * Team upload diff -Nru node-shell-quote-1.7.4+~1.7.1/debian/patches/CVE-2026-102422.patch node-shell-quote-1.7.4+~1.7.1/debian/patches/CVE-2026-102422.patch --- node-shell-quote-1.7.4+~1.7.1/debian/patches/CVE-2026-102422.patch 1969-12-31 18:00:00.000000000 -0600 +++ node-shell-quote-1.7.4+~1.7.1/debian/patches/CVE-2026-102422.patch 2026-11-07 10:18:41.000000000 -0600 @@ -0,0 +1,100 @@ +Origin: backport, https://github.com/ljharb/shell-quote/commit/6002b2ed90c6b83095eb272b6b0adaf3a172b0bc +Reviewed-by: Simon Quigley <[email protected]> +Last-Update: 2026-11-07 + +Backported to 1.7.4. quote() and parse() still share index.js, and +this tree has no quote.d.ts. The check sits in front of the comment +branch. + +From 6002b2ed90c6b83095eb272b6b0adaf3a172b0bc Mon Sep 17 00:00:00 2001 +From: Jordan Harband <[email protected]> +Date: Sun, 27 Sep 2026 18:05:08 +0900 +Subject: [PATCH] [Fix] `quote`: reject line terminators in tokens after a + `comment` + +A `{ comment }` token emits `#...`, which comments out the rest of the line, +including the opening quote of any later string token. +A line terminator inside that later string ends the comment, +and the rest of the string is parsed as shell input: +`quote(['echo', { comment: 'x' }, 'a\nid;#'])` runs `id` in sh, bash, zsh, ksh, and dash. + +`parse` emits a comment for a mid-word `#` (e.g. `http://x/#frag`), +so appending tokens to `parse` output is enough to reach it. + +The line-terminator check added for `comment` in 4378a6e only covered the comment's own text. +Now any string after a comment is checked with the same line-terminator set. +Tokens after a comment without line terminators are unchanged. +--- a/README.md ++++ b/README.md +@@ -111,8 +111,9 @@ + `parse` emits: `{ op }` (where `op` is one of the control operators + `||`, `&&`, `;;`, `|&`, `<(`, `<<<`, `>>`, `>&`, `<&`, `&`, `;`, `(`, + `)`, `|`, `<`, `>`), `{ op: 'glob', pattern }`, or `{ comment }`. Any +-other object shape, an unrecognized `op`, or a `pattern`/`comment` +-containing line terminators throws a `TypeError`. ++other object shape, an unrecognized `op`, a `pattern`/`comment` ++containing line terminators, or a string containing line terminators ++anywhere after a `{ comment }` throws a `TypeError`. + + ## parse(cmd, env={}) + +--- a/index.js ++++ b/index.js +@@ -22,7 +22,11 @@ + var GLOB_SHELL_SPECIAL = /[\s#!"$&'():;<=>@\\^`|]/g; + + exports.quote = function (xs) { ++ var sawComment = false; + return xs.map(function (s) { ++ if (sawComment && typeof s === 'string' && LINE_TERMINATORS.test(s)) { ++ throw new TypeError('a token after a `comment` must not contain line terminators'); ++ } + if (s && typeof s === 'object') { + if (s.op === 'glob') { + if (typeof s.pattern !== 'string') { +@@ -43,6 +47,7 @@ + if (LINE_TERMINATORS.test(s.comment)) { + throw new TypeError('`comment` must not contain line terminators'); + } ++ sawComment = true; + return '#' + s.comment; + } + throw new TypeError('unrecognized object token shape'); +--- a/test/quote.js ++++ b/test/quote.js +@@ -2,6 +2,7 @@ + + var test = require('tape'); + var quote = require('../').quote; ++var parse = require('../').parse; + + test('quote', function (t) { + t.equal(quote(['a', 'b', 'c d']), 'a b \'c d\''); +@@ -98,6 +99,27 @@ + t.end(); + }); + ++test('quote comment: rejects line terminators in later tokens', function (t) { ++ t['throws'](function () { quote(['echo', { comment: 'x' }, 'a\nid;#']); }, TypeError, 'newline after a comment'); ++ t['throws'](function () { quote(['echo', { comment: 'x' }, 'a\rb']); }, TypeError, 'CR after a comment'); ++ t['throws'](function () { quote(['echo', { comment: 'x' }, 'a\u2028b']); }, TypeError, 'U+2028 after a comment'); ++ t['throws'](function () { quote(['echo', { comment: 'x' }, 'a\u2029b']); }, TypeError, 'U+2029 after a comment'); ++ t['throws']( ++ function () { quote(['echo', { comment: 'x' }, 'ok', 'it\'s\nid;#']); }, ++ TypeError, ++ 'newline in any later token, not just the next one' ++ ); ++ t['throws']( ++ function () { quote(parse('curl http://x/#frag').concat('a\nid;#')); }, ++ TypeError, ++ 'a mid-word `#` from parse, followed by an appended token' ++ ); ++ ++ t.equal(quote(['echo', 'a\nb', { comment: 'x' }]), 'echo \'a\nb\' #x', 'a line terminator before a comment is fine'); ++ t.equal(quote(['echo', { comment: 'x' }, 'y']), 'echo #x y', 'later tokens without line terminators are unchanged'); ++ t.end(); ++}); ++ + test('quote rejects unrecognized object shapes', function (t) { + t['throws'](function () { quote([{}]); }, TypeError, 'empty object'); + t['throws'](function () { quote([{ foo: 'bar' }]); }, TypeError, 'unknown key'); diff -Nru node-shell-quote-1.7.4+~1.7.1/debian/patches/CVE-2026-13311.patch node-shell-quote-1.7.4+~1.7.1/debian/patches/CVE-2026-13311.patch --- node-shell-quote-1.7.4+~1.7.1/debian/patches/CVE-2026-13311.patch 1969-12-31 18:00:00.000000000 -0600 +++ node-shell-quote-1.7.4+~1.7.1/debian/patches/CVE-2026-13311.patch 2026-11-07 10:19:11.000000000 -0600 @@ -0,0 +1,89 @@ +Origin: backport, https://github.com/ljharb/shell-quote/commit/7ff5488599d01c323514f02f5efb74088dd134ec +Reviewed-by: Simon Quigley <[email protected]> +Last-Update: 2026-11-07 + +Backported to 1.7.4. Both finalizers still live in index.js. The +environment-function path still matches the placeholder with an +inline RegExp, and an undefined token is still skipped with +arg === undefined. + +From 7ff5488599d01c323514f02f5efb74088dd134ec Mon Sep 17 00:00:00 2001 +From: Jordan Harband <[email protected]> +Date: Wed, 24 Jun 2026 16:35:02 -0700 +Subject: [PATCH] [Fix] `parse`: finalize tokens in linear time + (GHSA-395f-4hp3-45gv) + +The two finalizing reduces used `prev.concat(arg)` as the accumulator, +reallocating and copying the whole array each iteration, +so `parse()` ran in O(n²) in the token count +— a small (~128 KB) input could block the event loop for tens of seconds (CWE-407 DoS). +Push into the accumulator instead, +flattening one-level array entries with `forEach` +(not `.apply`, which can overflow the argument limit on large inputs) +Output is byte-identical; +`engines >= 0.4` is preserved (no `.flat()`). +--- a/index.js ++++ b/index.js +@@ -223,7 +223,10 @@ + if (arg === undefined) { + return prev; + } +- return prev.concat(arg); ++ [].concat(arg).forEach(function (entry) { ++ prev[prev.length] = entry; ++ }); ++ return prev; + }, []); + } + +@@ -234,17 +237,19 @@ + } + return mapped.reduce(function (acc, s) { + if (typeof s === 'object') { +- return acc.concat(s); ++ acc[acc.length] = s; ++ return acc; + } + var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g')); + if (xs.length === 1) { +- return acc.concat(xs[0]); ++ acc[acc.length] = xs[0]; ++ return acc; + } +- return acc.concat(xs.filter(Boolean).map(function (x) { +- if (RegExp('^' + TOKEN).test(x)) { +- return JSON.parse(x.split(TOKEN)[1]); +- } +- return x; +- })); ++ xs.filter(Boolean).forEach(function (x) { ++ acc[acc.length] = RegExp('^' + TOKEN).test(x) ++ ? JSON.parse(x.split(TOKEN)[1]) ++ : x; ++ }); ++ return acc; + }, []); + }; +--- a/test/parse.js ++++ b/test/parse.js +@@ -23,3 +23,20 @@ + + t.end(); + }); ++ ++test('parse stays linear in token count (GHSA-395f-4hp3-45gv)', function (t) { ++ // the old concat-in-reduce finalizer was O(n^2): this many tokens took ++ // ~minutes, so under the unfixed code this test hangs rather than passes ++ var n = 2e5; ++ var input = new Array(n + 1).join('x '); // avoid String#repeat for old engines ++ ++ var words = parse(input); ++ t.equal(words.length, n, 'every token is returned'); ++ t.equal(words[0], 'x', 'first token is correct'); ++ t.equal(words[n - 1], 'x', 'last token is correct'); ++ ++ var withEnv = parse(input, function () { return 'v'; }); ++ t.equal(withEnv.length, n, 'env-function path returns every token'); ++ ++ t.end(); ++}); diff -Nru node-shell-quote-1.7.4+~1.7.1/debian/patches/series node-shell-quote-1.7.4+~1.7.1/debian/patches/series --- node-shell-quote-1.7.4+~1.7.1/debian/patches/series 2026-05-23 04:49:31.000000000 -0500 +++ node-shell-quote-1.7.4+~1.7.1/debian/patches/series 2026-11-07 10:19:14.000000000 -0600 @@ -1 +1,3 @@ CVE-2026-9277.patch +CVE-2026-102422.patch +CVE-2026-13311.patch
------------------------------------------------------------------------- Debian LTS Advisory DLA-XXXX-1 [email protected] https://www.debian.org/lts/security/ Simon Quigley November 12, 2026 https://wiki.debian.org/LTS ------------------------------------------------------------------------- Package : node-shell-quote Version : 1.7.4+~1.7.1-1+deb12u2 CVE ID : CVE-2026-102422 CVE-2026-13311 node-shell-quote quotes and parses shell commands for Node.js programs. An attacker who can supply the text a program quotes after a comment can run a shell command. An attacker who can supply the text a program parses can stall that program. CVE-2026-102422 quote() turns a comment token into a shell comment. That comment runs to the end of the line, including the opening quote of a later string. A line break in that later string ends the comment, and the rest of the string is shell syntax. CVE-2026-13311 parse() copied the whole token list again for every token it kept. The work grows with the square of the number of tokens. For Debian 12 bookworm, these problems have been fixed in version 1.7.4+~1.7.1-1+deb12u2. We recommend that you upgrade your node-shell-quote packages. For the detailed security status of node-shell-quote please refer to its security tracker page at: https://security-tracker.debian.org/tracker/node-shell-quote Further information about Debian LTS security advisories, how to apply these updates to your system, and frequently asked questions can be found at: https://wiki.debian.org/LTS
OpenPGP_signature.asc
Description: OpenPGP digital signature
