Skip to content

Commit d5952e8

Browse files
Fix processing of replacement pattern with named capture groups (#17173)
* Update index.spec.js test wrapRegExp * Update wrapRegExp.js * Update index.spec.js * fix: avoid branch ambiguity in substitution regex * move test to babel-runtime * fix: improve check * add more test cases * update artifacts * add common test case for #17174 * fix test * match the line terminator instead * fix: support hasOwnProperty as captured name * update artifacts * add more test cases * fixup update artifacts * shrink helper * update artifacts * make windows happy --------- Co-authored-by: Huáng Jùnliàng <[email protected]>
1 parent 143064d commit d5952e8

File tree

5 files changed

+146
-14
lines changed

5 files changed

+146
-14
lines changed

packages/babel-helpers/src/helpers-generated.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1414,10 +1414,10 @@ const helpers: Record<string, Helper> = {
14141414
},
14151415
},
14161416
),
1417-
// size: 1163, gzip size: 540
1417+
// size: 1213, gzip size: 560
14181418
wrapRegExp: helper(
14191419
"7.19.0",
1420-
'function _wrapRegExp(){_wrapRegExp=function(e,r){return new BabelRegExp(e,void 0,r)};var e=RegExp.prototype,r=new WeakMap;function BabelRegExp(e,t,p){var o=RegExp(e,t);return r.set(o,p||r.get(e)),setPrototypeOf(o,BabelRegExp.prototype)}function buildGroups(e,t){var p=r.get(t);return Object.keys(p).reduce((function(r,t){var o=p[t];if("number"==typeof o)r[t]=e[o];else{for(var i=0;void 0===e[o[i]]&&i+1<o.length;)i++;r[t]=e[o[i]]}return r}),Object.create(null))}return inherits(BabelRegExp,RegExp),BabelRegExp.prototype.exec=function(r){var t=e.exec.call(this,r);if(t){t.groups=buildGroups(t,this);var p=t.indices;p&&(p.groups=buildGroups(p,this))}return t},BabelRegExp.prototype[Symbol.replace]=function(t,p){if("string"==typeof p){var o=r.get(this);return e[Symbol.replace].call(this,t,p.replace(/\\$<([^>]+)>/g,(function(e,r){var t=o[r];return"$"+(Array.isArray(t)?t.join("$"):t)})))}if("function"==typeof p){var i=this;return e[Symbol.replace].call(this,t,(function(){var e=arguments;return"object"!=typeof e[e.length-1]&&(e=[].slice.call(e)).push(buildGroups(e,i)),p.apply(this,e)}))}return e[Symbol.replace].call(this,t,p)},_wrapRegExp.apply(this,arguments)}',
1420+
'function _wrapRegExp(){_wrapRegExp=function(e,r){return new BabelRegExp(e,void 0,r)};var e=RegExp.prototype,r=new WeakMap;function BabelRegExp(e,t,p){var o=RegExp(e,t);return r.set(o,p||r.get(e)),setPrototypeOf(o,BabelRegExp.prototype)}function buildGroups(e,t){var p=r.get(t);return Object.keys(p).reduce((function(r,t){var o=p[t];if("number"==typeof o)r[t]=e[o];else{for(var i=0;void 0===e[o[i]]&&i+1<o.length;)i++;r[t]=e[o[i]]}return r}),Object.create(null))}return inherits(BabelRegExp,RegExp),BabelRegExp.prototype.exec=function(r){var t=e.exec.call(this,r);if(t){t.groups=buildGroups(t,this);var p=t.indices;p&&(p.groups=buildGroups(p,this))}return t},BabelRegExp.prototype[Symbol.replace]=function(t,p){if("string"==typeof p){var o=r.get(this);return e[Symbol.replace].call(this,t,p.replace(/\\$<([^>]+)(>|$)/g,(function(e,r,t){if(""===t)return e;var p=o[r];return Array.isArray(p)?"$"+p.join("$"):"number"==typeof p?"$"+p:""})))}if("function"==typeof p){var i=this;return e[Symbol.replace].call(this,t,(function(){var e=arguments;return"object"!=typeof e[e.length-1]&&(e=[].slice.call(e)).push(buildGroups(e,i)),p.apply(this,e)}))}return e[Symbol.replace].call(this,t,p)},_wrapRegExp.apply(this,arguments)}',
14211421
{
14221422
globals: ["RegExp", "WeakMap", "Object", "Symbol", "Array"],
14231423
locals: {

packages/babel-helpers/src/helpers/wrapRegExp.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import inherits from "./inherits.ts";
55

66
// Define interfaces for clarity and type safety
77
interface GroupMap {
8-
[key: string]: string | [number, number];
8+
[key: string]: number | [number, number];
99
}
1010

1111
declare class BabelRegExp extends RegExp {
@@ -72,9 +72,18 @@ export default function _wrapRegExp(this: any): RegExp {
7272
).call(
7373
this,
7474
str,
75-
substitution.replace(/\$<([^>]+)>/g, function (_, name) {
76-
var group = groups[name];
77-
return "$" + (Array.isArray(group) ? group.join("$") : group);
75+
substitution.replace(/\$<([^>]+)(>|$)/g, function (match, name, end) {
76+
if (end === "") {
77+
// return unterminated group name as-is
78+
return match;
79+
} else {
80+
var group = groups[name];
81+
return Array.isArray(group)
82+
? "$" + group.join("$")
83+
: typeof group === "number"
84+
? "$" + group
85+
: "";
86+
}
7887
}),
7988
);
8089
} else if (typeof substitution === "function") {
@@ -116,13 +125,10 @@ export default function _wrapRegExp(this: any): RegExp {
116125
if (typeof i === "number") groups[name] = result[i];
117126
else {
118127
var k = 0;
119-
while (
120-
result[(i as [number, number])[k]] === undefined &&
121-
k + 1 < i.length
122-
) {
128+
while (result[i[k]] === undefined && k + 1 < i.length) {
123129
k++;
124130
}
125-
groups[name] = result[(i as [number, number])[k]];
131+
groups[name] = result[i[k]];
126132
}
127133
return groups;
128134
}, Object.create(null));

packages/babel-runtime-corejs3/helpers/esm/wrapRegExp.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ function _wrapRegExp() {
4242
}, BabelRegExp.prototype[_Symbol$replace] = function (t, p) {
4343
if ("string" == typeof p) {
4444
var o = r.get(this);
45-
return e[_Symbol$replace].call(this, t, p.replace(/\$<([^>]+)>/g, function (e, r) {
46-
var t = o[r];
47-
return "$" + (_Array$isArray(t) ? t.join("$") : t);
45+
return e[_Symbol$replace].call(this, t, p.replace(/\$<([^>]+)(>|$)/g, function (e, r, t) {
46+
if ("" === t) return e;
47+
var p = o[r];
48+
return _Array$isArray(p) ? "$" + p.join("$") : "number" == typeof p ? "$" + p : "";
4849
}));
4950
}
5051
if ("function" == typeof p) {
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "module"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import wrapRegExp from "../../helpers/wrapRegExp.js";
2+
3+
describe("wrapRegExp", () => {
4+
describe("should handle maliciously crafted substitutions", () => {
5+
it("$<$<...$<group", () => {
6+
const pattern = "(foo)";
7+
const groups = { group: 1 };
8+
const myRegExp = wrapRegExp(pattern, groups);
9+
const targetStr = "foofoo";
10+
const replacement = "$<".repeat(1e5) + "group";
11+
const startTime = Date.now();
12+
const result = myRegExp[Symbol.replace](targetStr, replacement);
13+
// This test will fail if 2000ms is passed
14+
const timeTaken = Date.now() - startTime;
15+
expect(timeTaken).toBeLessThan(2000);
16+
expect(result).toBe(replacement + "foo");
17+
});
18+
19+
it("$<$<...$<group>", () => {
20+
const pattern = "(foo)";
21+
const groups = { group: 1 };
22+
const myRegExp = wrapRegExp(pattern, groups);
23+
const targetStr = "foofoo";
24+
const replacement = "$<".repeat(1e5) + "group>";
25+
const startTime = Date.now();
26+
const result = myRegExp[Symbol.replace](targetStr, replacement);
27+
// This test will fail if 2000ms is passed
28+
const timeTaken = Date.now() - startTime;
29+
expect(timeTaken).toBeLessThan(2000);
30+
expect(result).toBe("foo");
31+
});
32+
33+
it("$<g$<g...$<group", () => {
34+
const pattern = "(foo)";
35+
const groups = { group: 1 };
36+
const myRegExp = wrapRegExp(pattern, groups);
37+
const targetStr = "foofoo";
38+
const replacement = "$<g".repeat(1e5) + "group";
39+
const startTime = Date.now();
40+
const result = myRegExp[Symbol.replace](targetStr, replacement);
41+
// This test will fail if 2000ms is passed
42+
const timeTaken = Date.now() - startTime;
43+
expect(timeTaken).toBeLessThan(2000);
44+
expect(result).toBe(replacement + "foo");
45+
});
46+
47+
it("$<g$<g...$<group>", () => {
48+
const pattern = "(foo)";
49+
const groups = { group: 1 };
50+
const myRegExp = wrapRegExp(pattern, groups);
51+
const targetStr = "foofoo";
52+
const replacement = "$<g".repeat(1e5) + "group>";
53+
const startTime = Date.now();
54+
const result = myRegExp[Symbol.replace](targetStr, replacement);
55+
// This test will fail when 2000ms is passed
56+
const timeTaken = Date.now() - startTime;
57+
expect(timeTaken).toBeLessThan(2000);
58+
expect(result).toBe("foo");
59+
});
60+
61+
it("$<_$>", () => {
62+
const pattern = "(foo)";
63+
const groups = { _$: 1 };
64+
const myRegExp = wrapRegExp(pattern, groups);
65+
const targetStr = "foobar";
66+
const replacement = "$<_$>$<_$>";
67+
const result = myRegExp[Symbol.replace](targetStr, replacement);
68+
expect(result).toBe("foofoobar");
69+
});
70+
71+
it("$<hasOwnProperty>", () => {
72+
const pattern = "(foo)";
73+
const groups = { hasOwnProperty: 1 };
74+
const myRegExp = wrapRegExp(pattern, groups);
75+
const targetStr = "foobar";
76+
const replacement = "$<hasOwnProperty>";
77+
const result = myRegExp[Symbol.replace](targetStr, replacement);
78+
expect(result).toBe("foobar");
79+
});
80+
81+
it("$<__proto__>", () => {
82+
const pattern = "(foo)";
83+
const groups = { ["__proto__"]: 1 };
84+
const myRegExp = wrapRegExp(pattern, groups);
85+
const targetStr = "foobar";
86+
const replacement = "$<__proto__>";
87+
const result = myRegExp[Symbol.replace](targetStr, replacement);
88+
expect(result).toBe("foobar");
89+
});
90+
});
91+
describe("substitutions", () => {
92+
it("unknown group", () => {
93+
const pattern = "(foo)";
94+
const groups = { group: 1 };
95+
const myRegExp = wrapRegExp(pattern, groups);
96+
const targetStr = "foobar";
97+
const replacement = "$<UNKNOWN>";
98+
const result = myRegExp[Symbol.replace](targetStr, replacement);
99+
expect(result).toBe("bar");
100+
});
101+
102+
it("$<hasOwnProperty> - unknown group", () => {
103+
const pattern = "(foo)";
104+
const groups = { group: 1 };
105+
const myRegExp = wrapRegExp(pattern, groups);
106+
const targetStr = "foobar";
107+
const replacement = "$<hasOwnProperty>";
108+
const result = myRegExp[Symbol.replace](targetStr, replacement);
109+
expect(result).toBe("bar");
110+
});
111+
112+
it("$<__proto__> -- unknown group", () => {
113+
const pattern = "(foo)";
114+
const groups = { group: 1 };
115+
const myRegExp = wrapRegExp(pattern, groups);
116+
const targetStr = "foobar";
117+
const replacement = "$<__proto__>";
118+
const result = myRegExp[Symbol.replace](targetStr, replacement);
119+
expect(result).toBe("bar");
120+
});
121+
});
122+
});

0 commit comments

Comments
 (0)