Skip to content

Commit aa2469d

Browse files
committed
Add backtrack protection to 6.x
1 parent 28a5b27 commit aa2469d

File tree

4 files changed

+141
-54
lines changed

4 files changed

+141
-54
lines changed

package-lock.json

Lines changed: 102 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@types/node": "^20.4.9",
3737
"@types/semver": "^7.3.1",
3838
"@vitest/coverage-v8": "^1.4.0",
39+
"recheck": "^4.4.5",
3940
"semver": "^7.3.5",
4041
"size-limit": "^11.1.2",
4142
"typescript": "^5.1.6"

src/index.spec.ts

Lines changed: 14 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,7 @@ const TESTS: Test[] = [
13531353
prefix: ".",
13541354
suffix: "",
13551355
modifier: "+",
1356-
pattern: "[^\\/#\\?]+?",
1356+
pattern: "(?:(?!\\.)[^\\/#\\?])+?",
13571357
},
13581358
],
13591359
[
@@ -1397,7 +1397,7 @@ const TESTS: Test[] = [
13971397
prefix: ".",
13981398
suffix: "",
13991399
modifier: "",
1400-
pattern: "[^\\/#\\?]+?",
1400+
pattern: "(?:(?!\\.)[^\\/#\\?])+?",
14011401
},
14021402
".",
14031403
],
@@ -1430,13 +1430,13 @@ const TESTS: Test[] = [
14301430
prefix: ".",
14311431
suffix: "",
14321432
modifier: "",
1433-
pattern: "[^\\/#\\?]+?",
1433+
pattern: "(?:(?!\\.)[^\\/#\\?])+?",
14341434
},
14351435
],
14361436
[
14371437
["/route.html", ["/route.html", "route", "html"]],
14381438
["/route", null],
1439-
["/route.html.json", ["/route.html.json", "route", "html.json"]],
1439+
["/route.html.json", ["/route.html.json", "route.html", "json"]],
14401440
],
14411441
[
14421442
[{}, null],
@@ -1459,13 +1459,13 @@ const TESTS: Test[] = [
14591459
prefix: ".",
14601460
suffix: "",
14611461
modifier: "?",
1462-
pattern: "[^\\/#\\?]+?",
1462+
pattern: "(?:(?!\\.)[^\\/#\\?])+?",
14631463
},
14641464
],
14651465
[
14661466
["/route", ["/route", "route", undefined]],
14671467
["/route.json", ["/route.json", "route", "json"]],
1468-
["/route.json.html", ["/route.json.html", "route", "json.html"]],
1468+
["/route.json.html", ["/route.json.html", "route.json", "html"]],
14691469
],
14701470
[
14711471
[{ test: "route" }, "/route"],
@@ -1491,13 +1491,13 @@ const TESTS: Test[] = [
14911491
prefix: ".",
14921492
suffix: "",
14931493
modifier: "?",
1494-
pattern: "[^\\/#\\?]+?",
1494+
pattern: "(?:(?!\\.)[^\\/#\\?])+?",
14951495
},
14961496
],
14971497
[
14981498
["/route", ["/route", "route", undefined]],
14991499
["/route.json", ["/route.json", "route", "json"]],
1500-
["/route.json.html", ["/route.json.html", "route", "json.html"]],
1500+
["/route.json.html", ["/route.json.html", "route.json", "html"]],
15011501
],
15021502
[
15031503
[{ test: "route" }, "/route"],
@@ -2084,7 +2084,7 @@ const TESTS: Test[] = [
20842084
prefix: "",
20852085
suffix: "",
20862086
modifier: "?",
2087-
pattern: "[^\\/#\\?]+?",
2087+
pattern: "(?:(?!\\()[^\\/#\\?])+?",
20882088
},
20892089
")",
20902090
],
@@ -2290,7 +2290,7 @@ const TESTS: Test[] = [
22902290
prefix: ".",
22912291
suffix: "",
22922292
modifier: "",
2293-
pattern: "[^\\/#\\?]+?",
2293+
pattern: "(?:(?!\\.)[^\\/#\\?])+?",
22942294
},
22952295
],
22962296
[
@@ -2356,14 +2356,14 @@ const TESTS: Test[] = [
23562356
[
23572357
{
23582358
name: "foo",
2359-
pattern: "[^\\/#\\?]+?",
2359+
pattern: "(?:(?!\\$)[^\\/#\\?])+?",
23602360
prefix: "$",
23612361
suffix: "",
23622362
modifier: "",
23632363
},
23642364
{
23652365
name: "bar",
2366-
pattern: "[^\\/#\\?]+?",
2366+
pattern: "(?:(?!\\$)[^\\/#\\?])+?",
23672367
prefix: "$",
23682368
suffix: "",
23692369
modifier: "?",
@@ -2392,14 +2392,14 @@ const TESTS: Test[] = [
23922392
},
23932393
{
23942394
name: "attr2",
2395-
pattern: "[^\\/#\\?]+?",
2395+
pattern: "(?:(?!-)[^\\/#\\?])+?",
23962396
prefix: "-",
23972397
suffix: "",
23982398
modifier: "?",
23992399
},
24002400
{
24012401
name: "attr3",
2402-
pattern: "[^\\/#\\?]+?",
2402+
pattern: "(?:(?!-)[^\\/#\\?])+?",
24032403
prefix: "-",
24042404
suffix: "",
24052405
modifier: "?",
@@ -2597,39 +2597,6 @@ const TESTS: Test[] = [
25972597
[{ foo: "#" }, null],
25982598
],
25992599
],
2600-
/**
2601-
* https://github.com/pillarjs/path-to-regexp/issues/260
2602-
*/
2603-
[
2604-
":name*",
2605-
undefined,
2606-
[
2607-
{
2608-
name: "name",
2609-
prefix: "",
2610-
suffix: "",
2611-
modifier: "*",
2612-
pattern: "[^\\/#\\?]+?",
2613-
},
2614-
],
2615-
[["foobar", ["foobar", "foobar"]]],
2616-
[[{ name: "foobar" }, "foobar"]],
2617-
],
2618-
[
2619-
":name+",
2620-
undefined,
2621-
[
2622-
{
2623-
name: "name",
2624-
prefix: "",
2625-
suffix: "",
2626-
modifier: "+",
2627-
pattern: "[^\\/#\\?]+?",
2628-
},
2629-
],
2630-
[["foobar", ["foobar", "foobar"]]],
2631-
[[{ name: "foobar" }, "foobar"]],
2632-
],
26332600
];
26342601

26352602
/**

src/index.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ export interface ParseOptions {
139139
*/
140140
export function parse(str: string, options: ParseOptions = {}): Token[] {
141141
const tokens = lexer(str);
142-
const { prefixes = "./" } = options;
143-
const defaultPattern = `[^${escapeString(options.delimiter || "/#?")}]+?`;
142+
const { prefixes = "./", delimiter = "/#?" } = options;
144143
const result: Token[] = [];
145144
let key = 0;
146145
let i = 0;
147146
let path = "";
147+
let backtrack = "";
148148

149149
const tryConsume = (type: LexToken["type"]): string | undefined => {
150150
if (i < tokens.length && tokens[i].type === type) return tokens[i++].value;
@@ -166,6 +166,17 @@ export function parse(str: string, options: ParseOptions = {}): Token[] {
166166
return result;
167167
};
168168

169+
const isSafe = (value: string): boolean => {
170+
for (const char of delimiter) if (value.includes(char)) return true;
171+
return false;
172+
};
173+
174+
const safePattern = (prefix: string, backtrack: string) => {
175+
const prevText = prefix || backtrack;
176+
if (!prevText || isSafe(prevText)) return `[^${escapeString(delimiter)}]+?`;
177+
return `(?:(?!${escapeString(prevText)})[^${escapeString(delimiter)}])+?`;
178+
};
179+
169180
while (i < tokens.length) {
170181
const char = tryConsume("CHAR");
171182
const name = tryConsume("NAME");
@@ -181,16 +192,18 @@ export function parse(str: string, options: ParseOptions = {}): Token[] {
181192

182193
if (path) {
183194
result.push(path);
195+
backtrack = path;
184196
path = "";
185197
}
186198

187199
result.push({
188200
name: name || key++,
189201
prefix,
190202
suffix: "",
191-
pattern: pattern || defaultPattern,
203+
pattern: pattern || safePattern(prefix, backtrack),
192204
modifier: tryConsume("MODIFIER") || "",
193205
});
206+
backtrack = "";
194207
continue;
195208
}
196209

@@ -202,6 +215,7 @@ export function parse(str: string, options: ParseOptions = {}): Token[] {
202215

203216
if (path) {
204217
result.push(path);
218+
backtrack = path;
205219
path = "";
206220
}
207221

@@ -216,11 +230,12 @@ export function parse(str: string, options: ParseOptions = {}): Token[] {
216230

217231
result.push({
218232
name: name || (pattern ? key++ : ""),
219-
pattern: name && !pattern ? defaultPattern : pattern,
233+
pattern: name && !pattern ? safePattern(prefix, backtrack) : pattern,
220234
prefix,
221235
suffix,
222236
modifier: tryConsume("MODIFIER") || "",
223237
});
238+
backtrack = suffix;
224239
continue;
225240
}
226241

@@ -564,10 +579,12 @@ export function tokensToRegexp(
564579
}
565580
} else {
566581
if (token.modifier === "+" || token.modifier === "*") {
567-
route += `((?:${token.pattern})${token.modifier})`;
568-
} else {
569-
route += `(${token.pattern})${token.modifier}`;
582+
throw new TypeError(
583+
`Can not repeat ${token.name} with no prefix or suffix, e.g. "/:param${token.modifier}"`,
584+
);
570585
}
586+
587+
route += `(${token.pattern})${token.modifier}`;
571588
}
572589
} else {
573590
route += `(?:${prefix}${suffix})${token.modifier}`;

0 commit comments

Comments
 (0)