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

Attachment: OpenPGP_signature.asc
Description: OpenPGP digital signature

Reply via email to