Skip to content

Commit

Permalink
fix(core): introduce SafeRegExp to primordials (#17592)
Browse files Browse the repository at this point in the history
  • Loading branch information
petamoriken authored Feb 28, 2023
1 parent 6ffbf8a commit 55833cf
Show file tree
Hide file tree
Showing 15 changed files with 171 additions and 60 deletions.
12 changes: 12 additions & 0 deletions cli/tests/unit/console_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2138,6 +2138,18 @@ Deno.test(async function inspectAggregateError() {
}
});

Deno.test(function inspectWithPrototypePollution() {
const originalExec = RegExp.prototype.exec;
try {
RegExp.prototype.exec = () => {
throw Error();
};
Deno.inspect("foo");
} finally {
RegExp.prototype.exec = originalExec;
}
});

Deno.test(function inspectorMethods() {
console.timeStamp("test");
console.profile("test");
Expand Down
15 changes: 15 additions & 0 deletions cli/tests/unit/headers_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,21 @@ Deno.test(function headersInitMultiple() {
]);
});

Deno.test(function headerInitWithPrototypePollution() {
const originalExec = RegExp.prototype.exec;
try {
RegExp.prototype.exec = () => {
throw Error();
};
new Headers([
["X-Deno", "foo"],
["X-Deno", "bar"],
]);
} finally {
RegExp.prototype.exec = originalExec;
}
});

Deno.test(function headersAppendMultiple() {
const headers = new Headers([
["Set-Cookie", "foo=bar"],
Expand Down
15 changes: 15 additions & 0 deletions cli/tests/unit/urlpattern_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,18 @@ Deno.test(function urlPatternFromInit() {

assert(pattern.test({ pathname: "/foo/x" }));
});

Deno.test(function urlPatternWithPrototypePollution() {
const originalExec = RegExp.prototype.exec;
try {
RegExp.prototype.exec = () => {
throw Error();
};
const pattern = new URLPattern({
pathname: "/foo/:bar",
});
assert(pattern.test("https://deno.land/foo/x"));
} finally {
RegExp.prototype.exec = originalExec;
}
});
9 changes: 9 additions & 0 deletions core/00_primordials.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,15 @@
},
);

primordials.SafeRegExp = makeSafe(
RegExp,
class SafeRegExp extends RegExp {
constructor(pattern, flags) {
super(pattern, flags);
}
},
);

primordials.SafeFinalizationRegistry = makeSafe(
FinalizationRegistry,
class SafeFinalizationRegistry extends FinalizationRegistry {
Expand Down
1 change: 1 addition & 0 deletions core/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ declare namespace __bootstrap {
export const SafePromisePrototypeFinally: UncurryThis<
Promise.prototype.finally
>;
export const SafeRegExp: typeof RegExp;

// safe iterators
export const SafeArrayIterator: new <T>(array: T[]) => IterableIterator<T>;
Expand Down
6 changes: 3 additions & 3 deletions ext/console/01_colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

const primordials = globalThis.__bootstrap.primordials;
const {
RegExp,
SafeRegExp,
StringPrototypeReplace,
ArrayPrototypeJoin,
} = primordials;
Expand All @@ -23,7 +23,7 @@ function code(open, close) {
return {
open: `\x1b[${open}m`,
close: `\x1b[${close}m`,
regexp: new RegExp(`\\x1b\\[${close}m`, "g"),
regexp: new SafeRegExp(`\\x1b\\[${close}m`, "g"),
};
}

Expand Down Expand Up @@ -74,7 +74,7 @@ function magenta(str) {
}

// https://github.com/chalk/ansi-regex/blob/02fa893d619d3da85411acc8fd4e2eea0e95a9d9/index.js
const ANSI_PATTERN = new RegExp(
const ANSI_PATTERN = new SafeRegExp(
ArrayPrototypeJoin([
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
Expand Down
85 changes: 59 additions & 26 deletions ext/console/02_console.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ const {
BooleanPrototype,
BooleanPrototypeToString,
ObjectKeys,
ObjectCreate,
ObjectAssign,
ObjectCreate,
ObjectFreeze,
ObjectIs,
ObjectValues,
ObjectFromEntries,
Expand Down Expand Up @@ -49,13 +50,13 @@ const {
TypeError,
NumberIsInteger,
NumberParseInt,
RegExp,
RegExpPrototype,
RegExpPrototypeTest,
RegExpPrototypeToString,
SafeArrayIterator,
SafeStringIterator,
SafeSet,
SafeRegExp,
SetPrototype,
SetPrototypeEntries,
SetPrototypeGetSize,
Expand Down Expand Up @@ -747,31 +748,34 @@ function quoteString(string) {
const quote =
ArrayPrototypeFind(QUOTES, (c) => !StringPrototypeIncludes(string, c)) ??
QUOTES[0];
const escapePattern = new RegExp(`(?=[${quote}\\\\])`, "g");
const escapePattern = new SafeRegExp(`(?=[${quote}\\\\])`, "g");
string = StringPrototypeReplace(string, escapePattern, "\\");
string = replaceEscapeSequences(string);
return `${quote}${string}${quote}`;
}

const ESCAPE_PATTERN = new SafeRegExp(/([\b\f\n\r\t\v])/g);
const ESCAPE_MAP = ObjectFreeze({
"\b": "\\b",
"\f": "\\f",
"\n": "\\n",
"\r": "\\r",
"\t": "\\t",
"\v": "\\v",
});

// deno-lint-ignore no-control-regex
const ESCAPE_PATTERN2 = new SafeRegExp(/[\x00-\x1f\x7f-\x9f]/g);

// Replace escape sequences that can modify output.
function replaceEscapeSequences(string) {
const escapeMap = {
"\b": "\\b",
"\f": "\\f",
"\n": "\\n",
"\r": "\\r",
"\t": "\\t",
"\v": "\\v",
};

return StringPrototypeReplace(
StringPrototypeReplace(
string,
/([\b\f\n\r\t\v])/g,
(c) => escapeMap[c],
ESCAPE_PATTERN,
(c) => ESCAPE_MAP[c],
),
// deno-lint-ignore no-control-regex
/[\x00-\x1f\x7f-\x9f]/g,
new SafeRegExp(ESCAPE_PATTERN2),
(c) =>
"\\x" +
StringPrototypePadStart(
Expand All @@ -782,22 +786,33 @@ function replaceEscapeSequences(string) {
);
}

const QUOTE_STRING_PATTERN = new SafeRegExp(/^[a-zA-Z_][a-zA-Z_0-9]*$/);

// Surround a string with quotes when it is required (e.g the string not a valid identifier).
function maybeQuoteString(string) {
if (RegExpPrototypeTest(/^[a-zA-Z_][a-zA-Z_0-9]*$/, string)) {
if (
RegExpPrototypeTest(QUOTE_STRING_PATTERN, string)
) {
return replaceEscapeSequences(string);
}

return quoteString(string);
}

const QUOTE_SYMBOL_REG = new SafeRegExp(/^[a-zA-Z_][a-zA-Z_.0-9]*$/);

// Surround a symbol's description in quotes when it is required (e.g the description has non printable characters).
function maybeQuoteSymbol(symbol) {
if (symbol.description === undefined) {
return SymbolPrototypeToString(symbol);
}

if (RegExpPrototypeTest(/^[a-zA-Z_][a-zA-Z_.0-9]*$/, symbol.description)) {
if (
RegExpPrototypeTest(
QUOTE_SYMBOL_REG,
symbol.description,
)
) {
return SymbolPrototypeToString(symbol);
}

Expand Down Expand Up @@ -980,6 +995,9 @@ function inspectRegExp(value, inspectOptions) {
return red(RegExpPrototypeToString(value)); // RegExps are red
}

const AGGREGATE_ERROR_HAS_AT_PATTERN = new SafeRegExp(/\s+at/);
const AGGREGATE_ERROR_NOT_EMPTY_LINE_PATTERN = new SafeRegExp(/^(?!\s*$)/gm);

function inspectError(value, cyan) {
const causes = [value];

Expand Down Expand Up @@ -1012,7 +1030,7 @@ function inspectError(value, cyan) {
const stackLines = StringPrototypeSplit(value.stack, "\n");
while (true) {
const line = ArrayPrototypeShift(stackLines);
if (RegExpPrototypeTest(/\s+at/, line)) {
if (RegExpPrototypeTest(AGGREGATE_ERROR_HAS_AT_PATTERN, line)) {
ArrayPrototypeUnshift(stackLines, line);
break;
} else if (typeof line === "undefined") {
Expand All @@ -1028,7 +1046,7 @@ function inspectError(value, cyan) {
(error) =>
StringPrototypeReplace(
inspectArgs([error]),
/^(?!\s*$)/gm,
AGGREGATE_ERROR_NOT_EMPTY_LINE_PATTERN,
StringPrototypeRepeat(" ", 4),
),
),
Expand Down Expand Up @@ -1519,12 +1537,25 @@ const colorKeywords = new Map([
["rebeccapurple", "#663399"],
]);

const HASH_PATTERN = new SafeRegExp(
/^#([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})?$/,
);
const SMALL_HASH_PATTERN = new SafeRegExp(
/^#([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])?$/,
);
const RGB_PATTERN = new SafeRegExp(
/^rgba?\(\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)\s*(,\s*([+\-]?\d*\.?\d+)\s*)?\)$/,
);
const HSL_PATTERN = new SafeRegExp(
/^hsla?\(\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)%\s*,\s*([+\-]?\d*\.?\d+)%\s*(,\s*([+\-]?\d*\.?\d+)\s*)?\)$/,
);

function parseCssColor(colorString) {
if (MapPrototypeHas(colorKeywords, colorString)) {
colorString = MapPrototypeGet(colorKeywords, colorString);
}
// deno-fmt-ignore
const hashMatch = StringPrototypeMatch(colorString, /^#([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})([\dA-Fa-f]{2})?$/);
const hashMatch = StringPrototypeMatch(colorString, HASH_PATTERN);
if (hashMatch != null) {
return [
Number(`0x${hashMatch[1]}`),
Expand All @@ -1533,7 +1564,7 @@ function parseCssColor(colorString) {
];
}
// deno-fmt-ignore
const smallHashMatch = StringPrototypeMatch(colorString, /^#([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])([\dA-Fa-f])?$/);
const smallHashMatch = StringPrototypeMatch(colorString, SMALL_HASH_PATTERN);
if (smallHashMatch != null) {
return [
Number(`0x${smallHashMatch[1]}0`),
Expand All @@ -1542,7 +1573,7 @@ function parseCssColor(colorString) {
];
}
// deno-fmt-ignore
const rgbMatch = StringPrototypeMatch(colorString, /^rgba?\(\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)\s*(,\s*([+\-]?\d*\.?\d+)\s*)?\)$/);
const rgbMatch = StringPrototypeMatch(colorString, RGB_PATTERN);
if (rgbMatch != null) {
return [
MathRound(MathMax(0, MathMin(255, Number(rgbMatch[1])))),
Expand All @@ -1551,7 +1582,7 @@ function parseCssColor(colorString) {
];
}
// deno-fmt-ignore
const hslMatch = StringPrototypeMatch(colorString, /^hsla?\(\s*([+\-]?\d*\.?\d+)\s*,\s*([+\-]?\d*\.?\d+)%\s*,\s*([+\-]?\d*\.?\d+)%\s*(,\s*([+\-]?\d*\.?\d+)\s*)?\)$/);
const hslMatch = StringPrototypeMatch(colorString, HSL_PATTERN);
if (hslMatch != null) {
// https://www.rapidtables.com/convert/color/hsl-to-rgb.html
let h = Number(hslMatch[1]) % 360;
Expand Down Expand Up @@ -1599,6 +1630,8 @@ function getDefaultCss() {
};
}

const SPACE_PATTERN = new SafeRegExp(/\s+/g);

function parseCss(cssString) {
const css = getDefaultCss();

Expand Down Expand Up @@ -1665,7 +1698,7 @@ function parseCss(cssString) {
}
} else if (key == "text-decoration-line") {
css.textDecorationLine = [];
const lineTypes = StringPrototypeSplit(value, /\s+/g);
const lineTypes = StringPrototypeSplit(value, SPACE_PATTERN);
for (let i = 0; i < lineTypes.length; ++i) {
const lineType = lineTypes[i];
if (
Expand All @@ -1685,7 +1718,7 @@ function parseCss(cssString) {
} else if (key == "text-decoration") {
css.textDecorationColor = null;
css.textDecorationLine = [];
const args = StringPrototypeSplit(value, /\s+/g);
const args = StringPrototypeSplit(value, SPACE_PATTERN);
for (let i = 0; i < args.length; ++i) {
const arg = args[i];
const maybeColor = parseCssColor(arg);
Expand Down
3 changes: 2 additions & 1 deletion ext/fetch/20_headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const {
ObjectEntries,
RegExpPrototypeTest,
SafeArrayIterator,
SafeRegExp,
Symbol,
SymbolFor,
SymbolIterator,
Expand Down Expand Up @@ -88,7 +89,7 @@ function fillHeaders(headers, object) {

// Regex matching illegal chars in a header value
// deno-lint-ignore no-control-regex
const ILLEGAL_VALUE_CHARS = /[\x00\x0A\x0D]/;
const ILLEGAL_VALUE_CHARS = new SafeRegExp(/[\x00\x0A\x0D]/);

/**
* https://fetch.spec.whatwg.org/#concept-headers-append
Expand Down
Loading

0 comments on commit 55833cf

Please sign in to comment.