@namzu/sandbox 1.1.0 → 2.0.0
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.
- package/CHANGELOG.md +234 -0
- package/README.md +205 -105
- package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.d.ts +2 -0
- package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.d.ts.map +1 -0
- package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.js +61 -0
- package/dist/backends/aci-standby-pool/__tests__/unenforceable-controls.test.js.map +1 -0
- package/dist/backends/aci-standby-pool/index.d.ts +2 -1
- package/dist/backends/aci-standby-pool/index.d.ts.map +1 -1
- package/dist/backends/aci-standby-pool/index.js +36 -1
- package/dist/backends/aci-standby-pool/index.js.map +1 -1
- package/dist/backends/docker/__tests__/hardening.test.d.ts +2 -0
- package/dist/backends/docker/__tests__/hardening.test.d.ts.map +1 -0
- package/dist/backends/docker/__tests__/hardening.test.js +32 -0
- package/dist/backends/docker/__tests__/hardening.test.js.map +1 -0
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.d.ts +1 -1
- package/dist/backends/docker/__tests__/leaf-permissions.smoke.test.js +1 -1
- package/dist/backends/docker/index.d.ts +47 -3
- package/dist/backends/docker/index.d.ts.map +1 -1
- package/dist/backends/docker/index.js +138 -5
- package/dist/backends/docker/index.js.map +1 -1
- package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.d.ts +16 -0
- package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.js +37 -0
- package/dist/backends/firecracker/__tests__/agent-timeout-clamp.test.js.map +1 -0
- package/dist/backends/firecracker/__tests__/backend.test.js +11 -3
- package/dist/backends/firecracker/__tests__/backend.test.js.map +1 -1
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js +10 -2
- package/dist/backends/firecracker/__tests__/control-plane-mtls.test.js.map +1 -1
- package/dist/backends/firecracker/__tests__/egress-policy.test.d.ts +2 -0
- package/dist/backends/firecracker/__tests__/egress-policy.test.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/egress-policy.test.js +67 -0
- package/dist/backends/firecracker/__tests__/egress-policy.test.js.map +1 -0
- package/dist/backends/firecracker/__tests__/fixtures/ipc-path.d.ts +21 -0
- package/dist/backends/firecracker/__tests__/fixtures/ipc-path.d.ts.map +1 -0
- package/dist/backends/firecracker/__tests__/fixtures/ipc-path.js +30 -0
- package/dist/backends/firecracker/__tests__/fixtures/ipc-path.js.map +1 -0
- package/dist/backends/firecracker/__tests__/protocol.test.js +7 -17
- package/dist/backends/firecracker/__tests__/protocol.test.js.map +1 -1
- package/dist/backends/firecracker/__tests__/transport.test.js +11 -3
- package/dist/backends/firecracker/__tests__/transport.test.js.map +1 -1
- package/dist/backends/firecracker/index.d.ts +20 -1
- package/dist/backends/firecracker/index.d.ts.map +1 -1
- package/dist/backends/firecracker/index.js +60 -15
- package/dist/backends/firecracker/index.js.map +1 -1
- package/dist/egress/__tests__/allowlist.test.d.ts +2 -0
- package/dist/egress/__tests__/allowlist.test.d.ts.map +1 -0
- package/dist/egress/__tests__/allowlist.test.js +85 -0
- package/dist/egress/__tests__/allowlist.test.js.map +1 -0
- package/dist/egress/__tests__/proxy.test.d.ts +2 -0
- package/dist/egress/__tests__/proxy.test.d.ts.map +1 -0
- package/dist/egress/__tests__/proxy.test.js +177 -0
- package/dist/egress/__tests__/proxy.test.js.map +1 -0
- package/dist/egress/allowlist.d.ts +40 -0
- package/dist/egress/allowlist.d.ts.map +1 -0
- package/dist/egress/allowlist.js +81 -0
- package/dist/egress/allowlist.js.map +1 -0
- package/dist/egress/index.d.ts +4 -0
- package/dist/egress/index.d.ts.map +1 -0
- package/dist/egress/index.js +3 -0
- package/dist/egress/index.js.map +1 -0
- package/dist/egress/proxy.d.ts +90 -0
- package/dist/egress/proxy.d.ts.map +1 -0
- package/dist/egress/proxy.js +194 -0
- package/dist/egress/proxy.js.map +1 -0
- package/dist/index.d.ts +101 -190
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +62 -80
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +18 -39
- package/dist/index.test.js.map +1 -1
- package/package.json +5 -4
- package/src/backends/aci-standby-pool/__tests__/unenforceable-controls.test.ts +69 -0
- package/src/backends/aci-standby-pool/index.ts +42 -1
- package/src/backends/docker/__tests__/hardening.test.ts +43 -0
- package/src/backends/docker/__tests__/leaf-permissions.smoke.test.ts +1 -1
- package/src/backends/docker/index.ts +204 -12
- package/src/backends/firecracker/__tests__/agent-timeout-clamp.test.ts +48 -0
- package/src/backends/firecracker/__tests__/backend.test.ts +76 -65
- package/src/backends/firecracker/__tests__/control-plane-mtls.test.ts +10 -2
- package/src/backends/firecracker/__tests__/egress-policy.test.ts +91 -0
- package/src/backends/firecracker/__tests__/fixtures/ipc-path.ts +31 -0
- package/src/backends/firecracker/__tests__/protocol.test.ts +8 -23
- package/src/backends/firecracker/__tests__/transport.test.ts +11 -3
- package/src/backends/firecracker/index.ts +66 -13
- package/src/egress/__tests__/allowlist.test.ts +103 -0
- package/src/egress/__tests__/proxy.test.ts +212 -0
- package/src/egress/allowlist.ts +82 -0
- package/src/egress/index.ts +7 -0
- package/src/egress/proxy.ts +294 -0
- package/src/index.test.ts +19 -41
- package/src/index.ts +170 -259
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host matching for an egress allowlist.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from the proxy because this is the part that decides whether
|
|
5
|
+
* untrusted code reaches the network, and it has to be readable on its own
|
|
6
|
+
* and testable without opening a socket.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Whether `host` is covered by `allowed`.
|
|
10
|
+
*
|
|
11
|
+
* Two forms, and only two:
|
|
12
|
+
*
|
|
13
|
+
* - `api.example.com` — that exact host.
|
|
14
|
+
* - `.example.com` — that domain and any subdomain of it.
|
|
15
|
+
*
|
|
16
|
+
* Substring matching is deliberately NOT one of them. `host.includes(entry)`
|
|
17
|
+
* is the obvious implementation and it is a hole: an allowlist entry of
|
|
18
|
+
* `example.com` would admit `example.com.attacker.net`, which is a domain
|
|
19
|
+
* the attacker owns. Suffix matching has the same hole without the leading
|
|
20
|
+
* dot — `notexample.com` ends with `example.com` — which is why the
|
|
21
|
+
* wildcard form requires it.
|
|
22
|
+
*
|
|
23
|
+
* Comparison is case-insensitive and ignores a trailing dot, because DNS
|
|
24
|
+
* treats `Example.COM.` and `example.com` as the same name and an allowlist
|
|
25
|
+
* that does not would be bypassable by typing the host differently.
|
|
26
|
+
*/
|
|
27
|
+
export function isHostAllowed(host, allowed) {
|
|
28
|
+
const target = normalizeHost(host);
|
|
29
|
+
if (target.length === 0)
|
|
30
|
+
return false;
|
|
31
|
+
for (const raw of allowed) {
|
|
32
|
+
const entry = normalizeHost(raw);
|
|
33
|
+
if (entry.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
if (entry.startsWith('.')) {
|
|
36
|
+
// `.example.com` covers `example.com` itself and any subdomain.
|
|
37
|
+
// Covering the apex matters: an allowlist author writing the
|
|
38
|
+
// wildcard form means the site, and a policy that admits
|
|
39
|
+
// `www.example.com` but not `example.com` reads as a bug.
|
|
40
|
+
const domain = entry.slice(1);
|
|
41
|
+
if (target === domain || target.endsWith(entry))
|
|
42
|
+
return true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (target === entry)
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/** Lowercase, trailing-dot-free, whitespace-free. */
|
|
51
|
+
function normalizeHost(host) {
|
|
52
|
+
return host.trim().toLowerCase().replace(/\.$/, '');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Split an authority into host and port.
|
|
56
|
+
*
|
|
57
|
+
* A CONNECT target and a `Host` header both carry `host:port`, and the
|
|
58
|
+
* allowlist is about the HOST — matching the whole authority would make an
|
|
59
|
+
* entry admit one port and silently refuse the same site on another.
|
|
60
|
+
* IPv6 literals are bracketed, which is why this is not a `split(':')`.
|
|
61
|
+
*/
|
|
62
|
+
export function splitAuthority(authority) {
|
|
63
|
+
const value = authority.trim();
|
|
64
|
+
if (value.startsWith('[')) {
|
|
65
|
+
const close = value.indexOf(']');
|
|
66
|
+
if (close < 0)
|
|
67
|
+
return { host: value };
|
|
68
|
+
const host = value.slice(1, close);
|
|
69
|
+
const rest = value.slice(close + 1);
|
|
70
|
+
const port = rest.startsWith(':') ? Number(rest.slice(1)) : undefined;
|
|
71
|
+
return port !== undefined && Number.isInteger(port) ? { host, port } : { host };
|
|
72
|
+
}
|
|
73
|
+
const colon = value.lastIndexOf(':');
|
|
74
|
+
if (colon < 0)
|
|
75
|
+
return { host: value };
|
|
76
|
+
const port = Number(value.slice(colon + 1));
|
|
77
|
+
if (!Number.isInteger(port))
|
|
78
|
+
return { host: value };
|
|
79
|
+
return { host: value.slice(0, colon), port };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=allowlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowlist.js","sourceRoot":"","sources":["../../src/egress/allowlist.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,OAA0B;IACrE,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;IAClC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAErC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAQ;QAEhC,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,gEAAgE;YAChE,6DAA6D;YAC7D,yDAAyD;YACzD,0DAA0D;YAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YAC7B,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAA;YAC5D,SAAQ;QACT,CAAC;QAED,IAAI,MAAM,KAAK,KAAK;YAAE,OAAO,IAAI,CAAA;IAClC,CAAC;IAED,OAAO,KAAK,CAAA;AACb,CAAC;AAED,qDAAqD;AACrD,SAAS,aAAa,CAAC,IAAY;IAClC,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC/C,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAA;IAE9B,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;QACrC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;QAClC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACrE,OAAO,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAA;IAChF,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;IACrC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;IAC3C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;IACnD,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE,CAAA;AAC7C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/egress/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,YAAY,EACX,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GAClB,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/egress/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The network boundary a sandbox's egress policy is enforced at.
|
|
3
|
+
*
|
|
4
|
+
* Until this existed, an egress policy could be *declared* and only two of
|
|
5
|
+
* its four shapes could be honoured anywhere: the container backend refuses
|
|
6
|
+
* a host allowlist outright because it has no proxy to filter through, and
|
|
7
|
+
* only the microVM backend forwards one. `deny-all` and `allow-all` are the
|
|
8
|
+
* whole spectrum a container-tier sandbox could express — all or nothing.
|
|
9
|
+
*
|
|
10
|
+
* It also settles where credentials live. Any token the agent needs in
|
|
11
|
+
* order to reach an allowed host used to have to be inside the sandbox, in
|
|
12
|
+
* the environment, readable by the untrusted code it is meant to be
|
|
13
|
+
* isolated from — via `/proc/self/environ`, or via a prompt injection that
|
|
14
|
+
* exfiltrates it over the very egress the policy permits. Here the token
|
|
15
|
+
* never enters the sandbox: a placeholder does, and the real value is
|
|
16
|
+
* stamped on at this boundary.
|
|
17
|
+
*/
|
|
18
|
+
/** A credential the proxy stamps on, and the host it may be sent to. */
|
|
19
|
+
export interface BrokeredCredential {
|
|
20
|
+
/**
|
|
21
|
+
* Host this credential is for. Same matching rules as the allowlist,
|
|
22
|
+
* so `.example.com` covers subdomains.
|
|
23
|
+
*
|
|
24
|
+
* Scoped per host on purpose: a credential attached to every request
|
|
25
|
+
* is a credential handed to whichever host the agent was talked into
|
|
26
|
+
* contacting.
|
|
27
|
+
*/
|
|
28
|
+
readonly host: string;
|
|
29
|
+
/** Header to set, e.g. `authorization`. */
|
|
30
|
+
readonly header: string;
|
|
31
|
+
/** The real value. Never leaves this process. */
|
|
32
|
+
readonly value: string;
|
|
33
|
+
}
|
|
34
|
+
export interface EgressProxyOptions {
|
|
35
|
+
/** Resolved at request time, so a rotating allowlist is honoured. */
|
|
36
|
+
readonly allowedHosts: () => Promise<readonly string[]>;
|
|
37
|
+
readonly credentials?: readonly BrokeredCredential[];
|
|
38
|
+
/**
|
|
39
|
+
* Rewrite a plain-HTTP request to HTTPS upstream.
|
|
40
|
+
*
|
|
41
|
+
* This is what makes brokering work at all. A credential cannot be
|
|
42
|
+
* injected into a CONNECT tunnel — the bytes are already encrypted by
|
|
43
|
+
* the time they reach the proxy, and reading them would mean
|
|
44
|
+
* terminating TLS with a CA the sandbox trusts, which is a far larger
|
|
45
|
+
* and far more dangerous thing to build. So the sandbox speaks plain
|
|
46
|
+
* HTTP to the proxy, and the proxy speaks HTTPS to the world.
|
|
47
|
+
*
|
|
48
|
+
* Default `true`. Turn it off only for a genuinely plaintext upstream.
|
|
49
|
+
*/
|
|
50
|
+
readonly upgradeToHttps?: boolean;
|
|
51
|
+
readonly onDenied?: (host: string, reason: string) => void;
|
|
52
|
+
}
|
|
53
|
+
export interface RunningEgressProxy {
|
|
54
|
+
readonly port: number;
|
|
55
|
+
/** `http://127.0.0.1:<port>` — what a sandbox sets `HTTP_PROXY` to. */
|
|
56
|
+
readonly url: string;
|
|
57
|
+
/** Swap the allowlist on a live proxy. See `setNetworkPolicy`. */
|
|
58
|
+
setAllowedHosts(resolve: () => Promise<readonly string[]>): void;
|
|
59
|
+
close(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export declare class EgressProxy {
|
|
62
|
+
private resolveAllowed;
|
|
63
|
+
private readonly credentials;
|
|
64
|
+
private readonly upgradeToHttps;
|
|
65
|
+
private readonly onDenied;
|
|
66
|
+
/** See the loop guard in `listen`. */
|
|
67
|
+
private selfPort;
|
|
68
|
+
constructor(options: EgressProxyOptions);
|
|
69
|
+
listen(port?: number): Promise<RunningEgressProxy>;
|
|
70
|
+
private allowed;
|
|
71
|
+
private deny;
|
|
72
|
+
/** Plain HTTP. The only path where a credential can be stamped on. */
|
|
73
|
+
private handleRequest;
|
|
74
|
+
/**
|
|
75
|
+
* HTTPS, tunnelled.
|
|
76
|
+
*
|
|
77
|
+
* The allowlist is enforceable here because the CONNECT target names
|
|
78
|
+
* the host in clear text. Credential brokering is NOT: the tunnel is
|
|
79
|
+
* opaque, and injecting into it would mean terminating TLS with a CA
|
|
80
|
+
* the sandbox trusts — which would let this process read every byte the
|
|
81
|
+
* agent sends anywhere, a strictly larger risk than the one being
|
|
82
|
+
* mitigated. A workload that needs brokering speaks plain HTTP to the
|
|
83
|
+
* proxy and lets it upgrade upstream.
|
|
84
|
+
*/
|
|
85
|
+
private handleConnect;
|
|
86
|
+
/** Whether a target names this proxy. See the loop guard in `listen`. */
|
|
87
|
+
private isSelf;
|
|
88
|
+
private credentialFor;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/egress/proxy.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;GAgBG;AAEH,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IAClC;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,kBAAkB;IAClC,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,MAAM,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;IACvD,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACpD;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAA;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CAC1D;AAED,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,eAAe,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,GAAG,IAAI,CAAA;IAChE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACtB;AAID,qBAAa,WAAW;IACvB,OAAO,CAAC,cAAc,CAAkC;IACxD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsD;IAC/E,sCAAsC;IACtC,OAAO,CAAC,QAAQ,CAAoB;gBAExB,OAAO,EAAE,kBAAkB;IAOjC,MAAM,CAAC,IAAI,SAAI,GAAG,OAAO,CAAC,kBAAkB,CAAC;YAyCrC,OAAO;IAWrB,OAAO,CAAC,IAAI;IAIZ,sEAAsE;YACxD,aAAa;IA8D3B;;;;;;;;;;OAUG;YACW,aAAa;IA2B3B,yEAAyE;IACzE,OAAO,CAAC,MAAM;IAKd,OAAO,CAAC,aAAa;CAGrB"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createServer, request as httpRequest } from 'node:http';
|
|
2
|
+
import { request as httpsRequest } from 'node:https';
|
|
3
|
+
import { connect as netConnect } from 'node:net';
|
|
4
|
+
import { isHostAllowed, splitAuthority } from './allowlist.js';
|
|
5
|
+
const DENIED_STATUS = 403;
|
|
6
|
+
export class EgressProxy {
|
|
7
|
+
resolveAllowed;
|
|
8
|
+
credentials;
|
|
9
|
+
upgradeToHttps;
|
|
10
|
+
onDenied;
|
|
11
|
+
/** See the loop guard in `listen`. */
|
|
12
|
+
selfPort;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.resolveAllowed = options.allowedHosts;
|
|
15
|
+
this.credentials = options.credentials ?? [];
|
|
16
|
+
this.upgradeToHttps = options.upgradeToHttps ?? true;
|
|
17
|
+
this.onDenied = options.onDenied;
|
|
18
|
+
}
|
|
19
|
+
async listen(port = 0) {
|
|
20
|
+
const server = createServer((req, res) => {
|
|
21
|
+
void this.handleRequest(req, res);
|
|
22
|
+
});
|
|
23
|
+
server.on('connect', (req, socket, head) => {
|
|
24
|
+
void this.handleConnect(req, socket, head);
|
|
25
|
+
});
|
|
26
|
+
await new Promise((resolve, reject) => {
|
|
27
|
+
server.once('error', reject);
|
|
28
|
+
// Loopback only. A proxy that holds real credentials and binds
|
|
29
|
+
// every interface is reachable by anything on the network, which
|
|
30
|
+
// is the opposite of what it exists for.
|
|
31
|
+
server.listen(port, '127.0.0.1', () => {
|
|
32
|
+
server.off('error', reject);
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
const address = server.address();
|
|
37
|
+
const boundPort = typeof address === 'object' && address !== null ? address.port : port;
|
|
38
|
+
// Refuse to forward to ourselves. A client that sends its `Host` as
|
|
39
|
+
// the proxy's own address — which is what a library that rewrites
|
|
40
|
+
// `Host` on a proxied request produces — would otherwise make the
|
|
41
|
+
// proxy call itself forever, holding a socket per hop until the
|
|
42
|
+
// process runs out. Found by a test that hung rather than failed,
|
|
43
|
+
// which is the shape this failure takes in production too.
|
|
44
|
+
this.selfPort = boundPort;
|
|
45
|
+
return {
|
|
46
|
+
port: boundPort,
|
|
47
|
+
url: `http://127.0.0.1:${boundPort}`,
|
|
48
|
+
setAllowedHosts: (resolve) => {
|
|
49
|
+
this.resolveAllowed = resolve;
|
|
50
|
+
},
|
|
51
|
+
close: () => new Promise((resolve) => {
|
|
52
|
+
server.close(() => resolve());
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async allowed(host) {
|
|
57
|
+
try {
|
|
58
|
+
return isHostAllowed(host, await this.resolveAllowed());
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// A resolver that throws is a policy that could not be read.
|
|
62
|
+
// Denying is the only safe reading: an allowlist that fails open
|
|
63
|
+
// is not an allowlist.
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
deny(host, reason) {
|
|
68
|
+
this.onDenied?.(host, reason);
|
|
69
|
+
}
|
|
70
|
+
/** Plain HTTP. The only path where a credential can be stamped on. */
|
|
71
|
+
async handleRequest(req, res) {
|
|
72
|
+
const target = parseTarget(req);
|
|
73
|
+
if (!target) {
|
|
74
|
+
res.writeHead(400, { 'content-type': 'text/plain' });
|
|
75
|
+
res.end('Malformed proxy request: no absolute URL and no Host header.\n');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (this.isSelf(target.host, target.port)) {
|
|
79
|
+
res.writeHead(400, { 'content-type': 'text/plain' });
|
|
80
|
+
res.end('Refusing to proxy a request addressed to the proxy itself; forwarding it would loop until the process ran out of sockets. Send an absolute URL in the request line, which is what a proxied client does.\n');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!(await this.allowed(target.host))) {
|
|
84
|
+
this.deny(target.host, 'not on the egress allowlist');
|
|
85
|
+
res.writeHead(DENIED_STATUS, { 'content-type': 'text/plain' });
|
|
86
|
+
// Named, not a generic refusal: an agent that cannot tell "this
|
|
87
|
+
// host is not permitted" from "the network is down" will retry
|
|
88
|
+
// forever, and a human debugging it has nothing to go on.
|
|
89
|
+
res.end(`Egress denied: ${target.host} is not on this sandbox's allowlist.\n`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const headers = { ...req.headers };
|
|
93
|
+
// The proxy re-issues the request, so hop-by-hop headers about the
|
|
94
|
+
// hop that just ended must not be forwarded.
|
|
95
|
+
for (const hop of ['proxy-authorization', 'proxy-connection', 'connection', 'keep-alive']) {
|
|
96
|
+
delete headers[hop];
|
|
97
|
+
}
|
|
98
|
+
const credential = this.credentialFor(target.host);
|
|
99
|
+
if (credential) {
|
|
100
|
+
headers[credential.header.toLowerCase()] = credential.value;
|
|
101
|
+
}
|
|
102
|
+
const secure = this.upgradeToHttps || target.protocol === 'https:';
|
|
103
|
+
const send = secure ? httpsRequest : httpRequest;
|
|
104
|
+
const upstream = send({
|
|
105
|
+
protocol: secure ? 'https:' : 'http:',
|
|
106
|
+
host: target.host,
|
|
107
|
+
port: target.port ?? (secure ? 443 : 80),
|
|
108
|
+
method: req.method,
|
|
109
|
+
path: target.path,
|
|
110
|
+
headers,
|
|
111
|
+
}, (response) => {
|
|
112
|
+
res.writeHead(response.statusCode ?? 502, response.headers);
|
|
113
|
+
response.pipe(res);
|
|
114
|
+
});
|
|
115
|
+
upstream.on('error', (err) => {
|
|
116
|
+
if (!res.headersSent)
|
|
117
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
118
|
+
res.end(`Upstream request failed: ${err.message}\n`);
|
|
119
|
+
});
|
|
120
|
+
req.pipe(upstream);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* HTTPS, tunnelled.
|
|
124
|
+
*
|
|
125
|
+
* The allowlist is enforceable here because the CONNECT target names
|
|
126
|
+
* the host in clear text. Credential brokering is NOT: the tunnel is
|
|
127
|
+
* opaque, and injecting into it would mean terminating TLS with a CA
|
|
128
|
+
* the sandbox trusts — which would let this process read every byte the
|
|
129
|
+
* agent sends anywhere, a strictly larger risk than the one being
|
|
130
|
+
* mitigated. A workload that needs brokering speaks plain HTTP to the
|
|
131
|
+
* proxy and lets it upgrade upstream.
|
|
132
|
+
*/
|
|
133
|
+
async handleConnect(req, socket, head) {
|
|
134
|
+
const { host, port } = splitAuthority(req.url ?? '');
|
|
135
|
+
if (!(await this.allowed(host))) {
|
|
136
|
+
this.deny(host, 'not on the egress allowlist');
|
|
137
|
+
socket.write(`HTTP/1.1 ${DENIED_STATUS} Forbidden\r\nContent-Type: text/plain\r\n\r\nEgress denied: ${host} is not on this sandbox's allowlist.\n`);
|
|
138
|
+
socket.end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const upstream = netConnect(port ?? 443, host, () => {
|
|
142
|
+
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
143
|
+
if (head.length > 0)
|
|
144
|
+
upstream.write(head);
|
|
145
|
+
upstream.pipe(socket);
|
|
146
|
+
socket.pipe(upstream);
|
|
147
|
+
});
|
|
148
|
+
upstream.on('error', () => {
|
|
149
|
+
socket.end();
|
|
150
|
+
});
|
|
151
|
+
socket.on('error', () => {
|
|
152
|
+
upstream.destroy();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Whether a target names this proxy. See the loop guard in `listen`. */
|
|
156
|
+
isSelf(host, port) {
|
|
157
|
+
if (port === undefined || port !== this.selfPort)
|
|
158
|
+
return false;
|
|
159
|
+
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
160
|
+
}
|
|
161
|
+
credentialFor(host) {
|
|
162
|
+
return this.credentials.find((c) => isHostAllowed(host, [c.host]));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Read the target from a proxied request.
|
|
167
|
+
*
|
|
168
|
+
* A proxy receives an absolute URL in the request line; a client that
|
|
169
|
+
* forgot it still sends `Host`, and honouring that is what makes a plain
|
|
170
|
+
* `fetch` through `HTTP_PROXY` work.
|
|
171
|
+
*/
|
|
172
|
+
function parseTarget(req) {
|
|
173
|
+
const raw = req.url ?? '';
|
|
174
|
+
if (raw.startsWith('http://') || raw.startsWith('https://')) {
|
|
175
|
+
const url = new URL(raw);
|
|
176
|
+
return {
|
|
177
|
+
host: url.hostname,
|
|
178
|
+
...(url.port ? { port: Number(url.port) } : {}),
|
|
179
|
+
path: `${url.pathname}${url.search}`,
|
|
180
|
+
protocol: url.protocol,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const hostHeader = req.headers.host;
|
|
184
|
+
if (!hostHeader)
|
|
185
|
+
return null;
|
|
186
|
+
const { host, port } = splitAuthority(hostHeader);
|
|
187
|
+
return {
|
|
188
|
+
host,
|
|
189
|
+
...(port !== undefined ? { port } : {}),
|
|
190
|
+
path: raw || '/',
|
|
191
|
+
protocol: 'http:',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/egress/proxy.ts"],"names":[],"mappings":"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"}
|