Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 167x 167x 167x 176x 176x 167x 9x 167x 167x 218x 185x 33x 23x 23x 23x 23x 10x 10x 10x 4x 6x 167x | export type FetchInput = Parameters<typeof fetch>[0];
export type ResolvedFetchInput = {
/** The URL that `fetch` will request. */
url: URL;
/**
* To hand to `fetch` in place of the caller's input: resolving it again
* always yields `url`. The guarantee is over the destination only — a
* `Request` body stream is still the caller's.
*/
input: FetchInput;
};
/**
* The `url` accessor is not always an own property of `Request.prototype`:
* jsdom exposes a `Request` subclass whose own prototype carries only
* `constructor`, leaving the accessor a level up.
*
* @returns The accessor, or `undefined` where `url` is a data property rather
* than an accessor, as a polyfill assigning `this.url` leaves it.
*/
const findRequestUrlAccessor = (): ((this: Request) => string) | undefined => {
// Not an `instanceof` guard's problem: this runs at module load, where a
// realm without `Request` would otherwise fail the import outright.
let proto: object | null =
typeof Request === 'undefined' ? null : Request.prototype;
while (proto) {
// Deliberately unbound: applied to whichever `Request` a caller passes.
// eslint-disable-next-line @typescript-eslint/unbound-method
const { get } = Object.getOwnPropertyDescriptor(proto, 'url') ?? {};
if (get) {
return get as (this: Request) => string;
}
proto = Object.getPrototypeOf(proto);
}
return undefined;
};
/**
* Captured at module load so that later tampering cannot redirect the read.
* `fetch` takes a `Request`'s URL from the internal slot this accessor reads,
* whereas `request.url` may be an override on a subclass.
*/
const getRequestUrl = findRequestUrlAccessor();
/**
* Resolve a `fetch` input to the URL that will actually be requested, along
* with a stand-in input that cannot resolve to any other URL.
*
* `fetch` accepts any object with a stringifier and stringifies it itself, so
* code that validates `new URL(input)` and then forwards the caller's `input`
* lets the input decide what each read returns — validating one URL while
* requesting another (CWE-367). A `Request` is no safer: a subclass can
* override `url`, and on some runtimes the state behind it is a mutable own
* property. Forwarding the returned `input` closes both: its destination comes
* from a string resolved here, so the check and the request cannot disagree.
*
* The result is deliberately not hardened: `harden` would transitively freeze
* `URL.prototype`.
*
* @param input - The first argument to `fetch`: a string, `URL`, `Request`, or
* anything else `fetch` would stringify.
* @returns The resolved URL and the input to forward in its place.
* @throws If the input does not resolve to a valid absolute URL.
*/
export const resolveFetchInput = (input: FetchInput): ResolvedFetchInput => {
// A string is immutable, so it is already its own stand-in.
if (typeof input === 'string') {
return { url: new URL(input), input };
}
if (input instanceof Request) {
// Copy first, so that only a genuine `Request`'s internals are read below.
// The rebuild reads its argument as a `RequestInit`, by string name, so a
// `dispatcher` planted on the caller's own object — undici's hook for where
// the bytes go — would route the request anywhere. Also throws for anything
// wearing `Request.prototype` without the backing state.
const genuine = new Request(input);
// Copying alone is not enough: undici on Node 22 keeps a `Request`'s state
// in a configurable own property, so a caller can leave a `URL` it still
// holds in there — copied by reference, mutated after the check, re-read at
// send time. Parsing a string fixes the destination.
//
// Preferring the captured accessor is hardening, not the invariant: whatever
// `href` turns out to be, the guard checks `new URL(href)` and the stand-in
// is built from `url.href`, and `RequestInit` has no `url` member for
// `genuine` to override. So check and request are pinned to the same string
// either way; reading the slot only denies a lying subclass the choice of
// which URL gets submitted for approval.
const href = getRequestUrl ? getRequestUrl.call(genuine) : genuine.url;
const url = new URL(href);
return { url, input: new Request(url.href, genuine) };
}
// A `URL`, or an object with a stringifier. Parsed from the captured
// primitive, so `input` is never consulted again.
const href = String(input);
const url = new URL(href);
// Forwarding a primitive already makes a second read harmless, but an input
// that answers differently each time is an escape attempt, not a mistake:
// surface it rather than silently requesting whichever URL came first.
if (String(input) !== href) {
throw new Error('fetch input resolved to a different URL when read again.');
}
return { url, input: url.href };
};
harden(resolveFetchInput);
|