UNPKG

11.8 kBJavaScriptView Raw
1import {isIP} from 'node:net';
2
3/**
4 * @external URL
5 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URL|URL}
6 */
7
8/**
9 * @module utils/referrer
10 * @private
11 */
12
13/**
14 * @see {@link https://w3c.github.io/webappsec-referrer-policy/#strip-url|Referrer Policy §8.4. Strip url for use as a referrer}
15 * @param {string} URL
16 * @param {boolean} [originOnly=false]
17 */
18export function stripURLForUseAsAReferrer(url, originOnly = false) {
19 // 1. If url is null, return no referrer.
20 if (url == null) { // eslint-disable-line no-eq-null, eqeqeq
21 return 'no-referrer';
22 }
23
24 url = new URL(url);
25
26 // 2. If url's scheme is a local scheme, then return no referrer.
27 if (/^(about|blob|data):$/.test(url.protocol)) {
28 return 'no-referrer';
29 }
30
31 // 3. Set url's username to the empty string.
32 url.username = '';
33
34 // 4. Set url's password to null.
35 // Note: `null` appears to be a mistake as this actually results in the password being `"null"`.
36 url.password = '';
37
38 // 5. Set url's fragment to null.
39 // Note: `null` appears to be a mistake as this actually results in the fragment being `"#null"`.
40 url.hash = '';
41
42 // 6. If the origin-only flag is true, then:
43 if (originOnly) {
44 // 6.1. Set url's path to null.
45 // Note: `null` appears to be a mistake as this actually results in the path being `"/null"`.
46 url.pathname = '';
47
48 // 6.2. Set url's query to null.
49 // Note: `null` appears to be a mistake as this actually results in the query being `"?null"`.
50 url.search = '';
51 }
52
53 // 7. Return url.
54 return url;
55}
56
57/**
58 * @see {@link https://w3c.github.io/webappsec-referrer-policy/#enumdef-referrerpolicy|enum ReferrerPolicy}
59 */
60export const ReferrerPolicy = new Set([
61 '',
62 'no-referrer',
63 'no-referrer-when-downgrade',
64 'same-origin',
65 'origin',
66 'strict-origin',
67 'origin-when-cross-origin',
68 'strict-origin-when-cross-origin',
69 'unsafe-url'
70]);
71
72/**
73 * @see {@link https://w3c.github.io/webappsec-referrer-policy/#default-referrer-policy|default referrer policy}
74 */
75export const DEFAULT_REFERRER_POLICY = 'strict-origin-when-cross-origin';
76
77/**
78 * @see {@link https://w3c.github.io/webappsec-referrer-policy/#referrer-policies|Referrer Policy §3. Referrer Policies}
79 * @param {string} referrerPolicy
80 * @returns {string} referrerPolicy
81 */
82export function validateReferrerPolicy(referrerPolicy) {
83 if (!ReferrerPolicy.has(referrerPolicy)) {
84 throw new TypeError(`Invalid referrerPolicy: ${referrerPolicy}`);
85 }
86
87 return referrerPolicy;
88}
89
90/**
91 * @see {@link https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy|Referrer Policy §3.2. Is origin potentially trustworthy?}
92 * @param {external:URL} url
93 * @returns `true`: "Potentially Trustworthy", `false`: "Not Trustworthy"
94 */
95export function isOriginPotentiallyTrustworthy(url) {
96 // 1. If origin is an opaque origin, return "Not Trustworthy".
97 // Not applicable
98
99 // 2. Assert: origin is a tuple origin.
100 // Not for implementations
101
102 // 3. If origin's scheme is either "https" or "wss", return "Potentially Trustworthy".
103 if (/^(http|ws)s:$/.test(url.protocol)) {
104 return true;
105 }
106
107 // 4. If origin's host component matches one of the CIDR notations 127.0.0.0/8 or ::1/128 [RFC4632], return "Potentially Trustworthy".
108 const hostIp = url.host.replace(/(^\[)|(]$)/g, '');
109 const hostIPVersion = isIP(hostIp);
110
111 if (hostIPVersion === 4 && /^127\./.test(hostIp)) {
112 return true;
113 }
114
115 if (hostIPVersion === 6 && /^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(hostIp)) {
116 return true;
117 }
118
119 // 5. If origin's host component is "localhost" or falls within ".localhost", and the user agent conforms to the name resolution rules in [let-localhost-be-localhost], return "Potentially Trustworthy".
120 // We are returning FALSE here because we cannot ensure conformance to
121 // let-localhost-be-loalhost (https://tools.ietf.org/html/draft-west-let-localhost-be-localhost)
122 if (url.host === 'localhost' || url.host.endsWith('.localhost')) {
123 return false;
124 }
125
126 // 6. If origin's scheme component is file, return "Potentially Trustworthy".
127 if (url.protocol === 'file:') {
128 return true;
129 }
130
131 // 7. If origin's scheme component is one which the user agent considers to be authenticated, return "Potentially Trustworthy".
132 // Not supported
133
134 // 8. If origin has been configured as a trustworthy origin, return "Potentially Trustworthy".
135 // Not supported
136
137 // 9. Return "Not Trustworthy".
138 return false;
139}
140
141/**
142 * @see {@link https://w3c.github.io/webappsec-secure-contexts/#is-url-trustworthy|Referrer Policy §3.3. Is url potentially trustworthy?}
143 * @param {external:URL} url
144 * @returns `true`: "Potentially Trustworthy", `false`: "Not Trustworthy"
145 */
146export function isUrlPotentiallyTrustworthy(url) {
147 // 1. If url is "about:blank" or "about:srcdoc", return "Potentially Trustworthy".
148 if (/^about:(blank|srcdoc)$/.test(url)) {
149 return true;
150 }
151
152 // 2. If url's scheme is "data", return "Potentially Trustworthy".
153 if (url.protocol === 'data:') {
154 return true;
155 }
156
157 // Note: The origin of blob: and filesystem: URLs is the origin of the context in which they were
158 // created. Therefore, blobs created in a trustworthy origin will themselves be potentially
159 // trustworthy.
160 if (/^(blob|filesystem):$/.test(url.protocol)) {
161 return true;
162 }
163
164 // 3. Return the result of executing §3.2 Is origin potentially trustworthy? on url's origin.
165 return isOriginPotentiallyTrustworthy(url);
166}
167
168/**
169 * Modifies the referrerURL to enforce any extra security policy considerations.
170 * @see {@link https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer|Referrer Policy §8.3. Determine request's Referrer}, step 7
171 * @callback module:utils/referrer~referrerURLCallback
172 * @param {external:URL} referrerURL
173 * @returns {external:URL} modified referrerURL
174 */
175
176/**
177 * Modifies the referrerOrigin to enforce any extra security policy considerations.
178 * @see {@link https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer|Referrer Policy §8.3. Determine request's Referrer}, step 7
179 * @callback module:utils/referrer~referrerOriginCallback
180 * @param {external:URL} referrerOrigin
181 * @returns {external:URL} modified referrerOrigin
182 */
183
184/**
185 * @see {@link https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer|Referrer Policy §8.3. Determine request's Referrer}
186 * @param {Request} request
187 * @param {object} o
188 * @param {module:utils/referrer~referrerURLCallback} o.referrerURLCallback
189 * @param {module:utils/referrer~referrerOriginCallback} o.referrerOriginCallback
190 * @returns {external:URL} Request's referrer
191 */
192export function determineRequestsReferrer(request, {referrerURLCallback, referrerOriginCallback} = {}) {
193 // There are 2 notes in the specification about invalid pre-conditions. We return null, here, for
194 // these cases:
195 // > Note: If request's referrer is "no-referrer", Fetch will not call into this algorithm.
196 // > Note: If request's referrer policy is the empty string, Fetch will not call into this
197 // > algorithm.
198 if (request.referrer === 'no-referrer' || request.referrerPolicy === '') {
199 return null;
200 }
201
202 // 1. Let policy be request's associated referrer policy.
203 const policy = request.referrerPolicy;
204
205 // 2. Let environment be request's client.
206 // not applicable to node.js
207
208 // 3. Switch on request's referrer:
209 if (request.referrer === 'about:client') {
210 return 'no-referrer';
211 }
212
213 // "a URL": Let referrerSource be request's referrer.
214 const referrerSource = request.referrer;
215
216 // 4. Let request's referrerURL be the result of stripping referrerSource for use as a referrer.
217 let referrerURL = stripURLForUseAsAReferrer(referrerSource);
218
219 // 5. Let referrerOrigin be the result of stripping referrerSource for use as a referrer, with the
220 // origin-only flag set to true.
221 let referrerOrigin = stripURLForUseAsAReferrer(referrerSource, true);
222
223 // 6. If the result of serializing referrerURL is a string whose length is greater than 4096, set
224 // referrerURL to referrerOrigin.
225 if (referrerURL.toString().length > 4096) {
226 referrerURL = referrerOrigin;
227 }
228
229 // 7. The user agent MAY alter referrerURL or referrerOrigin at this point to enforce arbitrary
230 // policy considerations in the interests of minimizing data leakage. For example, the user
231 // agent could strip the URL down to an origin, modify its host, replace it with an empty
232 // string, etc.
233 if (referrerURLCallback) {
234 referrerURL = referrerURLCallback(referrerURL);
235 }
236
237 if (referrerOriginCallback) {
238 referrerOrigin = referrerOriginCallback(referrerOrigin);
239 }
240
241 // 8.Execute the statements corresponding to the value of policy:
242 const currentURL = new URL(request.url);
243
244 switch (policy) {
245 case 'no-referrer':
246 return 'no-referrer';
247
248 case 'origin':
249 return referrerOrigin;
250
251 case 'unsafe-url':
252 return referrerURL;
253
254 case 'strict-origin':
255 // 1. If referrerURL is a potentially trustworthy URL and request's current URL is not a
256 // potentially trustworthy URL, then return no referrer.
257 if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) {
258 return 'no-referrer';
259 }
260
261 // 2. Return referrerOrigin.
262 return referrerOrigin.toString();
263
264 case 'strict-origin-when-cross-origin':
265 // 1. If the origin of referrerURL and the origin of request's current URL are the same, then
266 // return referrerURL.
267 if (referrerURL.origin === currentURL.origin) {
268 return referrerURL;
269 }
270
271 // 2. If referrerURL is a potentially trustworthy URL and request's current URL is not a
272 // potentially trustworthy URL, then return no referrer.
273 if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) {
274 return 'no-referrer';
275 }
276
277 // 3. Return referrerOrigin.
278 return referrerOrigin;
279
280 case 'same-origin':
281 // 1. If the origin of referrerURL and the origin of request's current URL are the same, then
282 // return referrerURL.
283 if (referrerURL.origin === currentURL.origin) {
284 return referrerURL;
285 }
286
287 // 2. Return no referrer.
288 return 'no-referrer';
289
290 case 'origin-when-cross-origin':
291 // 1. If the origin of referrerURL and the origin of request's current URL are the same, then
292 // return referrerURL.
293 if (referrerURL.origin === currentURL.origin) {
294 return referrerURL;
295 }
296
297 // Return referrerOrigin.
298 return referrerOrigin;
299
300 case 'no-referrer-when-downgrade':
301 // 1. If referrerURL is a potentially trustworthy URL and request's current URL is not a
302 // potentially trustworthy URL, then return no referrer.
303 if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) {
304 return 'no-referrer';
305 }
306
307 // 2. Return referrerURL.
308 return referrerURL;
309
310 default:
311 throw new TypeError(`Invalid referrerPolicy: ${policy}`);
312 }
313}
314
315/**
316 * @see {@link https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header|Referrer Policy §8.1. Parse a referrer policy from a Referrer-Policy header}
317 * @param {Headers} headers Response headers
318 * @returns {string} policy
319 */
320export function parseReferrerPolicyFromHeader(headers) {
321 // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy`
322 // and response’s header list.
323 const policyTokens = (headers.get('referrer-policy') || '').split(/[,\s]+/);
324
325 // 2. Let policy be the empty string.
326 let policy = '';
327
328 // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty
329 // string, then set policy to token.
330 // Note: This algorithm loops over multiple policy values to allow deployment of new policy
331 // values with fallbacks for older user agents, as described in § 11.1 Unknown Policy Values.
332 for (const token of policyTokens) {
333 if (token && ReferrerPolicy.has(token)) {
334 policy = token;
335 }
336 }
337
338 // 4. Return policy.
339 return policy;
340}