[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