From 2ea6dbf5406d1652abcaac35189508cc92ce0e0c Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 27 Oct 2024 13:52:52 -0400 Subject: [PATCH 1/3] allow fetching file urls when experimental permissions are enabled --- lib/web/fetch/index.js | 41 ++++++++++++++++++--------- package.json | 3 +- test/fetch/file-url/fetch-file-url.js | 19 +++++++++++++ 3 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 test/fetch/file-url/fetch-file-url.js diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index 8b88904e9d5..e822d299b45 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -63,6 +63,7 @@ const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = requ const { getGlobalDispatcher } = require('../../global') const { webidl } = require('../webidl') const { STATUS_CODES } = require('node:http') +const { openAsBlob } = require('node:fs') const { bytesMatch } = require('../subresource-integrity/subresource-integrity') const { createDeferredPromise } = require('../../util/promise') const { isomorphicEncode } = require('../infra') @@ -772,13 +773,13 @@ async function mainFetch (fetchParams, recursive) { // https://fetch.spec.whatwg.org/#concept-scheme-fetch // given a fetch params fetchParams -function schemeFetch (fetchParams) { +async function schemeFetch (fetchParams) { // Note: since the connection is destroyed on redirect, which sets fetchParams to a // cancelled state, we do not want this condition to trigger *unless* there have been // no redirects. See https://github.com/nodejs/undici/issues/1776 // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) { - return Promise.resolve(makeAppropriateNetworkError(fetchParams)) + return makeAppropriateNetworkError(fetchParams) } // 2. Let request be fetchParams’s request. @@ -794,7 +795,7 @@ function schemeFetch (fetchParams) { // and body is the empty byte sequence as a body. // Otherwise, return a network error. - return Promise.resolve(makeNetworkError('about scheme is not supported')) + return makeNetworkError('about scheme is not supported') } case 'blob:': { if (!resolveObjectURL) { @@ -807,7 +808,7 @@ function schemeFetch (fetchParams) { // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 // Buffer.resolveObjectURL does not ignore URL queries. if (blobURLEntry.search.length !== 0) { - return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.')) + return makeNetworkError('NetworkError when attempting to fetch resource.') } const blob = resolveObjectURL(blobURLEntry.toString()) @@ -815,7 +816,7 @@ function schemeFetch (fetchParams) { // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s // object is not a Blob object, then return a network error. if (request.method !== 'GET' || !webidl.is.Blob(blob)) { - return Promise.resolve(makeNetworkError('invalid method')) + return makeNetworkError('invalid method') } // 3. Let blob be blobURLEntry’s object. @@ -863,7 +864,7 @@ function schemeFetch (fetchParams) { // 4. If rangeValue is failure, then return a network error. if (rangeValue === 'failure') { - return Promise.resolve(makeNetworkError('failed to fetch the data URL')) + return makeNetworkError('failed to fetch the data URL') } // 5. Let (rangeStart, rangeEnd) be rangeValue. @@ -880,7 +881,7 @@ function schemeFetch (fetchParams) { } else { // 1. If rangeStart is greater than or equal to fullLength, then return a network error. if (rangeStart >= fullLength) { - return Promise.resolve(makeNetworkError('Range start is greater than the blob\'s size.')) + return makeNetworkError('Range start is greater than the blob\'s size.') } // 2. If rangeEnd is null or rangeEnd is greater than or equal to fullLength, then set @@ -922,7 +923,7 @@ function schemeFetch (fetchParams) { } // 10. Return response. - return Promise.resolve(response) + return response } case 'data:': { // 1. Let dataURLStruct be the result of running the @@ -933,7 +934,7 @@ function schemeFetch (fetchParams) { // 2. If dataURLStruct is failure, then return a // network error. if (dataURLStruct === 'failure') { - return Promise.resolve(makeNetworkError('failed to fetch the data URL')) + return makeNetworkError('failed to fetch the data URL') } // 3. Let mimeType be dataURLStruct’s MIME type, serialized. @@ -942,18 +943,32 @@ function schemeFetch (fetchParams) { // 4. Return a response whose status message is `OK`, // header list is « (`Content-Type`, mimeType) », // and body is dataURLStruct’s body as a body. - return Promise.resolve(makeResponse({ + return makeResponse({ statusText: 'OK', headersList: [ ['content-type', { name: 'Content-Type', value: mimeType }] ], body: safelyExtractBody(dataURLStruct.body)[0] - })) + }) } case 'file:': { // For now, unfortunate as it is, file URLs are left as an exercise for the reader. // When in doubt, return a network error. - return Promise.resolve(makeNetworkError('not implemented... yet...')) + const fileURL = requestCurrentURL(request) + + if (!process.permission?.has('fs.read', fileURL.href)) { + return makeNetworkError(`Access to ${fileURL.href} is not permitted.`) + } + + try { + const blob = await openAsBlob(fileURL) + + return makeResponse({ + body: safelyExtractBody(blob)[0] + }) + } catch (e) { + return makeNetworkError(e) + } } case 'http:': case 'https:': { @@ -963,7 +978,7 @@ function schemeFetch (fetchParams) { .catch((err) => makeNetworkError(err)) } default: { - return Promise.resolve(makeNetworkError('unknown scheme')) + return makeNetworkError('unknown scheme') } } } diff --git a/package.json b/package.json index d27058bcc39..d7a0fb07069 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "test:cookies": "borp --timeout 180000 -p \"test/cookie/*.js\"", "test:eventsource": "npm run build:node && borp --timeout 180000 --expose-gc -p \"test/eventsource/*.js\"", "test:fuzzing": "node test/fuzzing/fuzzing.test.js", - "test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy", + "test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy && npm run test:fetch-file-url", + "test:fetch-file-url": "node scripts/verifyVersion.js 20 || node --experimental-permission --allow-fs-read=. test/fetch/file-url/fetch-file-url.js", "test:subresource-integrity": "borp --timeout 180000 -p \"test/subresource-integrity/*.js\"", "test:h2": "npm run test:h2:core && npm run test:h2:fetch", "test:h2:core": "borp --timeout 180000 -p \"test/+(http2|h2)*.js\"", diff --git a/test/fetch/file-url/fetch-file-url.js b/test/fetch/file-url/fetch-file-url.js new file mode 100644 index 00000000000..3c9eb505ba0 --- /dev/null +++ b/test/fetch/file-url/fetch-file-url.js @@ -0,0 +1,19 @@ +'use strict' + +const { fetch } = require('../../..') +const { test } = require('node:test') +const { pathToFileURL } = require('node:url') +const { join } = require('node:path') +const assert = require('node:assert') + +test('fetching a file url works', async () => { + const url = new URL(join(pathToFileURL(__dirname).toString(), 'fetch-file-url.js')) + + await assert.doesNotReject(fetch(url)) +}) + +test('fetching one outside of the permission scope rejects', async (t) => { + const url = new URL(join(pathToFileURL(process.cwd()).toString(), '..')) + + await assert.rejects(fetch(url), new TypeError('fetch failed')) +}) From b416293d68906b3cf6aee358eadcf0f999e7574f Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 15 Mar 2026 22:18:09 +0100 Subject: [PATCH 2/3] Update package.json Co-authored-by: Rafael Gonzaga --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7a0fb07069..67b10e6c3b3 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "test:eventsource": "npm run build:node && borp --timeout 180000 --expose-gc -p \"test/eventsource/*.js\"", "test:fuzzing": "node test/fuzzing/fuzzing.test.js", "test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy && npm run test:fetch-file-url", - "test:fetch-file-url": "node scripts/verifyVersion.js 20 || node --experimental-permission --allow-fs-read=. test/fetch/file-url/fetch-file-url.js", + "test:fetch-file-url": "node scripts/verifyVersion.js 20 || node --permission --allow-fs-read=. test/fetch/file-url/fetch-file-url.js", "test:subresource-integrity": "borp --timeout 180000 -p \"test/subresource-integrity/*.js\"", "test:h2": "npm run test:h2:core && npm run test:h2:fetch", "test:h2:core": "borp --timeout 180000 -p \"test/+(http2|h2)*.js\"", From 9a0e6ba60322cd46da327de52061ca9ee8992b7e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 17 Mar 2026 11:50:27 +0100 Subject: [PATCH 3/3] fix(fetch): keep file URL support behind fs permissions Signed-off-by: Matteo Collina --- lib/web/fetch/index.js | 2 -- test/fetch/file-url/fetch-file-url.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index e822d299b45..80f2a6e3ab8 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -952,8 +952,6 @@ async function schemeFetch (fetchParams) { }) } case 'file:': { - // For now, unfortunate as it is, file URLs are left as an exercise for the reader. - // When in doubt, return a network error. const fileURL = requestCurrentURL(request) if (!process.permission?.has('fs.read', fileURL.href)) { diff --git a/test/fetch/file-url/fetch-file-url.js b/test/fetch/file-url/fetch-file-url.js index 3c9eb505ba0..1f156a57e40 100644 --- a/test/fetch/file-url/fetch-file-url.js +++ b/test/fetch/file-url/fetch-file-url.js @@ -12,7 +12,7 @@ test('fetching a file url works', async () => { await assert.doesNotReject(fetch(url)) }) -test('fetching one outside of the permission scope rejects', async (t) => { +test('fetching one outside of the permission scope rejects', async () => { const url = new URL(join(pathToFileURL(process.cwd()).toString(), '..')) await assert.rejects(fetch(url), new TypeError('fetch failed'))