Validate URL schemes in fs path arguments like Node (#713)

fsPath degraded any URL to its decoded pathname, so fs.existsSync accepted
a vscode-remote:// workspace whose stripped pathname exists locally — an
SSH host opened at / always does — and Raycast's Visual Studio Code
extension handed that URI to fileURLToPath, crashing Search Recent
Projects with "The URL must be of scheme file". URL arguments now go
through fileURLToPath: a non-file scheme throws ERR_INVALID_URL_SCHEME
like Node, existsSync counts that as absence, and the remote entries
render as remote items.
This commit is contained in:
Matt Farrell
2026-09-15 09:40:13 +06:00
committed by GitHub
parent 4d3cbf94be
commit 92da832989
4 changed files with 37 additions and 11 deletions
+16
View File
@@ -170,6 +170,7 @@ export default function Command() {
const nodeSource = `
import path from "node:path";
import os from "node:os";
import fs from "node:fs";
import crypto from "node:crypto";
import { Buffer } from "node:buffer";
import { fileURLToPath, pathToFileURL } from "node:url";
@@ -236,6 +237,15 @@ export default function Command() {
errorCode(() => fileURLToPath("file://user@localhost/tmp/a")),
errorCode(() => fileURLToPath("file://localhost:/tmp/a")),
errorCode(() => fileURLToPath("file:///C:/a", { windows: true })),
// fs validates URL schemes the way Node does: a vscode-remote:// workspace URI whose stripped
// pathname exists locally ("/" always does) must not pass existsSync — Raycast's Search Recent
// Projects relies on that guard before handing the URI to fileURLToPath.
String(fs.existsSync(new URL("vscode-remote://ssh-remote%2Bucg/"))),
String(fs.existsSync(new URL("vscode-remote://ssh-remote%2Bserver/etc/docker/daemon.json"))),
String(fs.existsSync(new URL("file:///etc/hosts"))),
String(fs.existsSync("/etc/hosts")),
errorCode(() => fs.statSync(new URL("https://example.com/a"))),
errorCode(() => fs.readFileSync(new URL("https://example.com/a"))),
];
return <Detail markdown={parts.join("\\n")} />;
}
@@ -744,6 +754,12 @@ export async function runFixtures() {
"ERR_INVALID_URL",
"ERR_INVALID_URL",
"Error",
"false",
"false",
"true",
"true",
"ERR_INVALID_URL_SCHEME",
"ERR_INVALID_URL_SCHEME",
];
expected.forEach((value, index) => check(`shim ${index}: ${value}`, markdown[index] === value, markdown[index]));
});
+4 -1
View File
@@ -330,7 +330,10 @@ class Dirent {
}
function fsPath(input) {
if (input instanceof URL) return decodeURIComponent(input.pathname);
// Node validates URL inputs through fileURLToPath: a non-file scheme (say a VS Code
// vscode-remote:// workspace URI) must throw ERR_INVALID_URL_SCHEME rather than quietly
// degrading to its pathname — extensions like Search Recent Projects guard on that failure.
if (input instanceof URL) return fileURLToPath(input);
if (input instanceof Uint8Array) return utf8Decode(input);
return String(input);
}
File diff suppressed because one or more lines are too long
+7
View File
@@ -586,6 +586,13 @@ host rather than returning a wrong path. Node's `windows` override is absent: Ti
macOS, so drive-letter and UNC output would be unreachable. `url.pathToFileURL` escapes `?` and `#`
so a filename holding either survives the round trip.
The `fs` functions hand URL arguments to that same validator: a URL whose scheme is not `file:`
throws `ERR_INVALID_URL_SCHEME` instead of degrading to its pathname, and `fs.existsSync` counts
that as absence, like Node. Raycast's Visual Studio Code extension leans on the guard — a
`vscode-remote://` workspace whose stripped pathname exists locally (an SSH host opened at `/`
always does) would otherwise pass `isFolderEntry` and reach `fileURLToPath`, which took the whole
Search Recent Projects command down.
A bundle that ships its own HTTP client rather than calling `fetch` — node-fetch travels inside
`@raycast/utils`, and axios has a Node adapter — reaches the network through `http.request`, so the
shim answers it: one request when the body ends, one response chunk when the bridge replies. The