import chai from 'chai'; import fetch, {Request, Headers} from '../src/index.js'; import { DEFAULT_REFERRER_POLICY, ReferrerPolicy, stripURLForUseAsAReferrer, validateReferrerPolicy, isOriginPotentiallyTrustworthy, isUrlPotentiallyTrustworthy, determineRequestsReferrer, parseReferrerPolicyFromHeader } from '../src/utils/referrer.js'; import TestServer from './utils/server.js'; const {expect} = chai; describe('fetch() with referrer and referrerPolicy', () => { const local = new TestServer(); let base; before(async () => { await local.start(); base = `http://${local.hostname}:${local.port}/`; }); after(async () => { return local.stop(); }); it('should send request without a referrer by default', () => { return fetch(`${base}inspect`).then(res => res.json()).then(res => { expect(res.headers.referer).to.be.undefined; }); }); it('should send request with a referrer', () => { return fetch(`${base}inspect`, { referrer: base, referrerPolicy: 'unsafe-url' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.equal(base); }); }); it('should send request with referrerPolicy strict-origin-when-cross-origin by default', () => { return Promise.all([ fetch(`${base}inspect`, { referrer: base }).then(res => res.json()).then(res => { expect(res.headers.referer).to.equal(base); }), fetch(`${base}inspect`, { referrer: 'https://example.com' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.be.undefined; }) ]); }); it('should send request with a referrer and respect redirected referrer-policy', () => { return Promise.all([ fetch(`${base}redirect/referrer-policy`, { referrer: base }).then(res => res.json()).then(res => { expect(res.headers.referer).to.equal(base); }), fetch(`${base}redirect/referrer-policy`, { referrer: 'https://example.com' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.be.undefined; }), fetch(`${base}redirect/referrer-policy`, { referrer: 'https://example.com', referrerPolicy: 'unsafe-url' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.equal('https://example.com/'); }), fetch(`${base}redirect/referrer-policy/same-origin`, { referrer: 'https://example.com', referrerPolicy: 'unsafe-url' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.undefined; }) ]); }); }); describe('Request constructor', () => { describe('referrer', () => { it('should leave referrer undefined by default', () => { const req = new Request('http://example.com'); expect(req.referrer).to.be.undefined; }); it('should accept empty string referrer as no-referrer', () => { const referrer = ''; const req = new Request('http://example.com', {referrer}); expect(req.referrer).to.equal(referrer); }); it('should accept about:client referrer as client', () => { const referrer = 'about:client'; const req = new Request('http://example.com', {referrer}); expect(req.referrer).to.equal(referrer); }); it('should accept about://client referrer as client', () => { const req = new Request('http://example.com', {referrer: 'about://client'}); expect(req.referrer).to.equal('about:client'); }); it('should accept a string URL referrer', () => { const referrer = 'http://example.com/'; const req = new Request('http://example.com', {referrer}); expect(req.referrer).to.equal(referrer); }); it('should accept a URL referrer', () => { const referrer = new URL('http://example.com'); const req = new Request('http://example.com', {referrer}); expect(req.referrer).to.equal(referrer.toString()); }); it('should accept a referrer from input', () => { const referrer = 'http://example.com/'; const req = new Request(new Request('http://example.com', {referrer})); expect(req.referrer).to.equal(referrer.toString()); }); it('should throw a TypeError for an invalid URL', () => { expect(() => { const req = new Request('http://example.com', {referrer: 'foobar'}); expect.fail(req); }).to.throw(TypeError, /Invalid URL/); }); }); describe('referrerPolicy', () => { it('should default refererPolicy to empty string', () => { const req = new Request('http://example.com'); expect(req.referrerPolicy).to.equal(''); }); it('should accept refererPolicy', () => { const referrerPolicy = 'unsafe-url'; const req = new Request('http://example.com', {referrerPolicy}); expect(req.referrerPolicy).to.equal(referrerPolicy); }); it('should accept referrerPolicy from input', () => { const referrerPolicy = 'unsafe-url'; const req = new Request(new Request('http://example.com', {referrerPolicy})); expect(req.referrerPolicy).to.equal(referrerPolicy); }); it('should throw a TypeError for an invalid referrerPolicy', () => { expect(() => { const req = new Request('http://example.com', {referrerPolicy: 'foobar'}); expect.fail(req); }).to.throw(TypeError, 'Invalid referrerPolicy: foobar'); }); }); }); describe('utils/referrer', () => { it('default policy should be strict-origin-when-cross-origin', () => { expect(DEFAULT_REFERRER_POLICY).to.equal('strict-origin-when-cross-origin'); }); describe('stripURLForUseAsAReferrer', () => { it('should return no-referrer for null/undefined URL', () => { expect(stripURLForUseAsAReferrer(undefined)).to.equal('no-referrer'); expect(stripURLForUseAsAReferrer(null)).to.equal('no-referrer'); }); it('should return no-referrer for about:, blob:, and data: URLs', () => { expect(stripURLForUseAsAReferrer('about:client')).to.equal('no-referrer'); expect(stripURLForUseAsAReferrer('blob:theblog')).to.equal('no-referrer'); expect(stripURLForUseAsAReferrer('data:,thedata')).to.equal('no-referrer'); }); it('should strip the username, password, and hash', () => { const urlStr = 'http://foo:bar@example.com/foo?q=search#theanchor'; expect(stripURLForUseAsAReferrer(urlStr).toString()) .to.equal('http://example.com/foo?q=search'); }); it('should strip the pathname and query when origin-only', () => { const urlStr = 'http://foo:bar@example.com/foo?q=search#theanchor'; expect(stripURLForUseAsAReferrer(urlStr, true).toString()) .to.equal('http://example.com/'); }); }); describe('validateReferrerPolicy', () => { it('should return the referrer policy', () => { for (const referrerPolicy of ReferrerPolicy) { expect(validateReferrerPolicy(referrerPolicy)).to.equal(referrerPolicy); } }); it('should throw a TypeError for invalid referrer policies', () => { expect(validateReferrerPolicy.bind(null, undefined)) .to.throw(TypeError, 'Invalid referrerPolicy: undefined'); expect(validateReferrerPolicy.bind(null, null)) .to.throw(TypeError, 'Invalid referrerPolicy: null'); expect(validateReferrerPolicy.bind(null, false)) .to.throw(TypeError, 'Invalid referrerPolicy: false'); expect(validateReferrerPolicy.bind(null, 0)) .to.throw(TypeError, 'Invalid referrerPolicy: 0'); expect(validateReferrerPolicy.bind(null, 'always')) .to.throw(TypeError, 'Invalid referrerPolicy: always'); }); }); const testIsOriginPotentiallyTrustworthyStatements = func => { it('should be potentially trustworthy for HTTPS and WSS URLs', () => { expect(func(new URL('https://example.com'))).to.be.true; expect(func(new URL('wss://example.com'))).to.be.true; }); it('should be potentially trustworthy for loopback IP address URLs', () => { expect(func(new URL('http://127.0.0.1'))).to.be.true; expect(func(new URL('http://127.1.2.3'))).to.be.true; expect(func(new URL('ws://[::1]'))).to.be.true; }); it('should not be potentially trustworthy for "localhost" URLs', () => { expect(func(new URL('http://localhost'))).to.be.false; }); it('should be potentially trustworthy for file: URLs', () => { expect(func(new URL('file://foo/bar'))).to.be.true; }); it('should not be potentially trustworthy for all other origins', () => { expect(func(new URL('http://example.com'))).to.be.false; expect(func(new URL('ws://example.com'))).to.be.false; }); }; describe('isOriginPotentiallyTrustworthy', () => { testIsOriginPotentiallyTrustworthyStatements(isOriginPotentiallyTrustworthy); }); describe('isUrlPotentiallyTrustworthy', () => { it('should be potentially trustworthy for about:blank and about:srcdoc', () => { expect(isUrlPotentiallyTrustworthy(new URL('about:blank'))).to.be.true; expect(isUrlPotentiallyTrustworthy(new URL('about:srcdoc'))).to.be.true; }); it('should be potentially trustworthy for data: URLs', () => { expect(isUrlPotentiallyTrustworthy(new URL('data:,thedata'))).to.be.true; }); it('should be potentially trustworthy for blob: and filesystem: URLs', () => { expect(isUrlPotentiallyTrustworthy(new URL('blob:theblob'))).to.be.true; expect(isUrlPotentiallyTrustworthy(new URL('filesystem:thefilesystem'))).to.be.true; }); testIsOriginPotentiallyTrustworthyStatements(isUrlPotentiallyTrustworthy); }); describe('determineRequestsReferrer', () => { it('should return null for no-referrer or empty referrerPolicy', () => { expect(determineRequestsReferrer({referrer: 'no-referrer'})).to.be.null; expect(determineRequestsReferrer({referrerPolicy: ''})).to.be.null; }); it('should return no-referrer for about:client', () => { expect(determineRequestsReferrer({ referrer: 'about:client', referrerPolicy: DEFAULT_REFERRER_POLICY })).to.equal('no-referrer'); }); it('should return just the origin for URLs over 4096 characters', () => { expect(determineRequestsReferrer({ url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: `http://example.com/${'0'.repeat(4096)}`, referrerPolicy: DEFAULT_REFERRER_POLICY }).toString()).to.equal('http://example.com/'); }); it('should alter the referrer URL by callback', () => { expect(determineRequestsReferrer({ url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor', referrerPolicy: 'unsafe-url' }, { referrerURLCallback: referrerURL => { return new URL(referrerURL.toString().replace(/^http:/, 'myprotocol:')); } }).toString()).to.equal('myprotocol://example.com/foo?q=search'); }); it('should alter the referrer origin by callback', () => { expect(determineRequestsReferrer({ url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor', referrerPolicy: 'origin' }, { referrerOriginCallback: referrerOrigin => { return new URL(referrerOrigin.toString().replace(/^http:/, 'myprotocol:')); } }).toString()).to.equal('myprotocol://example.com/'); }); it('should throw a TypeError for an invalid policy', () => { expect(() => { determineRequestsReferrer({ url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor', referrerPolicy: 'always' }); }).to.throw(TypeError, 'Invalid referrerPolicy: always'); }); const referrerPolicyTestLabel = ({currentURLTrust, referrerURLTrust, sameOrigin}) => { if (currentURLTrust === null && referrerURLTrust === null && sameOrigin === null) { return 'Always'; } const result = []; if (currentURLTrust !== null) { result.push(`Current URL is ${currentURLTrust ? '' : 'not '}potentially trustworthy`); } if (referrerURLTrust !== null) { result.push(`Referrer URL is ${referrerURLTrust ? '' : 'not '}potentially trustworthy`); } if (sameOrigin !== null) { result.push(`Current URL & Referrer URL do ${sameOrigin ? '' : 'not '}have same origin`); } return result.join(', '); }; const referrerPolicyTests = (referrerPolicy, matrix) => { describe(`Referrer policy: ${referrerPolicy}`, () => { for (const {currentURLTrust, referrerURLTrust, sameOrigin, result} of matrix) { describe(referrerPolicyTestLabel({currentURLTrust, referrerURLTrust, sameOrigin}), () => { const requests = []; if (sameOrigin === true || sameOrigin === null) { requests.push({ referrerPolicy, url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor' }); } if (sameOrigin === false || sameOrigin === null) { requests.push({ referrerPolicy, url: 'http://foo:bar@example2.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor' }); } let requestsLength = requests.length; switch (currentURLTrust) { case null: for (let i = 0; i < requestsLength; i++) { const req = requests[i]; requests.push({...req, url: req.url.replace(/^http:/, 'https:')}); } break; case true: for (let i = 0; i < requestsLength; i++) { const req = requests[i]; req.url = req.url.replace(/^http:/, 'https:'); } break; case false: // nothing to do, default is not potentially trustworthy break; default: throw new TypeError(`Invalid currentURLTrust condition: ${currentURLTrust}`); } requestsLength = requests.length; switch (referrerURLTrust) { case null: for (let i = 0; i < requestsLength; i++) { const req = requests[i]; if (sameOrigin) { if (req.url.startsWith('https:')) { requests.splice(i, 1); } else { continue; } } requests.push({...req, referrer: req.referrer.replace(/^http:/, 'https:')}); } break; case true: for (let i = 0; i < requestsLength; i++) { const req = requests[i]; req.referrer = req.referrer.replace(/^http:/, 'https:'); } break; case false: // nothing to do, default is not potentially trustworthy break; default: throw new TypeError(`Invalid referrerURLTrust condition: ${referrerURLTrust}`); } it('should have tests', () => { expect(requests).to.not.be.empty; }); for (const req of requests) { it(`should return ${result} for url: ${req.url}, referrer: ${req.referrer}`, () => { if (result === 'no-referrer') { return expect(determineRequestsReferrer(req).toString()) .to.equal('no-referrer'); } if (result === 'referrer-origin') { const referrerOrigih = stripURLForUseAsAReferrer(req.referrer, true); return expect(determineRequestsReferrer(req).toString()) .to.equal(referrerOrigih.toString()); } if (result === 'referrer-url') { const referrerURL = stripURLForUseAsAReferrer(req.referrer); return expect(determineRequestsReferrer(req).toString()) .to.equal(referrerURL.toString()); } throw new TypeError(`Invalid result: ${result}`); }); } }); } }); }; // 3.1 no-referrer referrerPolicyTests('no-referrer', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'no-referrer'} ]); // 3.2 no-referrer-when-downgrade referrerPolicyTests('no-referrer-when-downgrade', [ {currentURLTrust: false, referrerURLTrust: true, sameOrigin: null, result: 'no-referrer'}, {currentURLTrust: null, referrerURLTrust: false, sameOrigin: null, result: 'referrer-url'}, {currentURLTrust: true, referrerURLTrust: true, sameOrigin: null, result: 'referrer-url'} ]); // 3.3 same-origin referrerPolicyTests('same-origin', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: false, result: 'no-referrer'}, {currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'} ]); // 3.4 origin referrerPolicyTests('origin', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'referrer-origin'} ]); // 3.5 strict-origin referrerPolicyTests('strict-origin', [ {currentURLTrust: false, referrerURLTrust: true, sameOrigin: null, result: 'no-referrer'}, {currentURLTrust: null, referrerURLTrust: false, sameOrigin: null, result: 'referrer-origin'}, {currentURLTrust: true, referrerURLTrust: true, sameOrigin: null, result: 'referrer-origin'} ]); // 3.6 origin-when-cross-origin referrerPolicyTests('origin-when-cross-origin', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: false, result: 'referrer-origin'}, {currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'} ]); // 3.7 strict-origin-when-cross-origin referrerPolicyTests('strict-origin-when-cross-origin', [ {currentURLTrust: false, referrerURLTrust: true, sameOrigin: false, result: 'no-referrer'}, {currentURLTrust: null, referrerURLTrust: false, sameOrigin: false, result: 'referrer-origin'}, {currentURLTrust: true, referrerURLTrust: true, sameOrigin: false, result: 'referrer-origin'}, {currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'} ]); // 3.8 unsafe-url referrerPolicyTests('unsafe-url', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'referrer-url'} ]); }); describe('parseReferrerPolicyFromHeader', () => { it('should return an empty string when no referrer policy is found', () => { expect(parseReferrerPolicyFromHeader(new Headers())).to.equal(''); expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', '']]) )).to.equal(''); }); it('should return the last valid referrer policy', () => { expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', 'no-referrer']]) )).to.equal('no-referrer'); expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', 'no-referrer unsafe-url']]) )).to.equal('unsafe-url'); expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', 'foo no-referrer bar']]) )).to.equal('no-referrer'); expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', 'foo no-referrer unsafe-url bar']]) )).to.equal('unsafe-url'); }); it('should use all Referrer-Policy headers', () => { expect(parseReferrerPolicyFromHeader( new Headers([ ['Referrer-Policy', 'no-referrer'], ['Referrer-Policy', ''] ]) )).to.equal('no-referrer'); expect(parseReferrerPolicyFromHeader( new Headers([ ['Referrer-Policy', 'no-referrer'], ['Referrer-Policy', 'unsafe-url'] ]) )).to.equal('unsafe-url'); expect(parseReferrerPolicyFromHeader( new Headers([ ['Referrer-Policy', 'no-referrer foo'], ['Referrer-Policy', 'bar unsafe-url wow'] ]) )).to.equal('unsafe-url'); expect(parseReferrerPolicyFromHeader( new Headers([ ['Referrer-Policy', 'no-referrer unsafe-url'], ['Referrer-Policy', 'foo bar'] ]) )).to.equal('unsafe-url'); }); }); });