-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathloopback-guard.ts
More file actions
58 lines (56 loc) · 3.12 KB
/
Copy pathloopback-guard.ts
File metadata and controls
58 lines (56 loc) · 3.12 KB
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
/**
* The shared rule for every loopback listener Dormouse binds.
*
* **A loopback bind is not an access control.** `127.0.0.1` keeps out the
* network, but the attacker that matters is a web page open in the user's own
* browser, and that page reaches loopback exactly as easily as our own webview
* does. An ephemeral port is not a secret either — the range scans in seconds.
*
* The rule is about *privilege*, not admission: no listener may grant an
* unrecognized caller anything it could not already get by reaching the
* upstream directly. Some listeners honour that by refusing the request; the
* iframe proxy honours it by admitting everyone and vouching for no one. Both
* are answers to the same two questions:
*
* 1. **Was I addressed by my own loopback name?** (`isLoopbackHost`)
* A hostile domain re-pointed at 127.0.0.1 — DNS rebinding — arrives with
* its own name still in `Host`, and the browser considers that
* same-origin, so no CORS header ever gets a say. Checking `Host` is what
* makes rebinding fail. A listener that already demands an unguessable
* one-shot token gains nothing from it, since rebinding exists only to
* make same-origin-looking requests, and may skip it.
* 2. **Do I recognize this caller?** (`isOwnOrigin`, or a credential)
* The mechanism is forced by the listener's URL, not chosen: a token works
* where we own that URL, and cannot work where the URL is a page's own
* origin — it would land in `location.pathname`, break client-side
* routers, and never survive onto root-relative sub-resource requests.
*
* `SECURITY.md` → "Loopback Listeners" is the authority on which listeners
* exist and how each answers; it is deliberately not restated here, since it
* tells its reader to derive that set by search rather than trust a list.
*/
/**
* True when `Host` names this listener's own loopback address. Both spellings
* are accepted because either can appear in a hand-typed URL; neither is a
* rebinding vector, since browsers refuse to rebind them.
*/
export function isLoopbackHost(hostHeader: string | undefined, port: number): boolean {
const host = (hostHeader ?? '').toLowerCase();
return host === `127.0.0.1:${port}` || host === `localhost:${port}`;
}
/**
* True when `Origin` is this listener's own origin — i.e. the caller is a page
* we ourselves served, not a foreign site.
*
* An **absent** `Origin` is not "own": browsers omit it on top-level
* navigations and same-origin GETs, so callers must decide what absence means
* for them rather than having this function guess.
*/
export function isOwnOrigin(originHeader: string | undefined, port: number): boolean {
// An `Origin` is a *serialized* origin — scheme, host, port, nothing else —
// so an exact compare is the whole test; parsing it would only add ways to be
// lenient. Anything non-canonical fails, which is the safe direction: the
// caller then declines to vouch and forwards the header untouched.
const origin = (originHeader ?? '').toLowerCase();
return origin === `http://127.0.0.1:${port}` || origin === `http://localhost:${port}`;
}