Skip to content

Commit 1b85001

Browse files
authored
feat(fetch#Request): Implements determineRequestReferrer (nodejs#1236)
* feat: poc of determineRequestReferrer * refactor: apply shortcut * feat(partial): apply switch referrer statement * refactor: add in-code documentation * feat: add check for window * docs: add comments * feat: add check for trustworthy/non-trustworthy urls * docs: add documentation about pottentially trustworthy * feat: expose pottentially trustworthy * test: URL potentially trustworthy * fix: check for possibly undefined * test: initial round * feat: smaller improvements * docs: update in-code docs * lint: ignore line * tests: add more test scenarios * refactor: small improvements * refactor: apply review * tests: adjust testing * refactor: apply PR review * refactor: smaller adjustements
1 parent 2d38b7e commit 1b85001

File tree

2 files changed

+266
-2
lines changed

2 files changed

+266
-2
lines changed

lib/fetch/util.js

Lines changed: 170 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,175 @@ function clonePolicyContainer () {
332332

333333
// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
334334
function determineRequestsReferrer (request) {
335-
// TODO
336-
return 'no-referrer'
335+
// 1. Let policy be request's referrer policy.
336+
const policy = request.referrerPolicy
337+
338+
// Return no-referrer when empty or policy says so
339+
if (policy == null || policy === '' || policy === 'no-referrer') {
340+
return 'no-referrer'
341+
}
342+
343+
// 2. Let environment be the request client
344+
const environment = request.client
345+
let referrerSource = null
346+
347+
/**
348+
* 3, Switch on request’s referrer:
349+
"client"
350+
If environment’s global object is a Window object, then
351+
Let document be the associated Document of environment’s global object.
352+
If document’s origin is an opaque origin, return no referrer.
353+
While document is an iframe srcdoc document,
354+
let document be document’s browsing context’s browsing context container’s node document.
355+
Let referrerSource be document’s URL.
356+
357+
Otherwise, let referrerSource be environment’s creation URL.
358+
359+
a URL
360+
Let referrerSource be request’s referrer.
361+
*/
362+
if (request.referrer === 'client') {
363+
// Not defined in Node but part of the spec
364+
if (request.client?.globalObject?.constructor?.name === 'Window' ) { // eslint-disable-line
365+
const origin = environment.globalObject.self?.origin ?? environment.globalObject.location?.origin
366+
367+
// If document’s origin is an opaque origin, return no referrer.
368+
if (origin == null || origin === 'null') return 'no-referrer'
369+
370+
// Let referrerSource be document’s URL.
371+
referrerSource = new URL(environment.globalObject.location.href)
372+
} else {
373+
// 3(a)(II) If environment's global object is not Window,
374+
// Let referrerSource be environments creationURL
375+
if (environment?.globalObject?.location == null) {
376+
return 'no-referrer'
377+
}
378+
379+
referrerSource = new URL(environment.globalObject.location.href)
380+
}
381+
} else if (request.referrer instanceof URL) {
382+
// 3(b) If requests's referrer is a URL instance, then make
383+
// referrerSource be requests's referrer.
384+
referrerSource = request.referrer
385+
} else {
386+
// If referrerSource neither client nor instance of URL
387+
// then return "no-referrer".
388+
return 'no-referrer'
389+
}
390+
391+
const urlProtocol = referrerSource.protocol
392+
393+
// If url's scheme is a local scheme (i.e. one of "about", "data", "javascript", "file")
394+
// then return "no-referrer".
395+
if (
396+
urlProtocol === 'about:' || urlProtocol === 'data:' ||
397+
urlProtocol === 'blob:'
398+
) {
399+
return 'no-referrer'
400+
}
401+
402+
let temp
403+
let referrerOrigin
404+
// 4. Let requests's referrerURL be the result of stripping referrer
405+
// source for use as referrer (using util function, without origin only)
406+
const referrerUrl = (temp = stripURLForReferrer(referrerSource)).length > 4096
407+
// 5. Let referrerOrigin be the result of stripping referrer
408+
// source for use as referrer (using util function, with originOnly true)
409+
? (referrerOrigin = stripURLForReferrer(referrerSource, true))
410+
// 6. If result of seralizing referrerUrl is a string whose length is greater than
411+
// 4096, then set referrerURL to referrerOrigin
412+
: temp
413+
const areSameOrigin = sameOrigin(request, referrerUrl)
414+
const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerUrl) &&
415+
!isURLPotentiallyTrustworthy(request.url)
416+
417+
// NOTE: How to treat step 7?
418+
// 8. Execute the switch statements corresponding to the value of policy:
419+
switch (policy) {
420+
case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true)
421+
case 'unsafe-url': return referrerUrl
422+
case 'same-origin':
423+
return areSameOrigin ? referrerOrigin : 'no-referrer'
424+
case 'origin-when-cross-origin':
425+
return areSameOrigin ? referrerUrl : referrerOrigin
426+
case 'strict-origin-when-cross-origin':
427+
/**
428+
* 1. If the origin of referrerURL and the origin of request’s current URL are the same,
429+
* then return referrerURL.
430+
* 2. If referrerURL is a potentially trustworthy URL and request’s current URL is not a
431+
* potentially trustworthy URL, then return no referrer.
432+
* 3. Return referrerOrigin
433+
*/
434+
if (areSameOrigin) return referrerOrigin
435+
// else return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
436+
case 'strict-origin': // eslint-disable-line
437+
/**
438+
* 1. If referrerURL is a potentially trustworthy URL and
439+
* request’s current URL is not a potentially trustworthy URL,
440+
* then return no referrer.
441+
* 2. Return referrerOrigin
442+
*/
443+
case 'no-referrer-when-downgrade': // eslint-disable-line
444+
/**
445+
* 1. If referrerURL is a potentially trustworthy URL and
446+
* request’s current URL is not a potentially trustworthy URL,
447+
* then return no referrer.
448+
* 2. Return referrerOrigin
449+
*/
450+
451+
default: // eslint-disable-line
452+
return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
453+
}
454+
455+
function stripURLForReferrer (url, originOnly = false) {
456+
const urlObject = new URL(url.href)
457+
urlObject.username = ''
458+
urlObject.password = ''
459+
urlObject.hash = ''
460+
461+
return originOnly ? urlObject.origin : urlObject.href
462+
}
463+
}
464+
465+
function isURLPotentiallyTrustworthy (url) {
466+
if (!(url instanceof URL)) {
467+
return false
468+
}
469+
470+
// If child of about, return true
471+
if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
472+
return true
473+
}
474+
475+
// If scheme is data, return true
476+
if (url.protocol === 'data:') return true
477+
478+
// If file, return true
479+
if (url.protocol === 'file:') return true
480+
481+
return isOriginPotentiallyTrustworthy(url.origin)
482+
483+
function isOriginPotentiallyTrustworthy (origin) {
484+
// If origin is explicitly null, return false
485+
if (origin == null || origin === 'null') return false
486+
487+
const originAsURL = new URL(origin)
488+
489+
// If secure, return true
490+
if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') {
491+
return true
492+
}
493+
494+
// If localhost or variants, return true
495+
if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) ||
496+
(originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) ||
497+
(originAsURL.hostname.endsWith('.localhost'))) {
498+
return true
499+
}
500+
501+
// If any other, return false
502+
return false
503+
}
337504
}
338505

339506
/**
@@ -617,6 +784,7 @@ module.exports = {
617784
responseURL,
618785
responseLocationURL,
619786
isBlobLike,
787+
isURLPotentiallyTrustworthy,
620788
isValidReasonPhrase,
621789
sameOrigin,
622790
normalizeMethod,

test/fetch/util.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,99 @@ test('sameOrigin', (t) => {
113113

114114
t.end()
115115
})
116+
117+
test('isURLPotentiallyTrustworthy', (t) => {
118+
const valid = ['http://127.0.0.1', 'http://localhost.localhost',
119+
'http://[::1]', 'http://adb.localhost', 'https://something.com', 'wss://hello.com',
120+
'file:///link/to/file.txt', 'data:text/plain;base64,randomstring', 'about:blank', 'about:srcdoc']
121+
const invalid = ['http://121.3.4.5:55', 'null:8080', 'something:8080']
122+
123+
t.plan(valid.length + invalid.length + 1)
124+
t.notOk(util.isURLPotentiallyTrustworthy('string'))
125+
126+
for (const url of valid) {
127+
const instance = new URL(url)
128+
t.ok(util.isURLPotentiallyTrustworthy(instance))
129+
}
130+
131+
for (const url of invalid) {
132+
const instance = new URL(url)
133+
t.notOk(util.isURLPotentiallyTrustworthy(instance))
134+
}
135+
})
136+
137+
test('determineRequestsReferrer', (t) => {
138+
t.plan(7)
139+
140+
t.test('Should handle empty referrerPolicy', (tt) => {
141+
tt.plan(2)
142+
tt.equal(util.determineRequestsReferrer({}), 'no-referrer')
143+
tt.equal(util.determineRequestsReferrer({ referrerPolicy: '' }), 'no-referrer')
144+
})
145+
146+
t.test('Should handle "no-referrer" referrerPolicy', (tt) => {
147+
tt.plan(1)
148+
tt.equal(util.determineRequestsReferrer({ referrerPolicy: 'no-referrer' }), 'no-referrer')
149+
})
150+
151+
t.test('Should return "no-referrer" if request referrer is absent', (tt) => {
152+
tt.plan(1)
153+
tt.equal(util.determineRequestsReferrer({
154+
referrerPolicy: 'origin'
155+
}), 'no-referrer')
156+
})
157+
158+
t.test('Should return "no-referrer" if scheme is local scheme', (tt) => {
159+
tt.plan(3)
160+
const referrerSources = [
161+
new URL('data:something'),
162+
new URL('about:blank'),
163+
new URL('blob:https://video_url')]
164+
165+
for (const source of referrerSources) {
166+
tt.equal(util.determineRequestsReferrer({
167+
referrerPolicy: 'origin',
168+
referrer: source
169+
}), 'no-referrer')
170+
}
171+
})
172+
173+
t.test('Should return "no-referrer" if the request referrer is neither client nor instance of URL', (tt) => {
174+
tt.plan(4)
175+
const requests = [
176+
{ referrerPolicy: 'origin', referrer: 'string' },
177+
{ referrerPolicy: 'origin', referrer: null },
178+
{ referrerPolicy: 'origin', referrer: undefined },
179+
{ referrerPolicy: 'origin', referrer: '' }
180+
]
181+
182+
for (const request of requests) {
183+
tt.equal(util.determineRequestsReferrer(request), 'no-referrer')
184+
}
185+
})
186+
187+
t.test('Should return referrer origin on referrerPolicy origin', (tt) => {
188+
tt.plan(1)
189+
const expectedRequest = {
190+
referrerPolicy: 'origin',
191+
referrer: new URL('http://example:[email protected]')
192+
}
193+
194+
tt.equal(util.determineRequestsReferrer(expectedRequest), expectedRequest.referrer.origin)
195+
})
196+
197+
t.test('Should return referrer url on referrerPolicy unsafe-url', (tt) => {
198+
tt.plan(1)
199+
const expectedRequest = {
200+
referrerPolicy: 'unsafe-url',
201+
referrer: new URL('http://example:[email protected]/hello/world')
202+
}
203+
204+
const expectedReffererUrl = new URL(expectedRequest.referrer.href)
205+
206+
expectedReffererUrl.username = ''
207+
expectedReffererUrl.password = ''
208+
209+
tt.equal(util.determineRequestsReferrer(expectedRequest), expectedReffererUrl.href)
210+
})
211+
})

0 commit comments

Comments
 (0)