[Pkg-javascript-devel] Bug#1137372: Trixie/Bookworm patch for CVE-2026-9277

Xavier yadd at debian.org
Sat May 23 11:08:01 BST 2026


Hi,

here is a patch for Trixie. Same can be applied to bookworm, just to 
change dist/version

Best regards,
xavier
-------------- next part --------------
diff --git a/debian/changelog b/debian/changelog
index d3e8cfa..05e6b17 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+node-shell-quote (1.7.4+~1.7.1-1+deb13u1) trixie-security; urgency=medium
+
+  * Team upload
+  * Validate object-token shapes (Closes: #1137372, CVE-2026-9277)
+
+ -- Xavier Guimard <yadd at debian.org>  Sat, 23 May 2026 11:56:08 +0200
+
 node-shell-quote (1.7.4+~1.7.1-1) unstable; urgency=medium
 
   * Team upload
diff --git a/debian/patches/CVE-2026-9277.patch b/debian/patches/CVE-2026-9277.patch
new file mode 100644
index 0000000..86f765d
--- /dev/null
+++ b/debian/patches/CVE-2026-9277.patch
@@ -0,0 +1,155 @@
+Description: [PATCH] [Fix] `quote`: validate object-token shapes
+ The per-character `.op` escape (`/(.)/g`) did not match line terminators
+ (`\n`, `\r`, U+2028, U+2029),
+ so a literal newline in `.op` passed through unescaped and acted as a shell
+ command separator.
+ Replace the regex with strict shape validation:
+ - `{ op }`: `.op` must match the parser's control-operator allowlist
+ - `{ op: 'glob', pattern }`: `.pattern` must be a string without line
+   terminators; glob metas pass through, shell-special chars are escaped
+   (Previously the field was discarded and the literal `\g\l\o\b` was emitted)
+ - `{ comment }`: `.comment` must be a string without line terminators
+   (Previously `quote` crashed on `{ comment }` tokens that `parse` emits)
+ - Any other object shape: `TypeError`
+Author: Jordan Harband <ljharb at gmail.com>
+Origin: upstream, https://github.com/ljharb/shell-quote/commit/4378a6e6
+Bug: https://github.com/ljharb/shell-quote/security/advisories/GHSA-w7jw-789q-3m8p
+Bug-Debian: https://bugs.debian.org/1137372
+Forwarded: not-needed
+Applied-Upstream: 1.8.4, commit:4378a6e6
+Reviewed-By: Yadd <yadd at debian.org>
+Last-Update: 2026-05-23
+
+--- a/README.md
++++ b/README.md
+@@ -107,6 +107,13 @@
+ Return a quoted string for the array `args` suitable for using in shell
+ commands.
+ 
++Each entry of `args` may be a string, or one of the object shapes that
++`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`.
++
+ ## parse(cmd, env={})
+ 
+ Return an array of arguments from the quoted string `cmd`.
+--- a/index.js
++++ b/index.js
+@@ -1,9 +1,51 @@
+ 'use strict';
+ 
++var OPS = [
++	'||',
++	'&&',
++	';;',
++	'|&',
++	'<(',
++	'<<<',
++	'>>',
++	'>&',
++	'<&',
++	'&',
++	';',
++	'(',
++	')',
++	'|',
++	'<',
++	'>'
++];
++var LINE_TERMINATORS = /[\n\r\u2028\u2029]/;
++var GLOB_SHELL_SPECIAL = /[\s#!"$&'():;<=>@\\^`|]/g;
++
+ exports.quote = function (xs) {
+ 	return xs.map(function (s) {
+ 		if (s && typeof s === 'object') {
+-			return s.op.replace(/(.)/g, '\\$1');
++			if (s.op === 'glob') {
++				if (typeof s.pattern !== 'string') {
++					throw new TypeError('glob token requires a string `pattern`');
++				}
++				if (LINE_TERMINATORS.test(s.pattern)) {
++					throw new TypeError('glob `pattern` must not contain line terminators');
++				}
++				return s.pattern.replace(GLOB_SHELL_SPECIAL, '\\$&');
++			}
++			if (typeof s.op === 'string') {
++				if (OPS.indexOf(s.op) < 0) {
++					throw new TypeError('invalid `op` value: ' + JSON.stringify(s.op));
++				}
++				return s.op.replace(/[\s\S]/g, '\\$&');
++			}
++			if (typeof s.comment === 'string') {
++				if (LINE_TERMINATORS.test(s.comment)) {
++					throw new TypeError('`comment` must not contain line terminators');
++				}
++				return '#' + s.comment;
++			}
++			throw new TypeError('unrecognized object token shape');
+ 		} else if ((/["\s]/).test(s) && !(/'/).test(s)) {
+ 			return "'" + s.replace(/(['\\])/g, '\\$1') + "'";
+ 		} else if ((/["'\s]/).test(s)) {
+--- a/test/quote.js
++++ b/test/quote.js
+@@ -48,3 +48,59 @@
+ 	t.equal(quote([x]), '\\`\\:\\\\a\\\\b');
+ 	t.end();
+ });
++
++test('quote ops: allowlist', function (t) {
++	var ops = ['||', '&&', ';;', '|&', '<(', '<<<', '>>', '>&', '<&', '&', ';', '(', ')', '|', '<', '>'];
++	for (var i = 0; i < ops.length; i++) {
++		var op = ops[i];
++		var expected = '';
++		for (var j = 0; j < op.length; j++) { expected += '\\' + op.charAt(j); }
++		t.equal(quote([{ op: op }]), expected, 'op ' + op);
++	}
++	t.end();
++});
++
++test('quote ops: rejects line terminators (GHSA-w7jw-789q-3m8p)', function (t) {
++	t['throws'](function () { quote([{ op: ';\nid' }]); }, TypeError, 'newline in op');
++	t['throws'](function () { quote([{ op: ';\rid' }]); }, TypeError, 'carriage return in op');
++	t['throws'](function () { quote([{ op: ';\u2028id' }]); }, TypeError, 'U+2028 in op');
++	t['throws'](function () { quote([{ op: ';\u2029id' }]); }, TypeError, 'U+2029 in op');
++	t.end();
++});
++
++test('quote ops: rejects non-allowlisted values', function (t) {
++	t['throws'](function () { quote([{ op: '' }]); }, TypeError, 'empty op');
++	t['throws'](function () { quote([{ op: 'foo' }]); }, TypeError, 'arbitrary string');
++	t['throws'](function () { quote([{ op: '|||' }]); }, TypeError, 'near-miss');
++	t['throws'](function () { quote([{ op: 42 }]); }, TypeError, 'non-string op');
++	t.end();
++});
++
++test('quote glob pattern', function (t) {
++	t.equal(quote([{ op: 'glob', pattern: 'test/*.test.js' }]), 'test/*.test.js');
++	t.equal(quote([{ op: 'glob', pattern: '?ab' }]), '?ab');
++	t.equal(quote([{ op: 'glob', pattern: '[ab]c' }]), '[ab]c');
++	t.equal(quote([{ op: 'glob', pattern: '{a,b}' }]), '{a,b}');
++	t.equal(quote([{ op: 'glob', pattern: 'my dir/*.txt' }]), 'my\\ dir/*.txt');
++	t.equal(quote([{ op: 'glob', pattern: 'a$b' }]), 'a\\$b');
++	t['throws'](function () { quote([{ op: 'glob' }]); }, TypeError, 'missing pattern');
++	t['throws'](function () { quote([{ op: 'glob', pattern: 'a\nb' }]); }, TypeError, 'newline in pattern');
++	t['throws'](function () { quote([{ op: 'glob', pattern: 'a\u2028b' }]); }, TypeError, 'U+2028 in pattern');
++	t.end();
++});
++
++test('quote comment', function (t) {
++	t.equal(quote(['echo', 'hi', { comment: ' a comment' }]), 'echo hi # a comment');
++	t.equal(quote([{ comment: '' }]), '#');
++	t['throws'](function () { quote([{ comment: 'a\nb' }]); }, TypeError, 'newline in comment');
++	t['throws'](function () { quote([{ comment: 'a\rb' }]); }, TypeError, 'CR in comment');
++	t['throws'](function () { quote([{ comment: 'a\u2028b' }]); }, TypeError, 'U+2028 in comment');
++	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');
++	t['throws'](function () { quote([{ op: null }]); }, TypeError, 'null op');
++	t.end();
++});
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 0000000..fba295f
--- /dev/null
+++ b/debian/patches/series
@@ -0,0 +1 @@
+CVE-2026-9277.patch


More information about the Pkg-javascript-devel mailing list