Skip to content

Commit 22a9679

Browse files
authored
Reject large optional route combinations (#424)
1 parent 8881a88 commit 22a9679

4 files changed

Lines changed: 25 additions & 11 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"size-limit": [
5151
{
5252
"path": "dist/index.js",
53-
"limit": "2.3 kB"
53+
"limit": "2.15 kB"
5454
}
5555
],
5656
"ts-scripts": {

src/index.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@ describe("path-to-regexp", () => {
166166
expect(stack).toContain("index.spec.ts");
167167
}
168168
});
169+
170+
it("should throw when too many alternative routes are generated", () => {
171+
const segments = new Array(10).fill("{/x}").join("");
172+
173+
expect(() => pathToRegexp(segments)).toThrow(
174+
new PathError("Too many path combinations", segments),
175+
);
176+
});
169177
});
170178

171179
describe("stringify errors", () => {

src/index.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -463,10 +463,9 @@ export function pathToRegexp(
463463
sensitive = false,
464464
trailing = true,
465465
} = options;
466-
const flags = sensitive ? "" : "i";
467-
const keys: Keys = [];
468466
const root = new SourceNode("^");
469467
const paths: Array<Path | Path[]> = [path];
468+
let combinations = 0;
470469

471470
while (paths.length) {
472471
const path = paths.shift()!;
@@ -477,8 +476,11 @@ export function pathToRegexp(
477476
}
478477

479478
const data = typeof path === "object" ? path : parse(path, options);
480-
481479
flatten(data.tokens, 0, [], (tokens) => {
480+
if (combinations++ >= 256) {
481+
throw new PathError("Too many path combinations", data.originalPath);
482+
}
483+
482484
let node = root;
483485

484486
for (const part of toRegExpSource(tokens, delimiter, data.originalPath)) {
@@ -489,11 +491,12 @@ export function pathToRegexp(
489491
});
490492
}
491493

494+
const keys: Keys = [];
492495
let pattern = toRegExp(root, keys);
493496
if (trailing) pattern += "(?:" + escape(delimiter) + "$)?";
494497
pattern += end ? "$" : "(?=" + escape(delimiter) + "|$)";
495498

496-
return { regexp: new RegExp(pattern, flags), keys };
499+
return { regexp: new RegExp(pattern, sensitive ? "" : "i"), keys };
497500
}
498501

499502
function toRegExp(node: SourceNode, keys: Keys): string {
@@ -504,7 +507,7 @@ function toRegExp(node: SourceNode, keys: Keys): string {
504507
.map((id) => toRegExp(node.children[id], keys))
505508
.join("|");
506509

507-
return node.source + (children.length <= 1 ? text : `(?:${text})`);
510+
return node.source + (children.length < 2 ? text : `(?:${text})`);
508511
}
509512

510513
class SourceNode {
@@ -532,13 +535,15 @@ function flatten(
532535
): void {
533536
while (index < tokens.length) {
534537
const token = tokens[index++];
538+
535539
if (token.type === "group") {
536-
flatten(token.tokens, 0, result.slice(), (seq) => {
537-
flatten(tokens, index, seq, callback);
538-
});
539-
} else {
540-
result.push(token);
540+
flatten(token.tokens, 0, result.slice(), (seq) =>
541+
flatten(tokens, index, seq, callback),
542+
);
543+
continue;
541544
}
545+
546+
result.push(token);
542547
}
543548

544549
callback(result);

src/redos.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { describe, expect, it } from "vitest";
66
describe.concurrent("redos", () => {
77
it.each(MATCH_TESTS.map((x) => [x.path, pathToRegexp(x.path).regexp]))(
88
"%s - %s",
9+
{ timeout: 10_000 },
910
async (_, regexp) => {
1011
const result = await check(regexp.source, regexp.flags);
1112
expect(result.status).toBe("safe");

0 commit comments

Comments
 (0)