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 17 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
176 changes: 174 additions & 2 deletions lib/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,178 @@ 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 (environment?.globalObject instanceof Window) { // eslint-disable-line
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be changed to something similar to

request.client?.globalObject?.constructor?.name === 'Window'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, applied in 6ed09c1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't seem to have been done

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, sorry for that. Change was made but didn't stage it for the commit 😓
Here it is the new one: 681a755

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 === 'javascript:' || urlProtocol === 'file:'
KhafraDev marked this conversation as resolved.
Show resolved Hide resolved
) {
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)
// 6. If result of seralizing referrerUrl is a string whose length is greater than
// 4096, then set referrerURL to referrerOrigin
? (referrerOrigin = stripURLForReferrer(referrerSource, true))
: 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 ?? 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

let originAsURL

// If not valid because not semantically correct, return false
try { originAsURL = new URL(origin) } catch (e) { return false }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't be able to throw

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, a bad bad origin cannot happen without being priorly identified. Removed in 2ca0941


// 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 +787,8 @@ module.exports = {
responseURL,
responseLocationURL,
isBlobLike,
isFileLike,
isURLPotentiallyTrustworthy,
isValidReasonPhrase,
sameOrigin,
normalizeMethod,
Expand Down
142 changes: 142 additions & 0 deletions test/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,145 @@ test('sameOrigin', (t) => {

t.end()
})

test('CORBCheck', (t) => {
const allowedRequests = [{
initiator: 'download',
currentURL: { scheme: '' }
}, {
initiator: '',
currentURL: { scheme: 'https' }
}
]

const response = { headersList: { get () { return '' } } }

allowedRequests.forEach((request) => {
t.ok(util.CORBCheck(request, response))
})

t.ok(util.CORBCheck({
initiator: '',
currentURL: { scheme: '' }
}, response))

const protectedResponses = [{
status: 206,
headersList: { get () { return 'text/html' } }
}, {
status: 206,
headersList: { get () { return 'application/javascript' } }
}, {
status: 206,
headersList: { get () { return 'application/xml' } }
}, {
status: 218,
headersList: { get (type) { return type === 'content-type' ? 'text/html' : 'x-content-type-options' } }
}]

protectedResponses.forEach(response => {
t.equal(util.CORBCheck({
initiator: '',
currentURL: { scheme: 'https' }
}, response), 'blocked')
})

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(4)
const referrerSources = [
new URL('data:something'),
new URL('about:blank'),
new URL('javascript:something'),
new URL('file://path/to/file')]

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:12345@example.com')
}

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:12345@example.com/hello/world')
}

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

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

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