Skip to content

Commit 9c3ed66

Browse files
UzlopakKhafraDev
andauthored
fetch: refactor referrer policy util functions (#3706)
* improve setRequestReferrerPolicyOnRedirect, create * fix bug * adapt more * improve isOriginPotentiallyTrustworthy * comments and stuff * add newline * Update lib/web/fetch/util.js Co-authored-by: Khafra <[email protected]> * fix * fix test * add test * fix * revert * tests * fix --------- Co-authored-by: Khafra <[email protected]>
1 parent 9dcb874 commit 9c3ed66

File tree

4 files changed

+423
-188
lines changed

4 files changed

+423
-188
lines changed

lib/web/fetch/constants.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ const badPorts = /** @type {const} */ ([
2222
const badPortsSet = new Set(badPorts)
2323

2424
/**
25-
* @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
25+
* @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header
2626
*/
27-
const referrerPolicy = /** @type {const} */ ([
28-
'',
27+
const referrerPolicyTokens = /** @type {const} */ ([
2928
'no-referrer',
3029
'no-referrer-when-downgrade',
3130
'same-origin',
@@ -35,7 +34,15 @@ const referrerPolicy = /** @type {const} */ ([
3534
'strict-origin-when-cross-origin',
3635
'unsafe-url'
3736
])
38-
const referrerPolicySet = new Set(referrerPolicy)
37+
38+
/**
39+
* @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
40+
*/
41+
const referrerPolicy = /** @type {const} */ ([
42+
'',
43+
...referrerPolicyTokens
44+
])
45+
const referrerPolicyTokensSet = new Set(referrerPolicyTokens)
3946

4047
const requestRedirect = /** @type {const} */ (['follow', 'manual', 'error'])
4148

@@ -120,5 +127,5 @@ module.exports = {
120127
corsSafeListedMethodsSet,
121128
safeMethodsSet,
122129
forbiddenMethodsSet,
123-
referrerPolicySet
130+
referrerPolicyTokens: referrerPolicyTokensSet
124131
}

lib/web/fetch/util.js

Lines changed: 171 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const { Transform } = require('node:stream')
44
const zlib = require('node:zlib')
5-
const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants')
5+
const { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require('./constants')
66
const { getGlobalOrigin } = require('./global')
77
const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require('./data-url')
88
const { performance } = require('node:perf_hooks')
@@ -170,29 +170,24 @@ function isValidHeaderValue (potentialValue) {
170170
) === false
171171
}
172172

173-
// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
174-
function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
175-
// Given a request request and a response actualResponse, this algorithm
176-
// updates request’s referrer policy according to the Referrer-Policy
177-
// header (if any) in actualResponse.
178-
179-
// 1. Let policy be the result of executing § 8.1 Parse a referrer policy
180-
// from a Referrer-Policy header on actualResponse.
181-
182-
// 8.1 Parse a referrer policy from a Referrer-Policy header
173+
/**
174+
* Parse a referrer policy from a Referrer-Policy header
175+
* @see https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header
176+
*/
177+
function parseReferrerPolicy (actualResponse) {
183178
// 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list.
184-
const { headersList } = actualResponse
179+
const policyHeader = (actualResponse.headersList.get('referrer-policy', true) ?? '').split(',')
180+
185181
// 2. Let policy be the empty string.
182+
let policy = ''
183+
186184
// 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
187-
// 4. Return policy.
188-
const policyHeader = (headersList.get('referrer-policy', true) ?? '').split(',')
189185

190186
// Note: As the referrer-policy can contain multiple policies
191187
// separated by comma, we need to loop through all of them
192188
// and pick the first valid one.
193189
// Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy
194-
let policy = ''
195-
if (policyHeader.length > 0) {
190+
if (policyHeader.length) {
196191
// The right-most policy takes precedence.
197192
// The left-most policy is the fallback.
198193
for (let i = policyHeader.length; i !== 0; i--) {
@@ -204,6 +199,23 @@ function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
204199
}
205200
}
206201

202+
// 4. Return policy.
203+
return policy
204+
}
205+
206+
/**
207+
* Given a request request and a response actualResponse, this algorithm
208+
* updates request’s referrer policy according to the Referrer-Policy
209+
* header (if any) in actualResponse.
210+
* @see https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
211+
* @param {import('./request').Request} request
212+
* @param {import('./response').Response} actualResponse
213+
*/
214+
function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
215+
// 1. Let policy be the result of executing § 8.1 Parse a referrer policy
216+
// from a Referrer-Policy header on actualResponse.
217+
const policy = parseReferrerPolicy(actualResponse)
218+
207219
// 2. If policy is not the empty string, then set request’s referrer policy to policy.
208220
if (policy !== '') {
209221
request.referrerPolicy = policy
@@ -374,8 +386,16 @@ function clonePolicyContainer (policyContainer) {
374386
}
375387
}
376388

377-
// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
389+
/**
390+
* Determine request’s Referrer
391+
*
392+
* @see https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
393+
*/
378394
function determineRequestsReferrer (request) {
395+
// Given a request request, we can determine the correct referrer information
396+
// to send by examining its referrer policy as detailed in the following
397+
// steps, which return either no referrer or a URL:
398+
379399
// 1. Let policy be request's referrer policy.
380400
const policy = request.referrerPolicy
381401

@@ -387,6 +407,8 @@ function determineRequestsReferrer (request) {
387407
let referrerSource = null
388408

389409
// 3. Switch on request’s referrer:
410+
411+
// "client"
390412
if (request.referrer === 'client') {
391413
// Note: node isn't a browser and doesn't implement document/iframes,
392414
// so we bypass this step and replace it with our own.
@@ -397,8 +419,9 @@ function determineRequestsReferrer (request) {
397419
return 'no-referrer'
398420
}
399421

400-
// note: we need to clone it as it's mutated
422+
// Note: we need to clone it as it's mutated
401423
referrerSource = new URL(globalOrigin)
424+
// a URL
402425
} else if (webidl.is.URL(request.referrer)) {
403426
// Let referrerSource be request’s referrer.
404427
referrerSource = request.referrer
@@ -500,18 +523,26 @@ function determineRequestsReferrer (request) {
500523
}
501524

502525
/**
526+
* Certain portions of URLs must not be included when sending a URL as the
527+
* value of a `Referer` header: a URLs fragment, username, and password
528+
* components must be stripped from the URL before it’s sent out. This
529+
* algorithm accepts a origin-only flag, which defaults to false. If set to
530+
* true, the algorithm will additionally remove the URL’s path and query
531+
* components, leaving only the scheme, host, and port.
532+
*
503533
* @see https://w3c.github.io/webappsec-referrer-policy/#strip-url
504534
* @param {URL} url
505-
* @param {boolean} [originOnly]
535+
* @param {boolean} [originOnly=false]
506536
*/
507-
function stripURLForReferrer (url, originOnly) {
537+
function stripURLForReferrer (url, originOnly = false) {
508538
// 1. Assert: url is a URL.
509539
assert(webidl.is.URL(url))
510540

541+
// Note: Create a new URL instance to avoid mutating the original URL.
511542
url = new URL(url)
512543

513544
// 2. If url’s scheme is a local scheme, then return no referrer.
514-
if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') {
545+
if (urlIsLocal(url)) {
515546
return 'no-referrer'
516547
}
517548

@@ -525,7 +556,7 @@ function stripURLForReferrer (url, originOnly) {
525556
url.hash = ''
526557

527558
// 6. If the origin-only flag is true, then:
528-
if (originOnly) {
559+
if (originOnly === true) {
529560
// 1. Set url’s path to « the empty string ».
530561
url.pathname = ''
531562

@@ -537,45 +568,134 @@ function stripURLForReferrer (url, originOnly) {
537568
return url
538569
}
539570

540-
function isURLPotentiallyTrustworthy (url) {
541-
if (!webidl.is.URL(url)) {
571+
const potentialleTrustworthyIPv4RegExp = new RegExp('^(?:' +
572+
'(?:127\\.)' +
573+
'(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){2}' +
574+
'(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[1-9])' +
575+
')$')
576+
577+
const potentialleTrustworthyIPv6RegExp = new RegExp('^(?:' +
578+
'(?:(?:0{1,4}):){7}(?:(?:0{0,3}1))|' +
579+
'(?:(?:0{1,4}):){1,6}(?::(?:0{0,3}1))|' +
580+
'(?:::(?:0{0,3}1))|' +
581+
')$')
582+
583+
/**
584+
* Check if host matches one of the CIDR notations 127.0.0.0/8 or ::1/128.
585+
*
586+
* @param {string} origin
587+
* @returns {boolean}
588+
*/
589+
function isOriginIPPotentiallyTrustworthy (origin) {
590+
// IPv6
591+
if (origin.includes(':')) {
592+
// Remove brackets from IPv6 addresses
593+
if (origin[0] === '[' && origin[origin.length - 1] === ']') {
594+
origin = origin.slice(1, -1)
595+
}
596+
return potentialleTrustworthyIPv6RegExp.test(origin)
597+
}
598+
599+
// IPv4
600+
return potentialleTrustworthyIPv4RegExp.test(origin)
601+
}
602+
603+
/**
604+
* A potentially trustworthy origin is one which a user agent can generally
605+
* trust as delivering data securely.
606+
*
607+
* Return value `true` means `Potentially Trustworthy`.
608+
* Return value `false` means `Not Trustworthy`.
609+
*
610+
* @see https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
611+
* @param {string} origin
612+
* @returns {boolean}
613+
*/
614+
function isOriginPotentiallyTrustworthy (origin) {
615+
// 1. If origin is an opaque origin, return "Not Trustworthy".
616+
if (origin == null || origin === 'null') {
542617
return false
543618
}
544619

545-
// If child of about, return true
546-
if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
620+
// 2. Assert: origin is a tuple origin.
621+
origin = new URL(origin)
622+
623+
// 3. If origin’s scheme is either "https" or "wss",
624+
// return "Potentially Trustworthy".
625+
if (origin.protocol === 'https:' || origin.protocol === 'wss:') {
547626
return true
548627
}
549628

550-
// If scheme is data, return true
551-
if (url.protocol === 'data:') return true
629+
// 4. If origin’s host matches one of the CIDR notations 127.0.0.0/8 or
630+
// ::1/128 [RFC4632], return "Potentially Trustworthy".
631+
if (isOriginIPPotentiallyTrustworthy(origin.hostname)) {
632+
return true
633+
}
552634

553-
// If file, return true
554-
if (url.protocol === 'file:') return true
635+
// 5. If the user agent conforms to the name resolution rules in
636+
// [let-localhost-be-localhost] and one of the following is true:
555637

556-
return isOriginPotentiallyTrustworthy(url.origin)
638+
// origin’s host is "localhost" or "localhost."
639+
if (origin.hostname === 'localhost' || origin.hostname === 'localhost.') {
640+
return true
641+
}
557642

558-
function isOriginPotentiallyTrustworthy (origin) {
559-
// If origin is explicitly null, return false
560-
if (origin == null || origin === 'null') return false
643+
// origin’s host ends with ".localhost" or ".localhost."
644+
if (origin.hostname.endsWith('.localhost') || origin.hostname.endsWith('.localhost.')) {
645+
return true
646+
}
561647

562-
const originAsURL = new URL(origin)
648+
// 6. If origin’s scheme is "file", return "Potentially Trustworthy".
649+
if (origin.protocol === 'file:') {
650+
return true
651+
}
563652

564-
// If secure, return true
565-
if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') {
566-
return true
567-
}
653+
// 7. If origin’s scheme component is one which the user agent considers to
654+
// be authenticated, return "Potentially Trustworthy".
568655

569-
// If localhost or variants, return true
570-
if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) ||
571-
(originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) ||
572-
(originAsURL.hostname.endsWith('.localhost'))) {
573-
return true
574-
}
656+
// 8. If origin has been configured as a trustworthy origin, return
657+
// "Potentially Trustworthy".
575658

576-
// If any other, return false
659+
// 9. Return "Not Trustworthy".
660+
return false
661+
}
662+
663+
/**
664+
* A potentially trustworthy URL is one which either inherits context from its
665+
* creator (about:blank, about:srcdoc, data) or one whose origin is a
666+
* potentially trustworthy origin.
667+
*
668+
* Return value `true` means `Potentially Trustworthy`.
669+
* Return value `false` means `Not Trustworthy`.
670+
*
671+
* @see https://www.w3.org/TR/secure-contexts/#is-url-trustworthy
672+
* @param {URL} url
673+
* @returns {boolean}
674+
*/
675+
function isURLPotentiallyTrustworthy (url) {
676+
// Given a URL record (url), the following algorithm returns "Potentially
677+
// Trustworthy" or "Not Trustworthy" as appropriate:
678+
if (!webidl.is.URL(url)) {
577679
return false
578680
}
681+
682+
// 1. If url is "about:blank" or "about:srcdoc",
683+
// return "Potentially Trustworthy".
684+
if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
685+
return true
686+
}
687+
688+
// 2. If url’s scheme is "data", return "Potentially Trustworthy".
689+
if (url.protocol === 'data:') return true
690+
691+
// Note: The origin of blob: URLs is the origin of the context in which they
692+
// were created. Therefore, blobs created in a trustworthy origin will
693+
// themselves be potentially trustworthy.
694+
if (url.protocol === 'blob:') return true
695+
696+
// 3. Return the result of executing § 3.1 Is origin potentially trustworthy?
697+
// on url’s origin.
698+
return isOriginPotentiallyTrustworthy(url.origin)
579699
}
580700

581701
/**
@@ -1161,12 +1281,15 @@ async function readAllBytes (reader, successSteps, failureSteps) {
11611281
/**
11621282
* @see https://fetch.spec.whatwg.org/#is-local
11631283
* @param {URL} url
1284+
* @returns {boolean}
11641285
*/
11651286
function urlIsLocal (url) {
11661287
assert('protocol' in url) // ensure it's a url object
11671288

11681289
const protocol = url.protocol
11691290

1291+
// A URL is local if its scheme is a local scheme.
1292+
// A local scheme is "about", "blob", or "data".
11701293
return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'
11711294
}
11721295

@@ -1654,5 +1777,6 @@ module.exports = {
16541777
extractMimeType,
16551778
getDecodeSplit,
16561779
utf8DecodeBytes,
1657-
environmentSettingsObject
1780+
environmentSettingsObject,
1781+
isOriginIPPotentiallyTrustworthy
16581782
}

0 commit comments

Comments
 (0)