@stratusagent/egress 0.6.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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/addresses.d.ts +63 -0
- package/dist/addresses.d.ts.map +1 -0
- package/dist/addresses.js +246 -0
- package/dist/addresses.js.map +1 -0
- package/dist/connect.d.ts +71 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +309 -0
- package/dist/connect.js.map +1 -0
- package/dist/fixtures.d.ts +17 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +26 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stratus Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @stratusagent/egress
|
|
2
|
+
|
|
3
|
+
Which addresses a tool may reach, enforced on the connection rather than on
|
|
4
|
+
the name it was asked for.
|
|
5
|
+
|
|
6
|
+
Used by [`@stratusagent/tool-web`](../tool-web) and
|
|
7
|
+
[`@stratusagent/tool-browser`](../tool-browser). It is one module because two
|
|
8
|
+
copies of this rule would not drift into a style difference — the stale one
|
|
9
|
+
would be an SSRF hole — and both packs are tested against `HOSTILE_URLS`, the
|
|
10
|
+
table exported from here, which is what fails if it is ever forked.
|
|
11
|
+
|
|
12
|
+
## The rule
|
|
13
|
+
|
|
14
|
+
**Every globally routable address, and nothing else**, in both families. A
|
|
15
|
+
blacklist that stops at RFC 1918 still leaves:
|
|
16
|
+
|
|
17
|
+
- `169.254.169.254` — cloud instance metadata, and the credentials it serves
|
|
18
|
+
- `127.0.0.1` and `::1` — the daemon's own control API, among other things
|
|
19
|
+
- `fc00::/7` and `fe80::/10` — the same internal services over IPv6
|
|
20
|
+
- `::ffff:169.254.169.254`, `64:ff9b::a9fe:a9fe`, `2002:a9fe:a9fe::` — the
|
|
21
|
+
same IPv4 addresses again, written the ways an IPv4 check does not see.
|
|
22
|
+
The well-known NAT64 prefix is judged on the IPv4 it carries, so an
|
|
23
|
+
IPv6-only network still reaches the public internet through it; RFC 8215's
|
|
24
|
+
**local-use** prefix `64:ff9b:1::/48` is refused whole, because the
|
|
25
|
+
translator chooses the embedding length and there is no offset to read the
|
|
26
|
+
address from
|
|
27
|
+
|
|
28
|
+
Schemes are `http:` and `https:`. `file:`, `data:`, and `javascript:` are
|
|
29
|
+
refused before anything is fetched: the process doing the fetching runs as the
|
|
30
|
+
daemon user, and address validation cannot protect a filesystem.
|
|
31
|
+
|
|
32
|
+
## Validation binds to the connection
|
|
33
|
+
|
|
34
|
+
Resolving a name to check it and then letting something else resolve it again
|
|
35
|
+
to connect is DNS rebinding with extra steps — the attacker's server answers
|
|
36
|
+
`93.184.216.34` for the check and `169.254.169.254` for the connection.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { createPinnedLookup, requestThroughPolicy, createEgressProxy } from '@stratusagent/egress';
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `createPinnedLookup(policy)` is a `lookup` for Node's HTTP stack, so the
|
|
43
|
+
addresses the socket uses are the addresses this checked. There is no second
|
|
44
|
+
resolution to lose to.
|
|
45
|
+
- `requestThroughPolicy(url, …)` is one exchange over that lookup, with a byte
|
|
46
|
+
cap. It deliberately does **not** follow redirects: each hop is a new request
|
|
47
|
+
to a new host, so the caller loops and each hop faces the policy again.
|
|
48
|
+
Both of those pass `agent: false` alongside the lookup. Node's global agent
|
|
49
|
+
pools sockets by host and port and knows nothing about `lookup`, so a
|
|
50
|
+
connection opened under a permissive policy is one a stricter policy can be
|
|
51
|
+
handed — and a reused socket resolves nothing, so the pinned lookup that
|
|
52
|
+
would have refused it is never called.
|
|
53
|
+
|
|
54
|
+
- `createEgressProxy(policy)` is a loopback proxy for a browser, which resolves
|
|
55
|
+
names for itself whatever an interception handler decides. It resolves,
|
|
56
|
+
checks, and dials the checked address. `chromiumProxyOptions` /
|
|
57
|
+
`chromiumProxyArgs` include `<-loopback>` in the bypass list, without which
|
|
58
|
+
Chromium bypasses the proxy for exactly the addresses it exists to refuse.
|
|
59
|
+
|
|
60
|
+
## Widening it
|
|
61
|
+
|
|
62
|
+
| Setting | What it does |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `allowedHosts` | Exempts specific hosts, by name or literal address. The narrow override: one internal service an agent is meant to reach. |
|
|
65
|
+
| `allowPrivateAddresses` | Reaches any non-global address. The trusted-workstation posture — it turns the protection off rather than adjusting it. |
|
|
66
|
+
| `allowedSchemes` | Replaces the scheme allowlist. There is no good reason to add `file:`. |
|
|
67
|
+
|
|
68
|
+
Neither is a substitute for network-level egress rules in a VM or hosted
|
|
69
|
+
profile ([08](../../docs/roadmap/08-deployment-profiles.md)); this is the
|
|
70
|
+
in-process half.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which addresses a tool may reach.
|
|
3
|
+
*
|
|
4
|
+
* The rule is "every globally routable address, and nothing else" rather
|
|
5
|
+
* than a blacklist of the ranges people remember. A blacklist that stops at
|
|
6
|
+
* RFC 1918 leaves cloud metadata at `169.254.169.254`, the gateway on
|
|
7
|
+
* `127.0.0.1`, and — on any dual-stack network — the same services again
|
|
8
|
+
* over IPv6 unique-local addresses and IPv4-mapped forms. Enumerating what
|
|
9
|
+
* is *allowed* fails closed on the range nobody thought of.
|
|
10
|
+
*/
|
|
11
|
+
export interface AddressVerdict {
|
|
12
|
+
allowed: boolean;
|
|
13
|
+
/** One line naming what was refused, for the error the agent sees. */
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface EgressPolicy {
|
|
17
|
+
/**
|
|
18
|
+
* Schemes a tool may request. `http:`/`https:` by default — `file:`,
|
|
19
|
+
* `data:`, and the rest are rejected before anything is fetched, because
|
|
20
|
+
* a browser or fetcher runs as the daemon user and address validation
|
|
21
|
+
* cannot protect the filesystem.
|
|
22
|
+
*/
|
|
23
|
+
allowedSchemes?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Reach addresses that are not globally routable. Off by default; on, it
|
|
26
|
+
* is the trusted-workstation posture — an agent that is meant to talk to
|
|
27
|
+
* a local development server. It turns off the SSRF protection, which is
|
|
28
|
+
* why it is one switch an operator flips rather than something a call can
|
|
29
|
+
* ask for.
|
|
30
|
+
*/
|
|
31
|
+
allowPrivateAddresses?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Hosts exempt from the address check, by hostname or literal address —
|
|
34
|
+
* `localhost`, `127.0.0.1`, `dev.internal`. Narrower than the switch
|
|
35
|
+
* above and the one to reach for: an agent that needs one internal
|
|
36
|
+
* service should not thereby be able to read the metadata endpoint.
|
|
37
|
+
*/
|
|
38
|
+
allowedHosts?: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Classify one literal address, in either family.
|
|
42
|
+
*
|
|
43
|
+
* Called on the address a connection is actually about to use, never on
|
|
44
|
+
* the one a name resolved to a moment earlier somewhere else — that gap is
|
|
45
|
+
* DNS rebinding, and it is the difference between a check and a ceremony.
|
|
46
|
+
*/
|
|
47
|
+
export declare const classifyAddress: (address: string) => AddressVerdict;
|
|
48
|
+
/** Whether one resolved address may be connected to for this host. */
|
|
49
|
+
export declare const checkAddress: (policy: EgressPolicy, host: string, address: string) => AddressVerdict;
|
|
50
|
+
/** A URL this policy refuses, and why. */
|
|
51
|
+
export declare class EgressPolicyError extends Error {
|
|
52
|
+
constructor(message: string);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The check that can be made before anything is fetched: the scheme, and
|
|
56
|
+
* the address when the URL names one literally.
|
|
57
|
+
*
|
|
58
|
+
* A hostname cannot be judged here — that is what the connection-time
|
|
59
|
+
* check is for — so this returning without throwing is not permission to
|
|
60
|
+
* connect. It is permission to *try*, and it is where `file:///…` dies.
|
|
61
|
+
*/
|
|
62
|
+
export declare const assertRequestAllowed: (rawUrl: string, policy?: EgressPolicy) => URL;
|
|
63
|
+
//# sourceMappingURL=addresses.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addresses.d.ts","sourceRoot":"","sources":["../src/addresses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAiKD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,cAUjD,CAAC;AAOF,sEAAsE;AACtE,eAAO,MAAM,YAAY,GAAI,QAAQ,YAAY,EAAE,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,cAKlF,CAAC;AAEF,0CAA0C;AAC1C,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,EAAE,SAAQ,YAAiB,KAAG,GAyBhF,CAAC"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which addresses a tool may reach.
|
|
3
|
+
*
|
|
4
|
+
* The rule is "every globally routable address, and nothing else" rather
|
|
5
|
+
* than a blacklist of the ranges people remember. A blacklist that stops at
|
|
6
|
+
* RFC 1918 leaves cloud metadata at `169.254.169.254`, the gateway on
|
|
7
|
+
* `127.0.0.1`, and — on any dual-stack network — the same services again
|
|
8
|
+
* over IPv6 unique-local addresses and IPv4-mapped forms. Enumerating what
|
|
9
|
+
* is *allowed* fails closed on the range nobody thought of.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_SCHEMES = ['http:', 'https:'];
|
|
12
|
+
const parseIPv4 = (value) => {
|
|
13
|
+
const parts = value.split('.');
|
|
14
|
+
if (parts.length !== 4) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const octets = [];
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (!/^\d{1,3}$/.test(part)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const octet = Number(part);
|
|
23
|
+
if (octet > 255) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
octets.push(octet);
|
|
27
|
+
}
|
|
28
|
+
return octets;
|
|
29
|
+
};
|
|
30
|
+
/** Every IPv4 range that is not globally routable, with the name of each. */
|
|
31
|
+
const classifyIPv4 = (octets) => {
|
|
32
|
+
const [a = 0, b = 0, c = 0, d = 0] = octets;
|
|
33
|
+
const address = octets.join('.');
|
|
34
|
+
const refuse = (what) => ({
|
|
35
|
+
allowed: false,
|
|
36
|
+
reason: `${address} is ${what}, which is not a public address`,
|
|
37
|
+
});
|
|
38
|
+
if (a === 0)
|
|
39
|
+
return refuse('the unspecified range (0.0.0.0/8)');
|
|
40
|
+
if (a === 10)
|
|
41
|
+
return refuse('a private address (10.0.0.0/8)');
|
|
42
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
43
|
+
return refuse('a carrier-NAT address (100.64.0.0/10)');
|
|
44
|
+
if (a === 127)
|
|
45
|
+
return refuse('loopback (127.0.0.0/8)');
|
|
46
|
+
if (a === 169 && b === 254) {
|
|
47
|
+
// The one that matters most in a hosted deployment: this is where every
|
|
48
|
+
// major cloud serves instance credentials.
|
|
49
|
+
return refuse('link-local (169.254.0.0/16) — cloud instance metadata lives here');
|
|
50
|
+
}
|
|
51
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
52
|
+
return refuse('a private address (172.16.0.0/12)');
|
|
53
|
+
if (a === 192 && b === 0 && c === 0)
|
|
54
|
+
return refuse('IETF protocol assignment space (192.0.0.0/24)');
|
|
55
|
+
if (a === 192 && b === 0 && c === 2)
|
|
56
|
+
return refuse('documentation space (192.0.2.0/24)');
|
|
57
|
+
if (a === 192 && b === 88 && c === 99)
|
|
58
|
+
return refuse('6to4 relay anycast (192.88.99.0/24)');
|
|
59
|
+
if (a === 192 && b === 168)
|
|
60
|
+
return refuse('a private address (192.168.0.0/16)');
|
|
61
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
62
|
+
return refuse('benchmarking space (198.18.0.0/15)');
|
|
63
|
+
if (a === 198 && b === 51 && c === 100)
|
|
64
|
+
return refuse('documentation space (198.51.100.0/24)');
|
|
65
|
+
if (a === 203 && b === 0 && c === 113)
|
|
66
|
+
return refuse('documentation space (203.0.113.0/24)');
|
|
67
|
+
if (a >= 224 && a <= 239)
|
|
68
|
+
return refuse('multicast (224.0.0.0/4)');
|
|
69
|
+
if (a >= 240)
|
|
70
|
+
return refuse(d === 255 && a === 255 ? 'broadcast' : 'reserved space (240.0.0.0/4)');
|
|
71
|
+
return { allowed: true };
|
|
72
|
+
};
|
|
73
|
+
const expandIPv6 = (value) => {
|
|
74
|
+
const zoneless = value.split('%')[0] ?? value;
|
|
75
|
+
const [head, tail, ...extra] = zoneless.split('::');
|
|
76
|
+
if (extra.length > 0) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const parseGroups = (part) => {
|
|
80
|
+
if (!part) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const groups = [];
|
|
84
|
+
for (const piece of part.split(':')) {
|
|
85
|
+
if (piece.length === 0) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (piece.includes('.')) {
|
|
89
|
+
// A trailing dotted quad: `::ffff:169.254.169.254` and friends.
|
|
90
|
+
const octets = parseIPv4(piece);
|
|
91
|
+
if (!octets) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
groups.push(((octets[0] ?? 0) << 8) | (octets[1] ?? 0), ((octets[2] ?? 0) << 8) | (octets[3] ?? 0));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(piece)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
groups.push(Number.parseInt(piece, 16));
|
|
101
|
+
}
|
|
102
|
+
return groups;
|
|
103
|
+
};
|
|
104
|
+
const left = parseGroups(head);
|
|
105
|
+
const right = tail === undefined ? [] : parseGroups(tail);
|
|
106
|
+
if (!left || !right) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
if (tail === undefined) {
|
|
110
|
+
return left.length === 8 ? left : undefined;
|
|
111
|
+
}
|
|
112
|
+
const missing = 8 - left.length - right.length;
|
|
113
|
+
if (missing < 0) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return [...left, ...Array.from({ length: missing }, () => 0), ...right];
|
|
117
|
+
};
|
|
118
|
+
const embeddedIPv4 = (groups) => {
|
|
119
|
+
const [g0 = 0, g1 = 0, g2 = 0, g3 = 0, g4 = 0, g5 = 0, g6 = 0, g7 = 0] = groups;
|
|
120
|
+
const asOctets = (high, low) => [high >> 8, high & 0xff, low >> 8, low & 0xff];
|
|
121
|
+
// IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible (::a.b.c.d). Both are
|
|
122
|
+
// ways of writing an IPv4 address, so both are classified as one — a
|
|
123
|
+
// check that only knew dotted quads would pass `::ffff:127.0.0.1`.
|
|
124
|
+
if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && (g5 === 0xffff || g5 === 0)) {
|
|
125
|
+
if (g6 === 0 && g7 <= 1) {
|
|
126
|
+
return undefined; // :: and ::1 are classified as IPv6 below.
|
|
127
|
+
}
|
|
128
|
+
return asOctets(g6, g7);
|
|
129
|
+
}
|
|
130
|
+
// NAT64 (64:ff9b::/96) and 6to4 (2002::/16) both carry an IPv4 address
|
|
131
|
+
// that the network will actually route to.
|
|
132
|
+
if (g0 === 0x0064 && g1 === 0xff9b && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0) {
|
|
133
|
+
return asOctets(g6, g7);
|
|
134
|
+
}
|
|
135
|
+
if (g0 === 0x2002) {
|
|
136
|
+
return asOctets(g1, g2);
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
};
|
|
140
|
+
const classifyIPv6 = (groups, address) => {
|
|
141
|
+
const embedded = embeddedIPv4(groups);
|
|
142
|
+
if (embedded) {
|
|
143
|
+
const verdict = classifyIPv4(embedded);
|
|
144
|
+
return verdict.allowed
|
|
145
|
+
? verdict
|
|
146
|
+
: { allowed: false, reason: `${address} embeds ${verdict.reason ?? 'a non-public address'}` };
|
|
147
|
+
}
|
|
148
|
+
const [g0 = 0] = groups;
|
|
149
|
+
const refuse = (what) => ({
|
|
150
|
+
allowed: false,
|
|
151
|
+
reason: `${address} is ${what}, which is not a public address`,
|
|
152
|
+
});
|
|
153
|
+
// RFC 8215's local-use NAT64 prefix. The well-known `64:ff9b::/96` above
|
|
154
|
+
// embeds its IPv4 at a fixed offset, so that one is extracted and judged
|
|
155
|
+
// on the address it carries — which is what lets an IPv6-only network
|
|
156
|
+
// reach the public IPv4 internet. This one may embed at any of RFC 6052's
|
|
157
|
+
// lengths, chosen by whoever runs the translator, so there is no offset
|
|
158
|
+
// to read it from. It is called local-use for a reason: refused whole,
|
|
159
|
+
// rather than guessed at and let through as public.
|
|
160
|
+
if (g0 === 0x0064 && groups[1] === 0xff9b && groups[2] === 0x0001) {
|
|
161
|
+
return refuse('the local-use NAT64 prefix (64:ff9b:1::/48), which can translate to any address');
|
|
162
|
+
}
|
|
163
|
+
if (groups.every((group) => group === 0))
|
|
164
|
+
return refuse('the unspecified address (::)');
|
|
165
|
+
if (groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1)
|
|
166
|
+
return refuse('loopback (::1)');
|
|
167
|
+
if ((g0 & 0xfe00) === 0xfc00)
|
|
168
|
+
return refuse('a unique-local address (fc00::/7)');
|
|
169
|
+
if ((g0 & 0xffc0) === 0xfe80)
|
|
170
|
+
return refuse('link-local (fe80::/10)');
|
|
171
|
+
if ((g0 & 0xff00) === 0xff00)
|
|
172
|
+
return refuse('multicast (ff00::/8)');
|
|
173
|
+
if (g0 === 0x2001 && groups[1] === 0x0db8)
|
|
174
|
+
return refuse('documentation space (2001:db8::/32)');
|
|
175
|
+
if (g0 === 0x0100 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0)
|
|
176
|
+
return refuse('the discard prefix (100::/64)');
|
|
177
|
+
return { allowed: true };
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Classify one literal address, in either family.
|
|
181
|
+
*
|
|
182
|
+
* Called on the address a connection is actually about to use, never on
|
|
183
|
+
* the one a name resolved to a moment earlier somewhere else — that gap is
|
|
184
|
+
* DNS rebinding, and it is the difference between a check and a ceremony.
|
|
185
|
+
*/
|
|
186
|
+
export const classifyAddress = (address) => {
|
|
187
|
+
const ipv4 = parseIPv4(address);
|
|
188
|
+
if (ipv4) {
|
|
189
|
+
return classifyIPv4(ipv4);
|
|
190
|
+
}
|
|
191
|
+
const ipv6 = expandIPv6(address);
|
|
192
|
+
if (ipv6) {
|
|
193
|
+
return classifyIPv6(ipv6, address);
|
|
194
|
+
}
|
|
195
|
+
return { allowed: false, reason: `${address} is not an IP address` };
|
|
196
|
+
};
|
|
197
|
+
const hostMatches = (policy, host) => {
|
|
198
|
+
const normalized = host.toLowerCase().replace(/^\[|\]$/g, '');
|
|
199
|
+
return (policy.allowedHosts ?? []).some((entry) => entry.toLowerCase() === normalized);
|
|
200
|
+
};
|
|
201
|
+
/** Whether one resolved address may be connected to for this host. */
|
|
202
|
+
export const checkAddress = (policy, host, address) => {
|
|
203
|
+
if (policy.allowPrivateAddresses || hostMatches(policy, host)) {
|
|
204
|
+
return { allowed: true };
|
|
205
|
+
}
|
|
206
|
+
return classifyAddress(address);
|
|
207
|
+
};
|
|
208
|
+
/** A URL this policy refuses, and why. */
|
|
209
|
+
export class EgressPolicyError extends Error {
|
|
210
|
+
constructor(message) {
|
|
211
|
+
super(message);
|
|
212
|
+
this.name = 'EgressPolicyError';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The check that can be made before anything is fetched: the scheme, and
|
|
217
|
+
* the address when the URL names one literally.
|
|
218
|
+
*
|
|
219
|
+
* A hostname cannot be judged here — that is what the connection-time
|
|
220
|
+
* check is for — so this returning without throwing is not permission to
|
|
221
|
+
* connect. It is permission to *try*, and it is where `file:///…` dies.
|
|
222
|
+
*/
|
|
223
|
+
export const assertRequestAllowed = (rawUrl, policy = {}) => {
|
|
224
|
+
let url;
|
|
225
|
+
try {
|
|
226
|
+
url = new URL(rawUrl);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
throw new EgressPolicyError(`Not a URL: ${rawUrl}`);
|
|
230
|
+
}
|
|
231
|
+
const schemes = policy.allowedSchemes ?? DEFAULT_SCHEMES;
|
|
232
|
+
if (!schemes.includes(url.protocol)) {
|
|
233
|
+
throw new EgressPolicyError(`Refusing ${url.protocol}//: only ${schemes.join(', ')} may be requested. `
|
|
234
|
+
+ 'Local schemes are rejected before navigation — this process can read the files they name.');
|
|
235
|
+
}
|
|
236
|
+
const host = url.hostname.replace(/^\[|\]$/g, '');
|
|
237
|
+
const literal = parseIPv4(host) ? host : (expandIPv6(host) ? host : undefined);
|
|
238
|
+
if (literal) {
|
|
239
|
+
const verdict = checkAddress(policy, host, literal);
|
|
240
|
+
if (!verdict.allowed) {
|
|
241
|
+
throw new EgressPolicyError(`Refusing ${url.href}: ${verdict.reason}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return url;
|
|
245
|
+
};
|
|
246
|
+
//# sourceMappingURL=addresses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addresses.js","sourceRoot":"","sources":["../src/addresses.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import { type EgressPolicy } from './addresses.ts';
|
|
4
|
+
/**
|
|
5
|
+
* One policy, as a key — sorted, because `{a,b}` and `{b,a}` are the same
|
|
6
|
+
* policy. Exported because anything that keeps a per-policy resource needs
|
|
7
|
+
* the same answer to "is this the same policy", and two spellings of that
|
|
8
|
+
* question would be two answers.
|
|
9
|
+
*/
|
|
10
|
+
export declare const policyKeyFor: (policy: EgressPolicy) => string;
|
|
11
|
+
/**
|
|
12
|
+
* A DNS lookup that hands the socket only addresses the policy allows.
|
|
13
|
+
*
|
|
14
|
+
* This is the whole point of the module. Resolving a name in one place and
|
|
15
|
+
* connecting in another is a DNS rebinding race: the attacker's server
|
|
16
|
+
* answers `93.184.216.34` for the check and `169.254.169.254` a moment
|
|
17
|
+
* later for the connection, and a checker that looked the name up itself
|
|
18
|
+
* would have validated an address nothing ever connected to. Node lets the
|
|
19
|
+
* connection's *own* resolution be replaced, so the addresses this returns
|
|
20
|
+
* are the addresses the socket uses — there is no second lookup to lose to.
|
|
21
|
+
*/
|
|
22
|
+
export declare const createPinnedLookup: (policy: EgressPolicy) => net.LookupFunction;
|
|
23
|
+
export interface PolicyRequestOptions {
|
|
24
|
+
policy?: EgressPolicy;
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
/** Give up on the whole exchange after this long. */
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
/** Stop reading here. A tool's output is bounded; so is what it downloads. */
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
export interface PolicyResponse {
|
|
33
|
+
status: number;
|
|
34
|
+
headers: http.IncomingHttpHeaders;
|
|
35
|
+
body: string;
|
|
36
|
+
/** True when `maxBytes` cut the body short. */
|
|
37
|
+
truncated: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One HTTP exchange, pinned to a validated address.
|
|
41
|
+
*
|
|
42
|
+
* Redirects are deliberately **not** followed here: each hop is a new
|
|
43
|
+
* request to a new host, so each one has to face the policy again. A
|
|
44
|
+
* caller loops and re-enters this, which is what makes "an approved public
|
|
45
|
+
* URL that redirects into the metadata endpoint" a refusal rather than a
|
|
46
|
+
* fetch.
|
|
47
|
+
*/
|
|
48
|
+
export declare const requestThroughPolicy: (rawUrl: string, options?: PolicyRequestOptions) => Promise<PolicyResponse>;
|
|
49
|
+
export interface EgressProxy {
|
|
50
|
+
/** `http://127.0.0.1:<port>` — what a browser is launched pointing at. */
|
|
51
|
+
url: string;
|
|
52
|
+
port: number;
|
|
53
|
+
/** Requests this proxy refused, newest last. Surfaced to the agent as the reason a page is blank. */
|
|
54
|
+
refusals: string[];
|
|
55
|
+
close(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export declare const createEgressProxy: (policy?: EgressPolicy) => Promise<EgressProxy>;
|
|
58
|
+
/**
|
|
59
|
+
* What a Chromium launch needs to be actually pinned to a proxy.
|
|
60
|
+
*
|
|
61
|
+
* `--proxy-bypass-list=<-loopback>` is not optional: Chromium bypasses the
|
|
62
|
+
* proxy for loopback and `localhost` by default, so a browser pointed at
|
|
63
|
+
* this proxy would still reach `http://127.0.0.1:8080` directly — which is
|
|
64
|
+
* one of the exact destinations the proxy exists to refuse.
|
|
65
|
+
*/
|
|
66
|
+
export declare const chromiumProxyOptions: (proxy: Pick<EgressProxy, "url">) => {
|
|
67
|
+
server: string;
|
|
68
|
+
bypass: string;
|
|
69
|
+
};
|
|
70
|
+
export declare const chromiumProxyArgs: (proxy: Pick<EgressProxy, "url">) => string[];
|
|
71
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,GAAG,MAAM,UAAU,CAAC;AAG3B,OAAO,EAAyD,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE1G;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,YAAY,KAAG,MAIlD,CAAC;AAqBH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,YAAY,KAAG,GAAG,CAAC,cAqC7D,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,UAAS,oBAAyB,KACjC,OAAO,CAAC,cAAc,CAuFxB,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,qGAAqG;IACrG,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAqCD,eAAO,MAAM,iBAAiB,GAAU,SAAQ,YAAiB,KAAG,OAAO,CAAC,WAAW,CA+GtF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,KAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAIrG,CAAC;AAEH,eAAO,MAAM,iBAAiB,GAAI,OAAO,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,KAAG,MAAM,EAGzE,CAAC"}
|
package/dist/connect.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import dns from 'node:dns';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import { assertRequestAllowed, checkAddress, EgressPolicyError } from "./addresses.js";
|
|
6
|
+
/**
|
|
7
|
+
* One policy, as a key — sorted, because `{a,b}` and `{b,a}` are the same
|
|
8
|
+
* policy. Exported because anything that keeps a per-policy resource needs
|
|
9
|
+
* the same answer to "is this the same policy", and two spellings of that
|
|
10
|
+
* question would be two answers.
|
|
11
|
+
*/
|
|
12
|
+
export const policyKeyFor = (policy) => JSON.stringify({
|
|
13
|
+
allowPrivateAddresses: policy.allowPrivateAddresses ?? false,
|
|
14
|
+
allowedHosts: [...(policy.allowedHosts ?? [])].sort(),
|
|
15
|
+
allowedSchemes: [...(policy.allowedSchemes ?? [])].sort(),
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Connection options that keep a policy's sockets to itself.
|
|
19
|
+
*
|
|
20
|
+
* `agent: false` is the load-bearing half. Node's global agent pools
|
|
21
|
+
* sockets by host and port and knows nothing about `lookup`, so a
|
|
22
|
+
* connection opened under a permissive policy is a connection a stricter
|
|
23
|
+
* one can be handed — no DNS resolution happens on a reused socket, and the
|
|
24
|
+
* pinned lookup that would have refused it is never called. That is a
|
|
25
|
+
* policy bypass that gets *more* likely the busier the daemon is, and it
|
|
26
|
+
* cost us a browser test that passed alone and failed in company.
|
|
27
|
+
*
|
|
28
|
+
* The price is no keep-alive for tool traffic, which for a fetch an agent
|
|
29
|
+
* makes a few times a turn is not a price worth a shared pool.
|
|
30
|
+
*/
|
|
31
|
+
const pinnedConnection = (policy) => ({
|
|
32
|
+
agent: false,
|
|
33
|
+
lookup: createPinnedLookup(policy),
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* A DNS lookup that hands the socket only addresses the policy allows.
|
|
37
|
+
*
|
|
38
|
+
* This is the whole point of the module. Resolving a name in one place and
|
|
39
|
+
* connecting in another is a DNS rebinding race: the attacker's server
|
|
40
|
+
* answers `93.184.216.34` for the check and `169.254.169.254` a moment
|
|
41
|
+
* later for the connection, and a checker that looked the name up itself
|
|
42
|
+
* would have validated an address nothing ever connected to. Node lets the
|
|
43
|
+
* connection's *own* resolution be replaced, so the addresses this returns
|
|
44
|
+
* are the addresses the socket uses — there is no second lookup to lose to.
|
|
45
|
+
*/
|
|
46
|
+
export const createPinnedLookup = (policy) => {
|
|
47
|
+
const lookup = (hostname, options, callback) => {
|
|
48
|
+
dns.lookup(hostname, { all: true, verbatim: true }, (error, addresses) => {
|
|
49
|
+
if (error) {
|
|
50
|
+
callback(error, '', 0);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const permitted = addresses.filter((entry) => checkAddress(policy, hostname, entry.address).allowed);
|
|
54
|
+
if (permitted.length === 0) {
|
|
55
|
+
const first = addresses[0];
|
|
56
|
+
const verdict = first ? checkAddress(policy, hostname, first.address) : undefined;
|
|
57
|
+
callback(new EgressPolicyError(`Refusing to connect to ${hostname}: ${verdict?.reason ?? 'it resolves to no public address'}`), '', 0);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (options.all) {
|
|
61
|
+
callback(null, permitted);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const [chosen] = permitted;
|
|
65
|
+
callback(null, chosen?.address ?? '', chosen?.family ?? 4);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
return lookup;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* One HTTP exchange, pinned to a validated address.
|
|
72
|
+
*
|
|
73
|
+
* Redirects are deliberately **not** followed here: each hop is a new
|
|
74
|
+
* request to a new host, so each one has to face the policy again. A
|
|
75
|
+
* caller loops and re-enters this, which is what makes "an approved public
|
|
76
|
+
* URL that redirects into the metadata endpoint" a refusal rather than a
|
|
77
|
+
* fetch.
|
|
78
|
+
*/
|
|
79
|
+
export const requestThroughPolicy = async (rawUrl, options = {}) => {
|
|
80
|
+
const policy = options.policy ?? {};
|
|
81
|
+
const url = assertRequestAllowed(rawUrl, policy);
|
|
82
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
83
|
+
const maxBytes = options.maxBytes ?? 2_000_000;
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
let deadline;
|
|
86
|
+
const settle = (finish) => (value) => {
|
|
87
|
+
if (deadline) {
|
|
88
|
+
clearTimeout(deadline);
|
|
89
|
+
deadline = undefined;
|
|
90
|
+
}
|
|
91
|
+
finish(value);
|
|
92
|
+
};
|
|
93
|
+
const succeed = settle(resolve);
|
|
94
|
+
const fail = settle(reject);
|
|
95
|
+
const request = transport.request(url, {
|
|
96
|
+
method: 'GET',
|
|
97
|
+
headers: options.headers ?? {},
|
|
98
|
+
...pinnedConnection(policy),
|
|
99
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
100
|
+
}, (response) => {
|
|
101
|
+
let body = '';
|
|
102
|
+
let bytes = 0;
|
|
103
|
+
let truncated = false;
|
|
104
|
+
response.setEncoding('utf8');
|
|
105
|
+
response.on('data', (chunk) => {
|
|
106
|
+
if (truncated) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const size = Buffer.byteLength(chunk, 'utf8');
|
|
110
|
+
if (bytes + size > maxBytes) {
|
|
111
|
+
truncated = true;
|
|
112
|
+
// Kept to the cap, not to the end of whichever chunk crossed
|
|
113
|
+
// it: a single response can arrive in one piece, and "stop
|
|
114
|
+
// after the chunk that went over" is no cap at all.
|
|
115
|
+
body += chunk.slice(0, Math.max(0, maxBytes - bytes));
|
|
116
|
+
bytes = maxBytes;
|
|
117
|
+
// Stop the transfer rather than reading a gigabyte into a
|
|
118
|
+
// string we have already decided not to keep.
|
|
119
|
+
response.destroy();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
bytes += size;
|
|
123
|
+
body += chunk;
|
|
124
|
+
});
|
|
125
|
+
response.on('error', (error) => {
|
|
126
|
+
if (truncated) {
|
|
127
|
+
succeed({ status: response.statusCode ?? 0, headers: response.headers, body, truncated });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
fail(error);
|
|
131
|
+
});
|
|
132
|
+
response.on('end', () => {
|
|
133
|
+
succeed({ status: response.statusCode ?? 0, headers: response.headers, body, truncated });
|
|
134
|
+
});
|
|
135
|
+
response.on('close', () => {
|
|
136
|
+
if (truncated) {
|
|
137
|
+
succeed({ status: response.statusCode ?? 0, headers: response.headers, body, truncated });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
if (options.timeoutMs) {
|
|
142
|
+
// A deadline for the whole exchange, not for a quiet socket.
|
|
143
|
+
// `setTimeout` on a request fires on *inactivity*, so a server that
|
|
144
|
+
// sends one byte before each interval keeps a fetch alive forever —
|
|
145
|
+
// which for a one-shot `stratus run` is a command that never returns.
|
|
146
|
+
// Both are armed: the wall clock bounds the exchange, and the
|
|
147
|
+
// inactivity timer still ends a connection that has simply stalled.
|
|
148
|
+
deadline = setTimeout(() => {
|
|
149
|
+
request.destroy(new Error(`Timed out after ${options.timeoutMs}ms: ${url.href}`));
|
|
150
|
+
}, options.timeoutMs);
|
|
151
|
+
deadline.unref?.();
|
|
152
|
+
request.setTimeout(options.timeoutMs, () => {
|
|
153
|
+
request.destroy(new Error(`Timed out after ${options.timeoutMs}ms of silence: ${url.href}`));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
request.on('error', fail);
|
|
157
|
+
request.end();
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* A proxy that owns every connection a browser makes, so the browser's own
|
|
162
|
+
* resolver never decides where a request goes.
|
|
163
|
+
*
|
|
164
|
+
* Playwright can intercept requests, but interception happens in the
|
|
165
|
+
* browser and the browser resolves the name itself afterwards — validating
|
|
166
|
+
* there is the rebinding race again, one process further away. Routing
|
|
167
|
+
* Chromium through a proxy that resolves, validates, and connects means the
|
|
168
|
+
* socket the page gets is the socket this policy opened, for the address it
|
|
169
|
+
* checked. Redirects and subresources come back through here too, because
|
|
170
|
+
* every request does.
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
173
|
+
* The `host:port` a CONNECT carries, which is not a plain split on colons.
|
|
174
|
+
*
|
|
175
|
+
* A literal IPv6 host arrives bracketed — `[2606:4700::1111]:443` — and
|
|
176
|
+
* splitting that on every colon yields a host of `[`, which is then looked
|
|
177
|
+
* up in DNS and fails. Every IPv6-literal HTTPS page a browser is pointed
|
|
178
|
+
* at would be unreachable, and the failure would read as a policy refusal
|
|
179
|
+
* rather than a parse bug.
|
|
180
|
+
*/
|
|
181
|
+
const parseAuthority = (authority) => {
|
|
182
|
+
const bracketed = /^\[(?<host>[^\]]+)\](?::(?<port>\d+))?$/.exec(authority);
|
|
183
|
+
const plain = /^(?<host>[^:]+)(?::(?<port>\d+))?$/.exec(authority);
|
|
184
|
+
const groups = (bracketed ?? plain)?.groups;
|
|
185
|
+
if (!groups?.host) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const port = groups.port === undefined ? 443 : Number(groups.port);
|
|
189
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return { host: groups.host, port };
|
|
193
|
+
};
|
|
194
|
+
export const createEgressProxy = async (policy = {}) => {
|
|
195
|
+
const refusals = [];
|
|
196
|
+
const sockets = new Set();
|
|
197
|
+
const dial = async (host, port) => {
|
|
198
|
+
const addresses = await dns.promises.lookup(host, { all: true, verbatim: true });
|
|
199
|
+
const permitted = addresses.find((entry) => checkAddress(policy, host, entry.address).allowed);
|
|
200
|
+
if (!permitted) {
|
|
201
|
+
const first = addresses[0];
|
|
202
|
+
const verdict = first ? checkAddress(policy, host, first.address) : undefined;
|
|
203
|
+
throw new EgressPolicyError(`Refusing to connect to ${host}: ${verdict?.reason ?? 'it resolves to no public address'}`);
|
|
204
|
+
}
|
|
205
|
+
// Connected to the checked address itself, not to the name: a second
|
|
206
|
+
// resolution is a second answer, and the attacker chooses when it
|
|
207
|
+
// changes.
|
|
208
|
+
return net.connect({ host: permitted.address, port });
|
|
209
|
+
};
|
|
210
|
+
const server = http.createServer();
|
|
211
|
+
server.on('connect', (request, clientSocket, head) => {
|
|
212
|
+
sockets.add(clientSocket);
|
|
213
|
+
clientSocket.on('close', () => sockets.delete(clientSocket));
|
|
214
|
+
const target = parseAuthority(request.url ?? '');
|
|
215
|
+
if (!target) {
|
|
216
|
+
clientSocket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const { host, port } = target;
|
|
220
|
+
dial(host, port).then((upstream) => {
|
|
221
|
+
sockets.add(upstream);
|
|
222
|
+
upstream.on('close', () => sockets.delete(upstream));
|
|
223
|
+
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
224
|
+
if (head.length > 0) {
|
|
225
|
+
upstream.write(head);
|
|
226
|
+
}
|
|
227
|
+
upstream.pipe(clientSocket);
|
|
228
|
+
clientSocket.pipe(upstream);
|
|
229
|
+
upstream.on('error', () => clientSocket.destroy());
|
|
230
|
+
clientSocket.on('error', () => upstream.destroy());
|
|
231
|
+
}, (error) => {
|
|
232
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
233
|
+
refusals.push(reason);
|
|
234
|
+
// 403 rather than a dropped socket: the page sees a refusal it can
|
|
235
|
+
// render, and the reason reaches the agent through `refusals`.
|
|
236
|
+
clientSocket.end(`HTTP/1.1 403 Forbidden\r\n\r\n${reason}`);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
server.on('request', (request, response) => {
|
|
240
|
+
// Plain HTTP arrives as an absolute-URI request rather than a CONNECT.
|
|
241
|
+
let target;
|
|
242
|
+
try {
|
|
243
|
+
target = assertRequestAllowed(request.url ?? '', policy);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
247
|
+
refusals.push(reason);
|
|
248
|
+
response.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
|
|
249
|
+
response.end(reason);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const upstream = http.request({
|
|
253
|
+
host: target.hostname,
|
|
254
|
+
port: target.port ? Number(target.port) : 80,
|
|
255
|
+
path: `${target.pathname}${target.search}`,
|
|
256
|
+
method: request.method ?? 'GET',
|
|
257
|
+
headers: request.headers,
|
|
258
|
+
...pinnedConnection(policy),
|
|
259
|
+
}, (upstreamResponse) => {
|
|
260
|
+
response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
|
|
261
|
+
upstreamResponse.pipe(response);
|
|
262
|
+
});
|
|
263
|
+
upstream.on('error', (error) => {
|
|
264
|
+
refusals.push(error.message);
|
|
265
|
+
if (!response.headersSent) {
|
|
266
|
+
response.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
|
|
267
|
+
}
|
|
268
|
+
response.end(error.message);
|
|
269
|
+
});
|
|
270
|
+
request.pipe(upstream);
|
|
271
|
+
});
|
|
272
|
+
await new Promise((resolve) => {
|
|
273
|
+
// Loopback only: this proxy exists to narrow what one process may
|
|
274
|
+
// reach, and a proxy on a shared interface is an open relay.
|
|
275
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
276
|
+
});
|
|
277
|
+
const port = server.address().port;
|
|
278
|
+
return {
|
|
279
|
+
url: `http://127.0.0.1:${port}`,
|
|
280
|
+
port,
|
|
281
|
+
refusals,
|
|
282
|
+
async close() {
|
|
283
|
+
for (const socket of sockets) {
|
|
284
|
+
socket.destroy();
|
|
285
|
+
}
|
|
286
|
+
await new Promise((resolve) => {
|
|
287
|
+
server.close(() => resolve());
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* What a Chromium launch needs to be actually pinned to a proxy.
|
|
294
|
+
*
|
|
295
|
+
* `--proxy-bypass-list=<-loopback>` is not optional: Chromium bypasses the
|
|
296
|
+
* proxy for loopback and `localhost` by default, so a browser pointed at
|
|
297
|
+
* this proxy would still reach `http://127.0.0.1:8080` directly — which is
|
|
298
|
+
* one of the exact destinations the proxy exists to refuse.
|
|
299
|
+
*/
|
|
300
|
+
export const chromiumProxyOptions = (proxy) => ({
|
|
301
|
+
server: proxy.url,
|
|
302
|
+
// Same reason as the flag below, in the form Playwright takes.
|
|
303
|
+
bypass: '<-loopback>',
|
|
304
|
+
});
|
|
305
|
+
export const chromiumProxyArgs = (proxy) => [
|
|
306
|
+
`--proxy-server=${proxy.url}`,
|
|
307
|
+
'--proxy-bypass-list=<-loopback>',
|
|
308
|
+
];
|
|
309
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The table every tool that reaches the network is tested against.
|
|
3
|
+
*
|
|
4
|
+
* Shipped in `src` rather than kept in one package's tests because two
|
|
5
|
+
* packages must be held to it, and the acceptance criterion for that is
|
|
6
|
+
* exact: `web.fetch` refuses the same addresses and schemes as `browser.*`,
|
|
7
|
+
* *proven by a test that runs the same table through both*. If the shared
|
|
8
|
+
* module is ever forked, this is what fails — in whichever package forked
|
|
9
|
+
* it, because both suites read this list rather than their own.
|
|
10
|
+
*/
|
|
11
|
+
export interface HostileUrl {
|
|
12
|
+
url: string;
|
|
13
|
+
/** What it is, for a failing assertion that names the hole rather than an index. */
|
|
14
|
+
what: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const HOSTILE_URLS: HostileUrl[];
|
|
17
|
+
//# sourceMappingURL=fixtures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,EAwBpC,CAAC"}
|
package/dist/fixtures.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const HOSTILE_URLS = [
|
|
2
|
+
{ url: 'file:///home/ada/.stratus/credentials.json', what: 'the credential store, by file: URL' },
|
|
3
|
+
{ url: 'file:///etc/passwd', what: 'a local file' },
|
|
4
|
+
{ url: 'data:text/html,<script>fetch("http://169.254.169.254")</script>', what: 'an inline document' },
|
|
5
|
+
{ url: 'javascript:fetch("http://127.0.0.1")', what: 'a javascript: URL' },
|
|
6
|
+
{ url: 'ftp://example.com/secrets', what: 'a scheme nobody asked for' },
|
|
7
|
+
{ url: 'http://169.254.169.254/latest/meta-data/', what: 'cloud instance metadata' },
|
|
8
|
+
{ url: 'http://[fd00::1]/', what: 'an IPv6 unique-local address' },
|
|
9
|
+
{ url: 'http://[::ffff:169.254.169.254]/', what: 'metadata again, as an IPv4-mapped IPv6 address' },
|
|
10
|
+
{ url: 'http://[::ffff:127.0.0.1]/', what: 'loopback, as an IPv4-mapped IPv6 address' },
|
|
11
|
+
{ url: 'http://[64:ff9b::a9fe:a9fe]/', what: 'metadata again, through NAT64' },
|
|
12
|
+
{ url: 'http://[64:ff9b:1::a9fe:a9fe]/', what: 'metadata again, through the local-use NAT64 prefix' },
|
|
13
|
+
{ url: 'http://[2002:a9fe:a9fe::]/', what: 'metadata again, through 6to4' },
|
|
14
|
+
{ url: 'http://127.0.0.1:4123/api/v1/credentials', what: 'the daemon’s own control API' },
|
|
15
|
+
{ url: 'http://[::1]:4123/api/v1/credentials', what: 'the control API over IPv6 loopback' },
|
|
16
|
+
{ url: 'http://localhost:4123/api/v1/credentials', what: 'the control API by name' },
|
|
17
|
+
{ url: 'http://10.0.0.5/', what: 'an RFC 1918 address' },
|
|
18
|
+
{ url: 'http://172.16.4.4/', what: 'an RFC 1918 address in the /12' },
|
|
19
|
+
{ url: 'http://192.168.1.1/', what: 'the home router' },
|
|
20
|
+
{ url: 'http://100.100.100.200/', what: 'a carrier-NAT address' },
|
|
21
|
+
{ url: 'http://[fe80::1]/', what: 'an IPv6 link-local address' },
|
|
22
|
+
{ url: 'http://0.0.0.0/', what: 'the unspecified address' },
|
|
23
|
+
{ url: 'http://192.0.0.1/', what: 'IETF protocol assignment space' },
|
|
24
|
+
{ url: 'http://224.0.0.1/', what: 'multicast' },
|
|
25
|
+
];
|
|
26
|
+
//# sourceMappingURL=fixtures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures.js","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAgBA,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,EAAE,GAAG,EAAE,4CAA4C,EAAE,IAAI,EAAE,oCAAoC,EAAE;IACjG,EAAE,GAAG,EAAE,oBAAoB,EAAE,IAAI,EAAE,cAAc,EAAE;IACnD,EAAE,GAAG,EAAE,iEAAiE,EAAE,IAAI,EAAE,oBAAoB,EAAE;IACtG,EAAE,GAAG,EAAE,sCAAsC,EAAE,IAAI,EAAE,mBAAmB,EAAE;IAC1E,EAAE,GAAG,EAAE,2BAA2B,EAAE,IAAI,EAAE,2BAA2B,EAAE;IACvE,EAAE,GAAG,EAAE,0CAA0C,EAAE,IAAI,EAAE,yBAAyB,EAAE;IACpF,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,8BAA8B,EAAE;IAClE,EAAE,GAAG,EAAE,kCAAkC,EAAE,IAAI,EAAE,gDAAgD,EAAE;IACnG,EAAE,GAAG,EAAE,4BAA4B,EAAE,IAAI,EAAE,0CAA0C,EAAE;IACvF,EAAE,GAAG,EAAE,8BAA8B,EAAE,IAAI,EAAE,+BAA+B,EAAE;IAC9E,EAAE,GAAG,EAAE,gCAAgC,EAAE,IAAI,EAAE,oDAAoD,EAAE;IACrG,EAAE,GAAG,EAAE,4BAA4B,EAAE,IAAI,EAAE,8BAA8B,EAAE;IAC3E,EAAE,GAAG,EAAE,0CAA0C,EAAE,IAAI,EAAE,8BAA8B,EAAE;IACzF,EAAE,GAAG,EAAE,sCAAsC,EAAE,IAAI,EAAE,oCAAoC,EAAE;IAC3F,EAAE,GAAG,EAAE,0CAA0C,EAAE,IAAI,EAAE,yBAAyB,EAAE;IACpF,EAAE,GAAG,EAAE,kBAAkB,EAAE,IAAI,EAAE,qBAAqB,EAAE;IACxD,EAAE,GAAG,EAAE,oBAAoB,EAAE,IAAI,EAAE,gCAAgC,EAAE;IACrE,EAAE,GAAG,EAAE,qBAAqB,EAAE,IAAI,EAAE,iBAAiB,EAAE;IACvD,EAAE,GAAG,EAAE,yBAAyB,EAAE,IAAI,EAAE,uBAAuB,EAAE;IACjE,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,4BAA4B,EAAE;IAChE,EAAE,GAAG,EAAE,iBAAiB,EAAE,IAAI,EAAE,yBAAyB,EAAE;IAC3D,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,gCAAgC,EAAE;IACpE,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,WAAW,EAAE;CAChD,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One implementation of the address policy, shared.
|
|
3
|
+
*
|
|
4
|
+
* `tool-web` and `tool-browser` both reach the network on an agent's
|
|
5
|
+
* instruction, and a second hand-rolled copy of "which addresses may a tool
|
|
6
|
+
* reach" would not be a drift — it would be an SSRF hole in whichever copy
|
|
7
|
+
* was not updated. So the rule lives here, once, and the packages that
|
|
8
|
+
* enforce it import it.
|
|
9
|
+
*/
|
|
10
|
+
export { assertRequestAllowed, checkAddress, classifyAddress, EgressPolicyError, type AddressVerdict, type EgressPolicy, } from './addresses.ts';
|
|
11
|
+
export { chromiumProxyArgs, chromiumProxyOptions, createEgressProxy, createPinnedLookup, policyKeyFor, requestThroughPolicy, type EgressProxy, type PolicyRequestOptions, type PolicyResponse, } from './connect.ts';
|
|
12
|
+
export { HOSTILE_URLS, type HostileUrl } from './fixtures.ts';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,GAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One implementation of the address policy, shared.
|
|
3
|
+
*
|
|
4
|
+
* `tool-web` and `tool-browser` both reach the network on an agent's
|
|
5
|
+
* instruction, and a second hand-rolled copy of "which addresses may a tool
|
|
6
|
+
* reach" would not be a drift — it would be an SSRF hole in whichever copy
|
|
7
|
+
* was not updated. So the rule lives here, once, and the packages that
|
|
8
|
+
* enforce it import it.
|
|
9
|
+
*/
|
|
10
|
+
export { assertRequestAllowed, checkAddress, classifyAddress, EgressPolicyError, } from "./addresses.js";
|
|
11
|
+
export { chromiumProxyArgs, chromiumProxyOptions, createEgressProxy, createPinnedLookup, policyKeyFor, requestThroughPolicy, } from "./connect.js";
|
|
12
|
+
export { HOSTILE_URLS } from "./fixtures.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,eAAe,EACf,iBAAiB,GAGlB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,GAIrB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,YAAY,EAAmB,MAAM,eAAe,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stratusagent/egress",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.13 <23 || >=23.4"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^24.6.0",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/stratuslabs/agent.git",
|
|
29
|
+
"directory": "packages/egress"
|
|
30
|
+
},
|
|
31
|
+
"description": "The Stratus Agent egress policy: which addresses a tool may reach, enforced on the connection rather than on the name it was asked for",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"stratus",
|
|
34
|
+
"agent",
|
|
35
|
+
"ai",
|
|
36
|
+
"llm",
|
|
37
|
+
"ssrf",
|
|
38
|
+
"security"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -b",
|
|
45
|
+
"typecheck": "tsc --noEmit --project tsconfig.test.json",
|
|
46
|
+
"test": "node --test --experimental-strip-types"
|
|
47
|
+
}
|
|
48
|
+
}
|