1 | import {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 | */
|
18 | export 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 | */
|
60 | export 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 | */
|
75 | export 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 | */
|
82 | export 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 | */
|
95 | export 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 | */
|
146 | export 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 | */
|
192 | export 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 | */
|
320 | export 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 | }
|