From 55833cf799979e63c6b027fbbf018272308caf5c Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Wed, 1 Mar 2023 08:14:16 +0900 Subject: [PATCH] fix(core): introduce `SafeRegExp` to primordials (#17592) --- cli/tests/unit/console_test.ts | 12 +++++ cli/tests/unit/headers_test.ts | 15 ++++++ cli/tests/unit/urlpattern_test.ts | 15 ++++++ core/00_primordials.js | 9 ++++ core/internal.d.ts | 1 + ext/console/01_colors.js | 6 +-- ext/console/02_console.js | 85 +++++++++++++++++++++---------- ext/fetch/20_headers.js | 3 +- ext/fetch/21_formdata.js | 34 +++++++++---- ext/fetch/23_response.js | 4 +- ext/url/01_urlpattern.js | 4 +- ext/web/00_infra.js | 20 ++++---- ext/web/09_file.js | 5 +- ext/webidl/00_webidl.js | 7 ++- runtime/js/06_util.js | 11 ++-- 15 files changed, 171 insertions(+), 60 deletions(-) diff --git a/cli/tests/unit/console_test.ts b/cli/tests/unit/console_test.ts index 239a8bf26c..2b7426b999 100644 --- a/cli/tests/unit/console_test.ts +++ b/cli/tests/unit/console_test.ts @@ -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"); diff --git a/cli/tests/unit/headers_test.ts b/cli/tests/unit/headers_test.ts index fda4e81d9f..295f030712 100644 --- a/cli/tests/unit/headers_test.ts +++ b/cli/tests/unit/headers_test.ts @@ -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"], diff --git a/cli/tests/unit/urlpattern_test.ts b/cli/tests/unit/urlpattern_test.ts index 1ec64b2fec..9bed092355 100644 --- a/cli/tests/unit/urlpattern_test.ts +++ b/cli/tests/unit/urlpattern_test.ts @@ -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; + } +}); diff --git a/core/00_primordials.js b/core/00_primordials.js index 6495a52684..243f40e88f 100644 --- a/core/00_primordials.js +++ b/core/00_primordials.js @@ -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 { diff --git a/core/internal.d.ts b/core/internal.d.ts index 004e068ffb..a91ac62447 100644 --- a/core/internal.d.ts +++ b/core/internal.d.ts @@ -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 (array: T[]) => IterableIterator; diff --git a/ext/console/01_colors.js b/ext/console/01_colors.js index d01edd2471..a598db9217 100644 --- a/ext/console/01_colors.js +++ b/ext/console/01_colors.js @@ -4,7 +4,7 @@ const primordials = globalThis.__bootstrap.primordials; const { - RegExp, + SafeRegExp, StringPrototypeReplace, ArrayPrototypeJoin, } = primordials; @@ -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"), }; } @@ -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=><~]))", diff --git a/ext/console/02_console.js b/ext/console/02_console.js index a8b335b948..6f708c71f0 100644 --- a/ext/console/02_console.js +++ b/ext/console/02_console.js @@ -17,8 +17,9 @@ const { BooleanPrototype, BooleanPrototypeToString, ObjectKeys, - ObjectCreate, ObjectAssign, + ObjectCreate, + ObjectFreeze, ObjectIs, ObjectValues, ObjectFromEntries, @@ -49,13 +50,13 @@ const { TypeError, NumberIsInteger, NumberParseInt, - RegExp, RegExpPrototype, RegExpPrototypeTest, RegExpPrototypeToString, SafeArrayIterator, SafeStringIterator, SafeSet, + SafeRegExp, SetPrototype, SetPrototypeEntries, SetPrototypeGetSize, @@ -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( @@ -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); } @@ -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]; @@ -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") { @@ -1028,7 +1046,7 @@ function inspectError(value, cyan) { (error) => StringPrototypeReplace( inspectArgs([error]), - /^(?!\s*$)/gm, + AGGREGATE_ERROR_NOT_EMPTY_LINE_PATTERN, StringPrototypeRepeat(" ", 4), ), ), @@ -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]}`), @@ -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`), @@ -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])))), @@ -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; @@ -1599,6 +1630,8 @@ function getDefaultCss() { }; } +const SPACE_PATTERN = new SafeRegExp(/\s+/g); + function parseCss(cssString) { const css = getDefaultCss(); @@ -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 ( @@ -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); diff --git a/ext/fetch/20_headers.js b/ext/fetch/20_headers.js index b8fd8ab835..a432e76f47 100644 --- a/ext/fetch/20_headers.js +++ b/ext/fetch/20_headers.js @@ -32,6 +32,7 @@ const { ObjectEntries, RegExpPrototypeTest, SafeArrayIterator, + SafeRegExp, Symbol, SymbolFor, SymbolIterator, @@ -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 diff --git a/ext/fetch/21_formdata.js b/ext/fetch/21_formdata.js index 647c716b3e..724511633e 100644 --- a/ext/fetch/21_formdata.js +++ b/ext/fetch/21_formdata.js @@ -26,7 +26,9 @@ const { MapPrototypeGet, MapPrototypeSet, MathRandom, + ObjectFreeze, ObjectPrototypeIsPrototypeOf, + SafeRegExp, Symbol, StringFromCharCode, StringPrototypeTrim, @@ -277,19 +279,25 @@ webidl.mixinPairIterable("FormData", FormData, entryList, "name", "value"); webidl.configurePrototype(FormData); const FormDataPrototype = FormData.prototype; -const escape = (str, isFilename) => { - const escapeMap = { - "\n": "%0A", - "\r": "%0D", - '"': "%22", - }; +const ESCAPE_FILENAME_PATTERN = new SafeRegExp(/\r?\n|\r/g); +const ESCAPE_PATTERN = new SafeRegExp(/([\n\r"])/g); +const ESCAPE_MAP = ObjectFreeze({ + "\n": "%0A", + "\r": "%0D", + '"': "%22", +}); +function escape(str, isFilename) { return StringPrototypeReplace( - isFilename ? str : StringPrototypeReplace(str, /\r?\n|\r/g, "\r\n"), - /([\n\r"])/g, - (c) => escapeMap[c], + isFilename + ? str + : StringPrototypeReplace(str, ESCAPE_FILENAME_PATTERN, "\r\n"), + ESCAPE_PATTERN, + (c) => ESCAPE_MAP[c], ); -}; +} + +const FORM_DETA_SERIALIZE_PATTERN = new SafeRegExp(/\r(?!\n)|(? { const x = converters.DOMString(V, opts); if (!RegExpPrototypeTest(IS_BYTE_STRING, x)) { @@ -500,7 +501,9 @@ ArrayPrototypeForEach( ], (func) => { const name = func.name; - const article = RegExpPrototypeTest(/^[AEIOU]/, name) ? "an" : "a"; + const article = RegExpPrototypeTest(new SafeRegExp(/^[AEIOU]/), name) + ? "an" + : "a"; converters[name] = (V, opts = {}) => { if (TypedArrayPrototypeGetSymbolToStringTag(V) !== name) { throw makeException( diff --git a/runtime/js/06_util.js b/runtime/js/06_util.js index 435a55a61f..c9bd86285a 100644 --- a/runtime/js/06_util.js +++ b/runtime/js/06_util.js @@ -7,6 +7,7 @@ const { ObjectPrototypeIsPrototypeOf, Promise, SafeArrayIterator, + SafeRegExp, StringPrototypeReplace, TypeError, } = primordials; @@ -49,7 +50,7 @@ function createResolvable() { function pathFromURLWin32(url) { let p = StringPrototypeReplace( url.pathname, - /^\/*([A-Za-z]:)(\/|$)/, + new SafeRegExp(/^\/*([A-Za-z]:)(\/|$)/), "$1/", ); p = StringPrototypeReplace( @@ -59,7 +60,7 @@ function pathFromURLWin32(url) { ); p = StringPrototypeReplace( p, - /%(?![0-9A-Fa-f]{2})/g, + new SafeRegExp(/%(?![0-9A-Fa-f]{2})/g), "%25", ); let path = decodeURIComponent(p); @@ -79,7 +80,11 @@ function pathFromURLPosix(url) { } return decodeURIComponent( - StringPrototypeReplace(url.pathname, /%(?![0-9A-Fa-f]{2})/g, "%25"), + StringPrototypeReplace( + url.pathname, + new SafeRegExp(/%(?![0-9A-Fa-f]{2})/g), + "%25", + ), ); }