Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fetch#Request): Implements determineRequestReferrer #1236

Merged
merged 21 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 170 additions & 2 deletions lib/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,175 @@ function clonePolicyContainer () {

// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
function determineRequestsReferrer (request) {
// TODO
return 'no-referrer'
// 1. Let policy be request's referrer policy.
const policy = request.referrerPolicy

// Return no-referrer when empty or policy says so
if (policy == null || policy === '' || policy === 'no-referrer') {
return 'no-referrer'
}

// 2. Let environment be the request client
const environment = request.client
let referrerSource = null

/**
* 3, Switch on request’s referrer:
"client"
If environment’s global object is a Window object, then
Let document be the associated Document of environment’s global object.
If document’s origin is an opaque origin, return no referrer.
While document is an iframe srcdoc document,
let document be document’s browsing context’s browsing context container’s node document.
Let referrerSource be document’s URL.

Otherwise, let referrerSource be environment’s creation URL.

a URL
Let referrerSource be request’s referrer.
*/
if (request.referrer === 'client') {
ronag marked this conversation as resolved.
Show resolved Hide resolved
// Not defined in Node but part of the spec
if (request.client?.globalObject?.constructor?.name === 'Window' ) { // eslint-disable-line
const origin = environment.globalObject.self?.origin ?? environment.globalObject.location?.origin

// If document’s origin is an opaque origin, return no referrer.
if (origin == null || origin === 'null') return 'no-referrer'

// Let referrerSource be document’s URL.
referrerSource = new URL(environment.globalObject.location.href)
} else {
// 3(a)(II) If environment's global object is not Window,
// Let referrerSource be environments creationURL
if (environment?.globalObject?.location == null) {
return 'no-referrer'
}

referrerSource = new URL(environment.globalObject.location.href)
ronag marked this conversation as resolved.
Show resolved Hide resolved
}
} else if (request.referrer instanceof URL) {
// 3(b) If requests's referrer is a URL instance, then make
// referrerSource be requests's referrer.
referrerSource = request.referrer
} else {
// If referrerSource neither client nor instance of URL
// then return "no-referrer".
return 'no-referrer'
}

const urlProtocol = referrerSource.protocol

// If url's scheme is a local scheme (i.e. one of "about", "data", "javascript", "file")
// then return "no-referrer".
if (
urlProtocol === 'about:' || urlProtocol === 'data:' ||
urlProtocol === 'blob:'
) {
return 'no-referrer'
}

let temp
let referrerOrigin
// 4. Let requests's referrerURL be the result of stripping referrer
// source for use as referrer (using util function, without origin only)
const referrerUrl = (temp = stripURLForReferrer(referrerSource)).length > 4096
// 5. Let referrerOrigin be the result of stripping referrer
// source for use as referrer (using util function, with originOnly true)
? (referrerOrigin = stripURLForReferrer(referrerSource, true))
// 6. If result of seralizing referrerUrl is a string whose length is greater than
// 4096, then set referrerURL to referrerOrigin
: temp
const areSameOrigin = sameOrigin(request, referrerUrl)
const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerUrl) &&
!isURLPotentiallyTrustworthy(request.url)

// NOTE: How to treat step 7?
// 8. Execute the switch statements corresponding to the value of policy:
switch (policy) {
case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true)
case 'unsafe-url': return referrerUrl
case 'same-origin':
return areSameOrigin ? referrerOrigin : 'no-referrer'
case 'origin-when-cross-origin':
return areSameOrigin ? referrerUrl : referrerOrigin
case 'strict-origin-when-cross-origin':
/**
* 1. If the origin of referrerURL and the origin of request’s current URL are the same,
* then return referrerURL.
* 2. If referrerURL is a potentially trustworthy URL and request’s current URL is not a
* potentially trustworthy URL, then return no referrer.
* 3. Return referrerOrigin
*/
if (areSameOrigin) return referrerOrigin
// else return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
case 'strict-origin': // eslint-disable-line
/**
* 1. If referrerURL is a potentially trustworthy URL and
* request’s current URL is not a potentially trustworthy URL,
* then return no referrer.
* 2. Return referrerOrigin
*/
case 'no-referrer-when-downgrade': // eslint-disable-line
/**
* 1. If referrerURL is a potentially trustworthy URL and
* request’s current URL is not a potentially trustworthy URL,
* then return no referrer.
* 2. Return referrerOrigin
*/

default: // eslint-disable-line
return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
metcoder95 marked this conversation as resolved.
Show resolved Hide resolved
}

function stripURLForReferrer (url, originOnly = false) {
const urlObject = new URL(url.href)
urlObject.username = ''
urlObject.password = ''
urlObject.hash = ''

return originOnly ? urlObject.origin : urlObject.href
}
}

function isURLPotentiallyTrustworthy (url) {
if (!(url instanceof URL)) {
return false
}

// If child of about, return true
if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
return true
}

// If scheme is data, return true
if (url.protocol === 'data:') return true

// If file, return true
if (url.protocol === 'file:') return true

return isOriginPotentiallyTrustworthy(url.origin)

function isOriginPotentiallyTrustworthy (origin) {
// If origin is explicitly null, return false
if (origin == null || origin === 'null') return false

const originAsURL = new URL(origin)

// If secure, return true
if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') {
return true
}

// If localhost or variants, return true
if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) ||
(originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) ||
(originAsURL.hostname.endsWith('.localhost'))) {
return true
}

// If any other, return false
return false
}
}

/**
Expand Down Expand Up @@ -617,6 +784,7 @@ module.exports = {
responseURL,
responseLocationURL,
isBlobLike,
isURLPotentiallyTrustworthy,
isValidReasonPhrase,
sameOrigin,
normalizeMethod,
Expand Down
96 changes: 96 additions & 0 deletions test/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,99 @@ test('sameOrigin', (t) => {

t.end()
})

test('isURLPotentiallyTrustworthy', (t) => {
const valid = ['http://127.0.0.1', 'http://localhost.localhost',
'http://[::1]', 'http://adb.localhost', 'https://something.com', 'wss://hello.com',
'file:///link/to/file.txt', 'data:text/plain;base64,randomstring', 'about:blank', 'about:srcdoc']
const invalid = ['http://121.3.4.5:55', 'null:8080', 'something:8080']

t.plan(valid.length + invalid.length + 1)
t.notOk(util.isURLPotentiallyTrustworthy('string'))

for (const url of valid) {
const instance = new URL(url)
t.ok(util.isURLPotentiallyTrustworthy(instance))
}

for (const url of invalid) {
const instance = new URL(url)
t.notOk(util.isURLPotentiallyTrustworthy(instance))
}
})

test('determineRequestsReferrer', (t) => {
t.plan(7)

t.test('Should handle empty referrerPolicy', (tt) => {
tt.plan(2)
tt.equal(util.determineRequestsReferrer({}), 'no-referrer')
tt.equal(util.determineRequestsReferrer({ referrerPolicy: '' }), 'no-referrer')
})

t.test('Should handle "no-referrer" referrerPolicy', (tt) => {
tt.plan(1)
tt.equal(util.determineRequestsReferrer({ referrerPolicy: 'no-referrer' }), 'no-referrer')
})

t.test('Should return "no-referrer" if request referrer is absent', (tt) => {
tt.plan(1)
tt.equal(util.determineRequestsReferrer({
referrerPolicy: 'origin'
}), 'no-referrer')
})

t.test('Should return "no-referrer" if scheme is local scheme', (tt) => {
tt.plan(3)
const referrerSources = [
new URL('data:something'),
new URL('about:blank'),
new URL('blob:https://video_url')]

for (const source of referrerSources) {
tt.equal(util.determineRequestsReferrer({
referrerPolicy: 'origin',
referrer: source
}), 'no-referrer')
}
})

t.test('Should return "no-referrer" if the request referrer is neither client nor instance of URL', (tt) => {
tt.plan(4)
const requests = [
{ referrerPolicy: 'origin', referrer: 'string' },
{ referrerPolicy: 'origin', referrer: null },
{ referrerPolicy: 'origin', referrer: undefined },
{ referrerPolicy: 'origin', referrer: '' }
]

for (const request of requests) {
tt.equal(util.determineRequestsReferrer(request), 'no-referrer')
}
})

t.test('Should return referrer origin on referrerPolicy origin', (tt) => {
tt.plan(1)
const expectedRequest = {
referrerPolicy: 'origin',
referrer: new URL('http://example:[email protected]')
}

tt.equal(util.determineRequestsReferrer(expectedRequest), expectedRequest.referrer.origin)
})

t.test('Should return referrer url on referrerPolicy unsafe-url', (tt) => {
tt.plan(1)
const expectedRequest = {
referrerPolicy: 'unsafe-url',
referrer: new URL('http://example:[email protected]/hello/world')
}

const expectedReffererUrl = new URL(expectedRequest.referrer.href)

expectedReffererUrl.username = ''
expectedReffererUrl.password = ''

tt.equal(util.determineRequestsReferrer(expectedRequest), expectedReffererUrl.href)
})
})