Skip to content

Fix CVE-2026-93749: validate section offsets in indexed source maps - #78

Closed
aniebiet wants to merge 1 commit into
7rulnik:mainfrom
aniebiet:fix/cve-2026-93749-indexed-offset-validation
Closed

aniebiet wants to merge 1 commit into
7rulnik:mainfrom
aniebiet:fix/cve-2026-93749-indexed-offset-validation

Conversation

@aniebiet

Copy link
Copy Markdown

IndexedSourceMapConsumer read a section's offset.line/offset.column without validating them beyond a relative ordering check. Downstream consumers of a parsed map (e.g. SourceNode.fromStringWithSourceMap and SourceMapGenerator's mappings serializer) walk generated line numbers one at a time via while loops keyed off these offsets, so an attacker-supplied offset that is extremely large (or Infinity/NaN) can force a synchronous, near-unbounded loop and block the Node.js event loop for extended periods (CWE-1284, CVSS 7.5/8.7).

This adds validation when a section offset is read: it must be a finite, non-negative integer, and is capped at 2,000,000 (far beyond any realistic generated file's line/column count) so that even a maximal accepted value keeps the worst-case synchronous work bounded to roughly a hundred milliseconds instead of being unbounded.

Adds regression tests covering non-finite, negative, non-integer, non-numeric, and over-the-cap offset values, plus a sanity check that normal, small offsets are still accepted.

IndexedSourceMapConsumer read a section's offset.line/offset.column
without validating them beyond a relative ordering check. Downstream
consumers of a parsed map (e.g. SourceNode.fromStringWithSourceMap and
SourceMapGenerator's mappings serializer) walk generated line numbers
one at a time via while loops keyed off these offsets, so an
attacker-supplied offset that is extremely large (or Infinity/NaN) can
force a synchronous, near-unbounded loop and block the Node.js event
loop for extended periods (CWE-1284, CVSS 7.5/8.7).

This adds validation when a section offset is read: it must be a
finite, non-negative integer, and is capped at 2,000,000 (far beyond
any realistic generated file's line/column count) so that even a
maximal accepted value keeps the worst-case synchronous work bounded
to roughly a hundred milliseconds instead of being unbounded.

Adds regression tests covering non-finite, negative, non-integer,
non-numeric, and over-the-cap offset values, plus a sanity check that
normal, small offsets are still accepted.
@aniebiet
aniebiet marked this pull request as draft September 23, 2026 21:39
@aniebiet
aniebiet marked this pull request as ready for review September 23, 2026 22:13

@zurcacielos zurcacielos left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action: Implements the missing validation for section offsets.

Result: Mitigates the vulnerability by preventing the arbitrary offset values that trigger the event loop blocking condition. It is the correct and necessary fix.

@EdwardPayne

Copy link
Copy Markdown

Tested this locally — the fix works, but the branch currently conflicts with main, so here is a port that applies cleanly in case it saves some time.

Verification

Repro: a one-section indexed map with a large offset.line, passed to SourceNode.fromStringWithSourceMap() (Node 22, Linux).

offset.line 1.2.1 main (c1cd890) this PR
1,000,000 50 ms 21 ms allowed (26–44 ms)
20,000,000 403 ms 408 ms rejected immediately
100,000,000 1,888 ms 1,868 ms rejected immediately

Blocking time scales linearly (~19 ms per million lines) on both 1.2.1 and current main, and with this change the oversized offsets throw in the constructor before any loop runs.

Merge conflict

The PR is based on the 1.2.1 tag. Since then main has moved the tests to test/public/ and switched them to node:test, so GitHub reports a conflict:

  • lib/source-map-consumer.js — applies cleanly, no changes needed
  • test/test-source-map-consumer.js — conflicts; the two new tests need to move to test/public/test-source-map-consumer.js and use test('…', () => { … }) instead of exports['…'] = function (assert) { … }

Port onto current main

Same change, only the tests are converted. With it applied, node --test 'test/public/test-*.js' 'test/internal/test-*.js' passes 203/203. I did not run the conformance suite (it needs the dev dependencies installed).

Patch against main (c1cd890)
diff --git a/lib/source-map-consumer.js b/lib/source-map-consumer.js
index d25b494..a391dc6 100644
--- a/lib/source-map-consumer.js
+++ b/lib/source-map-consumer.js
@@ -15,6 +15,17 @@ var ArraySet = require('./array-set').ArraySet;
 // `_names._array`.
 var base64DecodeTable = require('./base64').charToIntMap;
 
+// Upper bound for a section's `offset.line`/`offset.column` in an indexed
+// source map. Consumers of a parsed map (e.g. SourceNode.fromStringWithSourceMap,
+// SourceMapGenerator's mappings serializer) walk generated line numbers one at
+// a time, so an unvalidated, attacker-supplied offset that is large (or
+// Infinity/NaN) can force a synchronous loop with an effectively unbounded
+// iteration count, blocking the event loop (CVE-2026-93749). 2 million is
+// already far beyond the line/column count of any realistic generated file
+// (or of a value legitimately used as a section offset), while keeping the
+// worst-case synchronous loop bounded to roughly a hundred milliseconds.
+var MAX_VALID_OFFSET_LINE = 2e6;
+
 function SourceMapConsumer(aSourceMap, aSourceMapURL) {
   var sourceMap = aSourceMap;
   if (typeof aSourceMap === 'string') {
@@ -1494,6 +1505,26 @@ function IndexedSourceMapConsumer(aSourceMap, aSourceMapURL) {
     var offsetLine = util.getArg(offset, 'line');
     var offsetColumn = util.getArg(offset, 'column');
 
+    // Reject non-integer, negative, or unreasonably large offset values.
+    // Section offsets are used elsewhere (e.g. when generating output or
+    // walking generated lines) in loops that advance one line at a time,
+    // so an unvalidated, attacker-controlled offset.line (for example
+    // Infinity or a huge number) can cause synchronous, near-unbounded
+    // looping and block the event loop. MAX_VALID_OFFSET_LINE is a
+    // generous but finite ceiling well beyond any legitimate source file.
+    if (typeof offsetLine !== 'number' ||
+        typeof offsetColumn !== 'number' ||
+        !isFinite(offsetLine) ||
+        !isFinite(offsetColumn) ||
+        !Number.isInteger(offsetLine) ||
+        !Number.isInteger(offsetColumn) ||
+        offsetLine < 0 ||
+        offsetColumn < 0 ||
+        offsetLine > MAX_VALID_OFFSET_LINE ||
+        offsetColumn > MAX_VALID_OFFSET_LINE) {
+      throw new Error('Section offset must be a non-negative integer within a reasonable range.');
+    }
+
     if (offsetLine < lastOffset.line ||
         (offsetLine === lastOffset.line && offsetColumn < lastOffset.column)) {
       throw new Error('Section offsets must be ordered and non-overlapping.');
diff --git a/test/public/test-source-map-consumer.js b/test/public/test-source-map-consumer.js
index 558e07c..7b0d7f8 100644
--- a/test/public/test-source-map-consumer.js
+++ b/test/public/test-source-map-consumer.js
@@ -919,6 +919,63 @@ test('test indexed source map errors when sections are out of order by line', ()
   }, Error);
 });
 
+// CVE-2026-93749: source-map-js failed to validate the per-section
+// `offset.line`/`offset.column` values in indexed source maps, letting an
+// attacker supply extremely large (or non-finite/non-integer) offset values
+// that cause synchronous, near-unbounded looping in downstream consumers
+// (e.g. SourceNode.fromStringWithSourceMap), blocking the event loop.
+test('test indexed source map rejects unreasonably large section offsets (CVE-2026-93749)', () => {
+  var badOffsets = [
+    Infinity,
+    -Infinity,
+    NaN,
+    1e9,
+    2e6 + 1, // just over the allowed maximum
+    -1,
+    1.5,
+    '5', // not a number
+    null
+  ];
+
+  badOffsets.forEach(function(offsetLine) {
+    var map = JSON.parse(JSON.stringify(util.indexedTestMap));
+    map.sections[0].offset = {
+      line: offsetLine,
+      column: 0
+    };
+
+    assert.throws(function() {
+      new SourceMapConsumer(map);
+    }, Error, 'offset.line = ' + offsetLine + ' should be rejected');
+  });
+
+  // Bad offset.column values should be rejected the same way.
+  var badColumns = [Infinity, NaN, 2e6 + 1, -1, 1.5, '5'];
+  badColumns.forEach(function(offsetColumn) {
+    var map = JSON.parse(JSON.stringify(util.indexedTestMap));
+    map.sections[0].offset = {
+      line: 0,
+      column: offsetColumn
+    };
+
+    assert.throws(function() {
+      new SourceMapConsumer(map);
+    }, Error, 'offset.column = ' + offsetColumn + ' should be rejected');
+  });
+});
+
+test('test indexed source map accepts valid, reasonably-sized section offsets', () => {
+  var map = JSON.parse(JSON.stringify(util.indexedTestMap));
+  map.sections[0].offset = {
+    line: 0,
+    column: 0
+  };
+
+  assert.doesNotThrow(function() {
+    new SourceMapConsumer(map);
+  });
+});
+
 test('test github issue #64', () => {
   var map = new SourceMapConsumer({
     "version": 3,

Apply with git apply on top of main. @aniebiet feel free to pull this into the branch; @7rulnik happy to open it as a separate PR if that is easier to merge.

@7rulnik

7rulnik commented Sep 30, 2026

Copy link
Copy Markdown
Owner

closed in favour #79

@7rulnik 7rulnik closed this Sep 30, 2026
meta-codesync Bot pushed a commit to facebook/memlab that referenced this pull request Oct 9, 2026
Summary:
Bumps [[https://github.com/7rulnik/source-map-js | source-map-js]] from 1.2.1 to 1.2.2.

== Release notes ==

//Sourced from [[https://github.com/7rulnik/source-map-js/releases | source-map-js's releases]].//

> **v1.2.2**
>
> - Fix crash when executing in browser with CSP script-src that don't permit unsafe-eval ([[7rulnik/source-map-js#29 | #29]]) [[https://github.com/xfournet | @​xfournet]]
> - Fix denial of service from malicious indexed source maps (CVE-2026-93749) ([[7rulnik/source-map-js#79 | #79]]) Reported by [[https://github.com/waydeshi | @​waydeshi]] in [[7rulnik/source-map-js#76 | #76]]. A fix was also proposed by [[https://github.com/aniebiet | @​aniebiet]] in [[7rulnik/source-map-js#78 | #78]].

== Changelog ==

//Sourced from [[https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md | source-map-js's changelog]].//

> **1.2.2**
>
> - Fix crash when executing in browser with CSP script-src that don't permit unsafe-eval ([[7rulnik/source-map-js#29 | #29]]) [[https://github.com/xfournet | @​xfournet]]
> - Fix denial of service from malicious indexed source maps (CVE-2026-93749) ([[7rulnik/source-map-js#79 | #79]]) Reported by [[https://github.com/waydeshi | @​waydeshi]] in [[7rulnik/source-map-js#76 | #76]]. A fix was also proposed by [[https://github.com/aniebiet | @​aniebiet]] in [[7rulnik/source-map-js#78 | #78]].

== Commits ==

- [[7rulnik/source-map-js@0a1d334 | 0a1d334]] 1.2.2
- [[7rulnik/source-map-js@4c6fa26 | 4c6fa26]] Update changelog
- [[7rulnik/source-map-js@cf76580 | cf76580]] Fix denial of service from malicious indexed source maps (CVE-2026-93749) ([[7rulnik/source-map-js#79 | #79]])
- [[7rulnik/source-map-js@7899a86 | 7899a86]] Fix crash when executing browser with CSP script-src that don't permit unsafe...
- See full diff in [[7rulnik/source-map-js@v1.2.1...v1.2.2 | compare view]]

Pull Request resolved: #161

Reviewed By: boujeepossum

Differential Revision: D124273775

Pulled By: JacksonGL

fbshipit-source-id: db3ef2c2a265f6211e8bbcf6cda1ca4c3b890c6e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants