Skip to content

Commit 5418d40

Browse files
aduh95injunchoi98
authored andcommitted
url: handle "unsafe" characters properly in pathToFileURL
Co-authored-by: EarlyRiser42 <tkfydtls464@gmail.com> PR-URL: #54545 Fixes: #54515 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent ee17fcd commit 5418d40

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
@@ -1500,44 +1500,75 @@ function fileURLToPath(path, options = kEmptyObject) {
15001500
return (windows ?? isWindows) ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
15011501
}
15021502

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

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

15381569
function pathToFileURL(filepath, options = kEmptyObject) {
1539-
const windows = options?.windows;
1540-
if ((windows ?? isWindows) && StringPrototypeStartsWith(filepath, '\\\\')) {
1570+
const windows = options?.windows ?? isWindows;
1571+
if (windows && StringPrototypeStartsWith(filepath, '\\\\')) {
15411572
const outURL = new URL('file://');
15421573
// UNC path format: \\server\share\resource
15431574
// Handle extended UNC path and standard UNC path
@@ -1568,7 +1599,7 @@ function pathToFileURL(filepath, options = kEmptyObject) {
15681599
);
15691600
return outURL;
15701601
}
1571-
let resolved = (windows ?? isWindows) ? path.win32.resolve(filepath) : path.posix.resolve(filepath);
1602+
let resolved = windows ? path.win32.resolve(filepath) : path.posix.resolve(filepath);
15721603
// path.resolve strips trailing slashes so we must add them back
15731604
const filePathLast = StringPrototypeCharCodeAt(filepath,
15741605
filepath.length - 1);
@@ -1577,18 +1608,7 @@ function pathToFileURL(filepath, options = kEmptyObject) {
15771608
resolved[resolved.length - 1] !== path.sep)
15781609
resolved += '/';
15791610

1580-
// Call encodePathChars first to avoid encoding % again for ? and #.
1581-
resolved = encodePathChars(resolved, { windows });
1582-
1583-
// Question and hash character should be included in pathname.
1584-
// Therefore, encoding is required to eliminate parsing them in different states.
1585-
// This is done as an optimization to not creating a URL instance and
1586-
// later triggering pathname setter, which impacts performance
1587-
if (StringPrototypeIndexOf(resolved, '?') !== -1)
1588-
resolved = RegExpPrototypeSymbolReplace(questionRegex, resolved, '%3F');
1589-
if (StringPrototypeIndexOf(resolved, '#') !== -1)
1590-
resolved = RegExpPrototypeSymbolReplace(hashRegex, resolved, '%23');
1591-
return new URL(`file://${resolved}`);
1611+
return new URL(`file://${encodePathChars(resolved, { windows })}`);
15921612
}
15931613

15941614
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)