Skip to content

Commit 7b3e38b

Browse files
aduh95injunchoi98
andcommitted
url: handle "unsafe" characters properly in pathToFileURL
Co-authored-by: EarlyRiser42 <[email protected]> PR-URL: #54545 Fixes: #54515 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent d9494a7 commit 7b3e38b

File tree

3 files changed

+67
-42
lines changed

3 files changed

+67
-42
lines changed

lib/internal/process/execution.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function tryGetCwd() {
4949

5050
let evalIndex = 0;
5151
function getEvalModuleUrl() {
52-
return pathToFileURL(`${process.cwd()}/[eval${++evalIndex}]`).href;
52+
return `${pathToFileURL(process.cwd())}/[eval${++evalIndex}]`;
5353
}
5454

5555
/**

lib/internal/url.js

+57-37
Original file line numberDiff line numberDiff line change
@@ -1498,44 +1498,75 @@ function fileURLToPath(path, options = kEmptyObject) {
14981498
return (windows ?? isWindows) ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
14991499
}
15001500

1501-
// The following characters are percent-encoded when converting from file path
1502-
// to URL:
1503-
// - %: The percent character is the only character not encoded by the
1504-
// `pathname` setter.
1505-
// - \: Backslash is encoded on non-windows platforms since it's a valid
1506-
// character but the `pathname` setters replaces it by a forward slash.
1507-
// - LF: The newline character is stripped out by the `pathname` setter.
1508-
// (See whatwg/url#419)
1509-
// - CR: The carriage return character is also stripped out by the `pathname`
1510-
// setter.
1511-
// - TAB: The tab character is also stripped out by the `pathname` setter.
1501+
// RFC1738 defines the following chars as "unsafe" for URLs
1502+
// @see https://www.ietf.org/rfc/rfc1738.txt 2.2. URL Character Encoding Issues
15121503
const percentRegEx = /%/g;
1513-
const backslashRegEx = /\\/g;
15141504
const newlineRegEx = /\n/g;
15151505
const carriageReturnRegEx = /\r/g;
15161506
const tabRegEx = /\t/g;
1517-
const questionRegex = /\?/g;
1507+
const quoteRegEx = /"/g;
15181508
const hashRegex = /#/g;
1509+
const spaceRegEx = / /g;
1510+
const questionMarkRegex = /\?/g;
1511+
const openSquareBracketRegEx = /\[/g;
1512+
const backslashRegEx = /\\/g;
1513+
const closeSquareBracketRegEx = /]/g;
1514+
const caretRegEx = /\^/g;
1515+
const verticalBarRegEx = /\|/g;
1516+
const tildeRegEx = /~/g;
15191517

15201518
function encodePathChars(filepath, options = kEmptyObject) {
1521-
const windows = options?.windows;
1522-
if (StringPrototypeIndexOf(filepath, '%') !== -1)
1519+
if (StringPrototypeIncludes(filepath, '%')) {
15231520
filepath = RegExpPrototypeSymbolReplace(percentRegEx, filepath, '%25');
1524-
// In posix, backslash is a valid character in paths:
1525-
if (!(windows ?? isWindows) && StringPrototypeIndexOf(filepath, '\\') !== -1)
1526-
filepath = RegExpPrototypeSymbolReplace(backslashRegEx, filepath, '%5C');
1527-
if (StringPrototypeIndexOf(filepath, '\n') !== -1)
1521+
}
1522+
1523+
if (StringPrototypeIncludes(filepath, '\t')) {
1524+
filepath = RegExpPrototypeSymbolReplace(tabRegEx, filepath, '%09');
1525+
}
1526+
if (StringPrototypeIncludes(filepath, '\n')) {
15281527
filepath = RegExpPrototypeSymbolReplace(newlineRegEx, filepath, '%0A');
1529-
if (StringPrototypeIndexOf(filepath, '\r') !== -1)
1528+
}
1529+
if (StringPrototypeIncludes(filepath, '\r')) {
15301530
filepath = RegExpPrototypeSymbolReplace(carriageReturnRegEx, filepath, '%0D');
1531-
if (StringPrototypeIndexOf(filepath, '\t') !== -1)
1532-
filepath = RegExpPrototypeSymbolReplace(tabRegEx, filepath, '%09');
1531+
}
1532+
if (StringPrototypeIncludes(filepath, ' ')) {
1533+
filepath = RegExpPrototypeSymbolReplace(spaceRegEx, filepath, '%20');
1534+
}
1535+
if (StringPrototypeIncludes(filepath, '"')) {
1536+
filepath = RegExpPrototypeSymbolReplace(quoteRegEx, filepath, '%22');
1537+
}
1538+
if (StringPrototypeIncludes(filepath, '#')) {
1539+
filepath = RegExpPrototypeSymbolReplace(hashRegex, filepath, '%23');
1540+
}
1541+
if (StringPrototypeIncludes(filepath, '?')) {
1542+
filepath = RegExpPrototypeSymbolReplace(questionMarkRegex, filepath, '%3F');
1543+
}
1544+
if (StringPrototypeIncludes(filepath, '[')) {
1545+
filepath = RegExpPrototypeSymbolReplace(openSquareBracketRegEx, filepath, '%5B');
1546+
}
1547+
// Back-slashes must be special-cased on Windows, where they are treated as path separator.
1548+
if (!options.windows && StringPrototypeIncludes(filepath, '\\')) {
1549+
filepath = RegExpPrototypeSymbolReplace(backslashRegEx, filepath, '%5C');
1550+
}
1551+
if (StringPrototypeIncludes(filepath, ']')) {
1552+
filepath = RegExpPrototypeSymbolReplace(closeSquareBracketRegEx, filepath, '%5D');
1553+
}
1554+
if (StringPrototypeIncludes(filepath, '^')) {
1555+
filepath = RegExpPrototypeSymbolReplace(caretRegEx, filepath, '%5E');
1556+
}
1557+
if (StringPrototypeIncludes(filepath, '|')) {
1558+
filepath = RegExpPrototypeSymbolReplace(verticalBarRegEx, filepath, '%7C');
1559+
}
1560+
if (StringPrototypeIncludes(filepath, '~')) {
1561+
filepath = RegExpPrototypeSymbolReplace(tildeRegEx, filepath, '%7E');
1562+
}
1563+
15331564
return filepath;
15341565
}
15351566

15361567
function pathToFileURL(filepath, options = kEmptyObject) {
1537-
const windows = options?.windows;
1538-
if ((windows ?? isWindows) && StringPrototypeStartsWith(filepath, '\\\\')) {
1568+
const windows = options?.windows ?? isWindows;
1569+
if (windows && StringPrototypeStartsWith(filepath, '\\\\')) {
15391570
const outURL = new URL('file://');
15401571
// UNC path format: \\server\share\resource
15411572
// Handle extended UNC path and standard UNC path
@@ -1566,20 +1597,9 @@ function pathToFileURL(filepath, options = kEmptyObject) {
15661597
);
15671598
return outURL;
15681599
}
1569-
let resolved = (windows ?? isWindows) ? path.win32.resolve(filepath) : path.posix.resolve(filepath);
1570-
1571-
// Call encodePathChars first to avoid encoding % again for ? and #.
1572-
resolved = encodePathChars(resolved, { windows });
1600+
const resolved = windows ? path.win32.resolve(filepath) : path.posix.resolve(filepath);
15731601

1574-
// Question and hash character should be included in pathname.
1575-
// Therefore, encoding is required to eliminate parsing them in different states.
1576-
// This is done as an optimization to not creating a URL instance and
1577-
// later triggering pathname setter, which impacts performance
1578-
if (StringPrototypeIndexOf(resolved, '?') !== -1)
1579-
resolved = RegExpPrototypeSymbolReplace(questionRegex, resolved, '%3F');
1580-
if (StringPrototypeIndexOf(resolved, '#') !== -1)
1581-
resolved = RegExpPrototypeSymbolReplace(hashRegex, resolved, '%23');
1582-
return new URL(`file://${resolved}`);
1602+
return new URL(`file://${encodePathChars(resolved, { windows })}`);
15831603
}
15841604

15851605
function toPathIfFileURL(fileURLOrPath) {

test/parallel/test-url-pathtofileurl.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ const url = require('url');
1313
{
1414
const fileURL = url.pathToFileURL('test\\').href;
1515
assert.ok(fileURL.startsWith('file:///'));
16-
if (isWindows)
17-
assert.ok(fileURL.endsWith('/'));
18-
else
19-
assert.ok(fileURL.endsWith('%5C'));
16+
assert.match(fileURL, isWindows ? /\/$/ : /%5C$/);
2017
}
2118

2219
{
@@ -104,6 +101,12 @@ const windowsTestCases = [
104101
{ path: 'C:\\€', expected: 'file:///C:/%E2%82%AC' },
105102
// Rocket emoji (non-BMP code point)
106103
{ path: 'C:\\🚀', expected: 'file:///C:/%F0%9F%9A%80' },
104+
// caret
105+
{ path: 'C:\\foo^bar', expected: 'file:///C:/foo%5Ebar' },
106+
// left bracket
107+
{ path: 'C:\\foo[bar', expected: 'file:///C:/foo%5Bbar' },
108+
// right bracket
109+
{ path: 'C:\\foo]bar', expected: 'file:///C:/foo%5Dbar' },
107110
// Local extended path
108111
{ path: '\\\\?\\C:\\path\\to\\file.txt', expected: 'file:///C:/path/to/file.txt' },
109112
// UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows)
@@ -154,6 +157,8 @@ const posixTestCases = [
154157
{ path: '/€', expected: 'file:///%E2%82%AC' },
155158
// Rocket emoji (non-BMP code point)
156159
{ path: '/🚀', expected: 'file:///%F0%9F%9A%80' },
160+
// "unsafe" chars
161+
{ path: '/foo\r\n\t<>"#%{}|^[\\~]`?bar', expected: 'file:///foo%0D%0A%09%3C%3E%22%23%25%7B%7D%7C%5E%5B%5C%7E%5D%60%3Fbar' },
157162
];
158163

159164
for (const { path, expected } of windowsTestCases) {

0 commit comments

Comments
 (0)