Skip to content

Commit 42092fc

Browse files
cynarlabclaude
andcommitted
fix(dart): linear regexes for the main( scan and env-var trailing separators (CodeQL, #790)
CodeQL flagged two polynomial-ReDoS sites on PR #880: - adapter-policy-dart.ts: the string-literal blanking regex used a backreference form whose escape branch overlapped its "not the quote" branch; an unterminated literal of escaped quotes backtracked polynomially. Replaced by the disjoint form "(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*' in one pass, same semantics. - sdk-locator.ts: `/[\\/]+$/` on an env var is quadratic on a long run of separators; stripped with a loop instead. Guard tests: 20 000 escaped quotes before `main(`, and 40 000 trailing separators on DART_SDK. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 8b39cff commit 42092fc

4 files changed

Lines changed: 31 additions & 2 deletions

File tree

‎packages/adapter-dart/src/utils/sdk-locator.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ export function locateToolchain(io: LocatorIo): DartToolchain {
7373
/** Accept either the install root or the executable itself for an env var. */
7474
const rootFromEnv = (value: string | undefined, exeName: string): string | undefined => {
7575
if (!value) return undefined;
76-
const trimmed = value.trim().replace(/[\\/]+$/, '');
76+
// Trailing separators stripped by hand: `/[\\/]+$/` backtracks quadratically on a long run
77+
// of separators (CodeQL polynomial-ReDoS).
78+
let trimmed = value.trim();
79+
while (trimmed.endsWith('/') || trimmed.endsWith('\\')) trimmed = trimmed.slice(0, -1);
7780
if (trimmed.toLowerCase().endsWith(exeName.toLowerCase())) return up(p, trimmed, 2);
7881
return trimmed;
7982
};

‎packages/adapter-dart/tests/unit/sdk-locator.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ describe('locateToolchain (win32)', () => {
5454
expect(t.flutterRoot).toBe(root);
5555
});
5656

57+
it('strips trailing separators from an env var, however many (no quadratic regex)', () => {
58+
const sdk = 'C:\\tools\\dart-sdk';
59+
const t = locateToolchain(io({
60+
env: { DART_SDK: sdk + '\\'.repeat(20_000) + '/'.repeat(20_000) },
61+
files: [W(sdk, 'bin', 'dart.exe')],
62+
}));
63+
expect(t.dartSdkRoot).toBe(sdk);
64+
expect(t.dartSource).toBe('env:DART_SDK');
65+
});
66+
5767
it('ignores an env var whose directory has no SDK and keeps looking', () => {
5868
const t = locateToolchain(io({
5969
env: { FLUTTER_ROOT: 'C:\\nope' },

‎packages/shared/src/interfaces/adapter-policy-dart.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@ function isAsyncGap(frame: StackFrame): boolean {
3636

3737
/** The `main(` declaration; the VM binds a breakpoint on that line to main's first statement. */
3838
const MAIN_DECLARATION = /\bmain\s*\(/;
39+
/**
40+
* A string literal of either quote kind. The alternatives inside are disjoint (a backslash is
41+
* consumed only by the escape branch), so the match is linear in the line — CodeQL's
42+
* polynomial-ReDoS check rejects the backreference form `(["'])(?:\\.|(?!\1).)*\1`.
43+
*/
44+
const STRING_LITERAL = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
3945
/** A line with its `//` comment and string literals blanked, so `main(` inside them does not count. */
4046
const codeOnly = (line: string): string =>
41-
line.replace(/(["'])(?:\\.|(?!\1).)*\1/g, '""').replace(/\/\/.*$/, '');
47+
line.replace(STRING_LITERAL, '""').replace(/\/\/.*$/, '');
4248

4349
export const DartAdapterPolicy = {
4450
name: 'dart',

‎packages/shared/tests/unit/adapter-policy-dart.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ describe('DartAdapterPolicy', () => {
3737
expect(DartAdapterPolicy.entryBreakpointLine('final remains = 1; // "main(" in a comment should not count\nvoid main() {}\n')).toBe(2);
3838
});
3939

40+
it('blanks string literals without backtracking: escaped quotes, an unterminated literal, both quote kinds', () => {
41+
// `main(` inside a literal never counts, whatever the escaping.
42+
expect(DartAdapterPolicy.entryBreakpointLine('final s = "main(\\" main(";\nvoid main() {}\n')).toBe(2);
43+
expect(DartAdapterPolicy.entryBreakpointLine("final s = 'it\\'s main(';\nvoid main() {}\n")).toBe(2);
44+
// An unterminated literal made of thousands of escaped quotes (CodeQL's polynomial-ReDoS
45+
// shape for the former backreference regex) is handled, and the next line still wins.
46+
const hostile = 'final s = "' + '\\"'.repeat(20_000) + ';\nvoid main() {}\n';
47+
expect(DartAdapterPolicy.entryBreakpointLine(hostile)).toBe(2);
48+
});
49+
4050
it('maps break-on-exception modes to the SDK filter ids', () => {
4151
expect(resolveExceptionFilters(DartAdapterPolicy, 'uncaught')).toEqual(['Unhandled']);
4252
expect(resolveExceptionFilters(DartAdapterPolicy, 'all')).toEqual(['All']);

0 commit comments

Comments
 (0)