@open-mercato/cezar 0.8.0-pr573.521 → 0.8.0-pr573.532

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,9 +19,25 @@ export interface Capabilities {
19
19
  localHandoff: boolean;
20
20
  followups: boolean;
21
21
  }
22
- /** True for hosts that only the local machine can reach. Undefined = the
23
- * default bind (127.0.0.1). */
22
+ /** Canonical hostname of an authority: port and IPv6 brackets removed, IPv6
23
+ * expanded, zone id and FQDN trailing dot dropped, lowercased.
24
+ * `[::1]:4321` → `0:0:0:0:0:0:0:1`, `localhost.:4321` → `localhost`.
25
+ *
26
+ * Strict by construction: an authority that does not parse as one of the three
27
+ * legal shapes returns `''`, which no allowlist matches. Being lax here is a
28
+ * security bug, not a convenience — a parser that merely *finds* a loopback
29
+ * name inside the header would accept `[::1]@evil.com` or `127.0.0.1:evil.com`. */
30
+ export declare function normalizeHostname(host: string): string;
31
+ /** True for bind hosts that only the local machine can reach. Undefined = the
32
+ * default bind (127.0.0.1), hence trusted — this is a *configuration* value we
33
+ * chose, not a request header. Do NOT use this on attacker-controlled input;
34
+ * use `isLoopbackHostHeader`, which fails closed on a missing host. */
24
35
  export declare function isLoopbackHost(host: string | undefined): boolean;
36
+ /** True for a request's `Host`/`Origin` hostname when it names this machine.
37
+ * The untrusted-input twin of `isLoopbackHost`: a missing or unparseable host
38
+ * is **untrusted** (false), because absent is not the same as "we defaulted to
39
+ * loopback" once the value arrives over the wire (#426). */
40
+ export declare function isLoopbackHostHeader(host: string | null | undefined): boolean;
25
41
  /** `CEZ_REMOTE=1` or a non-loopback bind host ⇒ hosted mode (no local handoff).
26
42
  * `CEZ_FOLLOWUPS=1` ⇒ the follow-up inbox exists (#471).
27
43
  *
@@ -16,13 +16,90 @@
16
16
  * The per-task handoff journal is independent and runs either way.
17
17
  */
18
18
  import { followupsEnabled } from '../handoff.js';
19
- /** True for hosts that only the local machine can reach. Undefined = the
20
- * default bind (127.0.0.1). */
19
+ /** Every IPv4 address in 127.0.0.0/8, anchored. Anchoring is load-bearing: a
20
+ * `startsWith('127.')` test also matches attacker-controlled *hostnames* like
21
+ * `127.0.0.1.evil.com`, which is exactly the DNS-rebinding bypass #426 is
22
+ * about — see `isLoopbackHostHeader`. */
23
+ const LOOPBACK_V4 = /^127(?:\.(?:25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
24
+ /** The IPv6 loopback in the canonical form `expandIpv6` emits. */
25
+ const LOOPBACK_V6 = '0:0:0:0:0:0:0:1';
26
+ /** Expand an IPv6 literal to all 8 groups, lowercase and without leading zeros
27
+ * (`::1` → `0:0:0:0:0:0:0:1`), or `null` when it is not a valid literal.
28
+ *
29
+ * Canonicalizing rather than merely *recognizing* matters: `Host` carries
30
+ * whatever spelling the client sent, but `new URL().hostname` compresses
31
+ * (`[0:0:0:0:0:0:0:1]` → `[::1]`), so the two only compare equal on a common
32
+ * form. */
33
+ function expandIpv6(h) {
34
+ const halves = h.split('::');
35
+ if (halves.length > 2)
36
+ return null;
37
+ let groups;
38
+ if (halves.length === 2) {
39
+ const head = halves[0] ? halves[0].split(':') : [];
40
+ const tail = halves[1] ? halves[1].split(':') : [];
41
+ if (head.length + tail.length > 7)
42
+ return null;
43
+ groups = [...head, ...Array(8 - head.length - tail.length).fill('0'), ...tail];
44
+ }
45
+ else {
46
+ groups = h.split(':');
47
+ }
48
+ if (groups.length !== 8 || !groups.every((g) => /^[0-9a-f]{1,4}$/.test(g)))
49
+ return null;
50
+ return groups.map((g) => parseInt(g, 16).toString(16)).join(':');
51
+ }
52
+ /** Canonical hostname of an authority: port and IPv6 brackets removed, IPv6
53
+ * expanded, zone id and FQDN trailing dot dropped, lowercased.
54
+ * `[::1]:4321` → `0:0:0:0:0:0:0:1`, `localhost.:4321` → `localhost`.
55
+ *
56
+ * Strict by construction: an authority that does not parse as one of the three
57
+ * legal shapes returns `''`, which no allowlist matches. Being lax here is a
58
+ * security bug, not a convenience — a parser that merely *finds* a loopback
59
+ * name inside the header would accept `[::1]@evil.com` or `127.0.0.1:evil.com`. */
60
+ export function normalizeHostname(host) {
61
+ // 1. Bracketed IPv6, optionally with a port: `[::1]`, `[::1%25eth0]:4321`.
62
+ // The end anchor is load-bearing — without it `[::1]@evil.com` → `::1`.
63
+ const bracketed = host.match(/^\[([^\]]+)\](?::\d+)?$/);
64
+ if (bracketed)
65
+ return expandIpv6(stripZone(bracketed[1].toLowerCase())) ?? '';
66
+ // 2. A bare IPv6 literal cannot carry a port (no brackets ⇒ every colon
67
+ // belongs to the address), so anything with >1 colon is taken whole.
68
+ if (host.split(':').length > 2)
69
+ return expandIpv6(stripZone(host.toLowerCase())) ?? '';
70
+ // 3. `name` or `name:port`, where the port must actually be digits.
71
+ const plain = host.match(/^([^:[\]]*)(?::(\d+))?$/);
72
+ if (plain)
73
+ return plain[1].toLowerCase().replace(/\.$/, '');
74
+ return ''; // unparseable ⇒ matches no allowlist
75
+ }
76
+ /** Drop an IPv6 zone id (`fe80::1%eth0`), anchored to the end so a `%` inside a
77
+ * registrable hostname can never truncate it down to a loopback name. */
78
+ function stripZone(h) {
79
+ return h.replace(/%[a-z0-9._~-]+$/, '');
80
+ }
81
+ /** True when the hostname names this machine and nothing else. Expects the
82
+ * canonical output of `normalizeHostname`. */
83
+ function isLoopbackName(hostname) {
84
+ return hostname === 'localhost' || LOOPBACK_V4.test(hostname) || hostname === LOOPBACK_V6;
85
+ }
86
+ /** True for bind hosts that only the local machine can reach. Undefined = the
87
+ * default bind (127.0.0.1), hence trusted — this is a *configuration* value we
88
+ * chose, not a request header. Do NOT use this on attacker-controlled input;
89
+ * use `isLoopbackHostHeader`, which fails closed on a missing host. */
21
90
  export function isLoopbackHost(host) {
22
91
  if (!host)
23
92
  return true;
24
- const h = host.toLowerCase().replace(/^\[|\]$/g, '');
25
- return h === 'localhost' || h === '::1' || h.startsWith('127.');
93
+ return isLoopbackName(normalizeHostname(host));
94
+ }
95
+ /** True for a request's `Host`/`Origin` hostname when it names this machine.
96
+ * The untrusted-input twin of `isLoopbackHost`: a missing or unparseable host
97
+ * is **untrusted** (false), because absent is not the same as "we defaulted to
98
+ * loopback" once the value arrives over the wire (#426). */
99
+ export function isLoopbackHostHeader(host) {
100
+ if (!host)
101
+ return false;
102
+ return isLoopbackName(normalizeHostname(host));
26
103
  }
27
104
  /** `CEZ_REMOTE=1` or a non-loopback bind host ⇒ hosted mode (no local handoff).
28
105
  * `CEZ_FOLLOWUPS=1` ⇒ the follow-up inbox exists (#471).
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../src/server/capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAOjD;gCACgC;AAChC,MAAM,UAAU,cAAc,CAAC,IAAwB;IACrD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,KAAK,WAAW,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;+FAO+F;AAC/F,MAAM,UAAU,mBAAmB,CAAC,GAAG,GAAsB,OAAO,CAAC,GAAG,EAAE,QAAiB;IACzF,OAAO;QACL,YAAY,EAAE,GAAG,CAAC,UAAU,KAAK,GAAG,IAAI,cAAc,CAAC,QAAQ,CAAC;QAChE,4EAA4E;QAC5E,oEAAoE;QACpE,SAAS,EAAE,gBAAgB,CAAC,GAAG,CAAC;KACjC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../src/server/capabilities.ts"],"names":[],"mappings":"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"}