ssrf-lab 0.0.0-stage → 0.1.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +407 -2
  3. package/dist/adapters/agentClient.d.ts +21 -0
  4. package/dist/adapters/agentClient.js +78 -0
  5. package/dist/adapters/agentClient.js.map +1 -0
  6. package/dist/adapters/checkThenConnect.d.ts +12 -0
  7. package/dist/adapters/checkThenConnect.js +94 -0
  8. package/dist/adapters/checkThenConnect.js.map +1 -0
  9. package/dist/adapters/guardedFetch.d.ts +13 -0
  10. package/dist/adapters/guardedFetch.js +110 -0
  11. package/dist/adapters/guardedFetch.js.map +1 -0
  12. package/dist/adapters/internal.d.ts +30 -0
  13. package/dist/adapters/internal.js +95 -0
  14. package/dist/adapters/internal.js.map +1 -0
  15. package/dist/adapters/libraries.d.ts +14 -0
  16. package/dist/adapters/libraries.js +47 -0
  17. package/dist/adapters/libraries.js.map +1 -0
  18. package/dist/adapters/optional.d.ts +37 -0
  19. package/dist/adapters/optional.js +84 -0
  20. package/dist/adapters/optional.js.map +1 -0
  21. package/dist/adapters/plainFetch.d.ts +9 -0
  22. package/dist/adapters/plainFetch.js +65 -0
  23. package/dist/adapters/plainFetch.js.map +1 -0
  24. package/dist/adapters/types.d.ts +55 -0
  25. package/dist/adapters/types.js +17 -0
  26. package/dist/adapters/types.js.map +1 -0
  27. package/dist/adapters/vercelGuardedFetch.d.ts +7 -0
  28. package/dist/adapters/vercelGuardedFetch.js +65 -0
  29. package/dist/adapters/vercelGuardedFetch.js.map +1 -0
  30. package/dist/checker/probes.d.ts +34 -0
  31. package/dist/checker/probes.js +124 -0
  32. package/dist/checker/probes.js.map +1 -0
  33. package/dist/checker/report.d.ts +5 -0
  34. package/dist/checker/report.js +26 -0
  35. package/dist/checker/report.js.map +1 -0
  36. package/dist/checker/run.d.ts +27 -0
  37. package/dist/checker/run.js +108 -0
  38. package/dist/checker/run.js.map +1 -0
  39. package/dist/cli.d.ts +2 -0
  40. package/dist/cli.js +120 -0
  41. package/dist/cli.js.map +1 -0
  42. package/dist/index.d.ts +16 -0
  43. package/dist/index.js +17 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/range/bomb.d.ts +13 -0
  46. package/dist/range/bomb.js +24 -0
  47. package/dist/range/bomb.js.map +1 -0
  48. package/dist/range/canaries.d.ts +12 -0
  49. package/dist/range/canaries.js +13 -0
  50. package/dist/range/canaries.js.map +1 -0
  51. package/dist/range/isolation.d.ts +4 -0
  52. package/dist/range/isolation.js +24 -0
  53. package/dist/range/isolation.js.map +1 -0
  54. package/dist/range/server.d.ts +51 -0
  55. package/dist/range/server.js +190 -0
  56. package/dist/range/server.js.map +1 -0
  57. package/dist/util/addresses.d.ts +21 -0
  58. package/dist/util/addresses.js +143 -0
  59. package/dist/util/addresses.js.map +1 -0
  60. package/dist/util/flipLookup.d.ts +11 -0
  61. package/dist/util/flipLookup.js +32 -0
  62. package/dist/util/flipLookup.js.map +1 -0
  63. package/package.json +57 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bomb.js","sourceRoot":"","sources":["../../src/range/bomb.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAEhD,uDAAuD;AACvD,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;AAEnD,IAAI,MAAM,GAAkB,IAAI,CAAA;AAEhC;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB;IAC/B,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,MAAM,CAAA;IAElC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAC/E,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IAC7E,MAAM,GAAG,QAAQ,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;IACxC,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Canaries are the proof that a client REACHED something it should not have.
3
+ *
4
+ * A probe that only watches for an error cannot tell a control from bad luck:
5
+ * a connection to a dead address fails on its own. A canary in the response
6
+ * body is positive evidence that the request completed.
7
+ */
8
+ export declare const CANARY_INTERNAL_SECRET = "SSRF-LAB-CANARY-INTERNAL-SECRET";
9
+ /** Returned by the body that a size cap must never finish reading. */
10
+ export declare const CANARY_ENDLESS_CHUNK = "SSRF-LAB-CANARY-ENDLESS";
11
+ /** Returned by the decompression bomb once it is fully expanded. */
12
+ export declare const CANARY_BOMB_TAIL = "SSRF-LAB-CANARY-BOMB-TAIL";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Canaries are the proof that a client REACHED something it should not have.
3
+ *
4
+ * A probe that only watches for an error cannot tell a control from bad luck:
5
+ * a connection to a dead address fails on its own. A canary in the response
6
+ * body is positive evidence that the request completed.
7
+ */
8
+ export const CANARY_INTERNAL_SECRET = 'SSRF-LAB-CANARY-INTERNAL-SECRET';
9
+ /** Returned by the body that a size cap must never finish reading. */
10
+ export const CANARY_ENDLESS_CHUNK = 'SSRF-LAB-CANARY-ENDLESS';
11
+ /** Returned by the decompression bomb once it is fully expanded. */
12
+ export const CANARY_BOMB_TAIL = 'SSRF-LAB-CANARY-BOMB-TAIL';
13
+ //# sourceMappingURL=canaries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canaries.js","sourceRoot":"","sources":["../../src/range/canaries.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,iCAAiC,CAAA;AAEvE,sEAAsE;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAG,yBAAyB,CAAA;AAE7D,oEAAoE;AACpE,MAAM,CAAC,MAAM,gBAAgB,GAAG,2BAA2B,CAAA"}
@@ -0,0 +1,4 @@
1
+ /** This address exists only on lo inside the isolated integration namespace. */
2
+ export declare const ISOLATED_PUBLIC_ADDRESS = "93.184.216.34";
3
+ /** Fail closed before a listener or client can use the public-address fixture. */
4
+ export declare function assertIsolatedNetwork(): void;
@@ -0,0 +1,24 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ /** This address exists only on lo inside the isolated integration namespace. */
3
+ export const ISOLATED_PUBLIC_ADDRESS = '93.184.216.34';
4
+ /** Fail closed before a listener or client can use the public-address fixture. */
5
+ export function assertIsolatedNetwork() {
6
+ if (process.platform !== 'linux')
7
+ throw new Error('Public-address tests require Linux network isolation');
8
+ const links = JSON.parse(execFileSync('ip', ['-j', 'address', 'show'], { encoding: 'utf8' }));
9
+ if (links.length !== 1 || links[0]?.ifname !== 'lo' || !links[0].flags.includes('UP')) {
10
+ throw new Error('Public-address tests require a network namespace with only lo, enabled');
11
+ }
12
+ if (!links[0].addr_info.some((address) => address.local === ISOLATED_PUBLIC_ADDRESS)) {
13
+ throw new Error(`Assign ${ISOLATED_PUBLIC_ADDRESS}/32 to lo inside the isolated namespace first`);
14
+ }
15
+ for (const family of ['-4', '-6']) {
16
+ const routes = JSON.parse(execFileSync('ip', ['-j', family, 'route', 'show', 'table', 'all'], {
17
+ encoding: 'utf8',
18
+ }));
19
+ if (routes.some((route) => route.dev !== 'lo' || route.gateway !== undefined || route.dst === 'default')) {
20
+ throw new Error('Public-address tests require no external routes or gateways');
21
+ }
22
+ }
23
+ }
24
+ //# sourceMappingURL=isolation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isolation.js","sourceRoot":"","sources":["../../src/range/isolation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEjD,gFAAgF;AAChF,MAAM,CAAC,MAAM,uBAAuB,GAAG,eAAe,CAAA;AAEtD,kFAAkF;AAClF,MAAM,UAAU,qBAAqB;IACnC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAA;IACzG,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CACrB,CAAA;IACvE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACtF,MAAM,IAAI,KAAK,CAAC,wEAAwE,CAAC,CAAA;IAC3F,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,KAAK,uBAAuB,CAAC,EAAE,CAAC;QACrF,MAAM,IAAI,KAAK,CAAC,UAAU,uBAAuB,+CAA+C,CAAC,CAAA;IACnG,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE;YAC5F,QAAQ,EAAE,MAAM;SACjB,CAAC,CAAuD,CAAA;QACzD,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS,CAAC,EAAE,CAAC;YACzG,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAA;QAChF,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A running attack range.
3
+ *
4
+ * It is 2 servers, because 1 is not enough to tell a control from an accident:
5
+ *
6
+ * public the site the operator asked for. It stands for the internet, so
7
+ * a client is allowed to fetch it, and it is where every attack
8
+ * starts.
9
+ * internal the service behind the firewall. Nothing outside may read it,
10
+ * and it answers with a canary so a hit is provable.
11
+ *
12
+ * Both normally listen on loopback. The optional public-address fixture binds
13
+ * an address assigned to lo in a verified isolated namespace. `trustedOrigin`
14
+ * is a LAB AFFORDANCE for the built-in clients, never a production setting.
15
+ */
16
+ export interface AttackRange {
17
+ readonly origin: string;
18
+ readonly port: number;
19
+ readonly internalOrigin: string;
20
+ readonly internalPort: number;
21
+ url(path: string): string;
22
+ /**
23
+ * How many requests the internal service has answered.
24
+ *
25
+ * This is the measurement that separates "the control stopped it" from "the
26
+ * client tried and something else failed". A canary in a body proves a hit;
27
+ * this counter proves the ABSENCE of one.
28
+ */
29
+ internalHits(): number;
30
+ /** Requests received by the entry server, including successful controls. */
31
+ publicHits(): number;
32
+ close(): Promise<void>;
33
+ }
34
+ /** Paths the public server serves. */
35
+ export declare const RANGE_PATHS: {
36
+ readonly index: "/";
37
+ readonly redirectLoopback: "/redirect/loopback";
38
+ readonly redirectDecimalLoopback: "/redirect/decimal-loopback";
39
+ readonly redirectLinkLocal: "/redirect/link-local";
40
+ readonly redirectPrivate: "/redirect/private";
41
+ readonly redirectCredentials: "/redirect/credentials";
42
+ readonly redirectLoop: "/redirect/loop";
43
+ readonly payloadBomb: "/payload/bomb.gz";
44
+ readonly payloadEndless: "/payload/endless";
45
+ readonly payloadSlow: "/payload/slow";
46
+ };
47
+ /** The only path the internal service serves. */
48
+ export declare const INTERNAL_PATH = "/internal/secret";
49
+ export declare function startAttackRange(options?: {
50
+ publicAddress?: boolean;
51
+ }): Promise<AttackRange>;
@@ -0,0 +1,190 @@
1
+ import { createServer } from 'node:http';
2
+ import { METADATA_ADDRESS } from '../util/addresses.js';
3
+ import { decompressionBomb } from './bomb.js';
4
+ import { CANARY_ENDLESS_CHUNK, CANARY_INTERNAL_SECRET } from './canaries.js';
5
+ import { assertIsolatedNetwork, ISOLATED_PUBLIC_ADDRESS } from './isolation.js';
6
+ /** Paths the public server serves. */
7
+ export const RANGE_PATHS = {
8
+ index: '/',
9
+ redirectLoopback: '/redirect/loopback',
10
+ redirectDecimalLoopback: '/redirect/decimal-loopback',
11
+ redirectLinkLocal: '/redirect/link-local',
12
+ redirectPrivate: '/redirect/private',
13
+ redirectCredentials: '/redirect/credentials',
14
+ redirectLoop: '/redirect/loop',
15
+ payloadBomb: '/payload/bomb.gz',
16
+ payloadEndless: '/payload/endless',
17
+ payloadSlow: '/payload/slow',
18
+ };
19
+ /** The only path the internal service serves. */
20
+ export const INTERNAL_PATH = '/internal/secret';
21
+ export async function startAttackRange(options = {}) {
22
+ if (options.publicAddress)
23
+ assertIsolatedNetwork();
24
+ const publicHost = options.publicAddress ? ISOLATED_PUBLIC_ADDRESS : '127.0.0.1';
25
+ const timers = new Set();
26
+ let internalHits = 0;
27
+ let publicHits = 0;
28
+ const internal = await listen((_request, response) => {
29
+ internalHits += 1;
30
+ text(response, 200, CANARY_INTERNAL_SECRET);
31
+ });
32
+ let publicSide;
33
+ try {
34
+ publicSide = await listen((request, response) => {
35
+ publicHits += 1;
36
+ handle(request, response, `http://127.0.0.1:${internal.port}`, internal.port, timers);
37
+ }, publicHost);
38
+ }
39
+ catch (error) {
40
+ await close(internal);
41
+ throw error;
42
+ }
43
+ return {
44
+ origin: `http://${publicHost}:${publicSide.port}`,
45
+ port: publicSide.port,
46
+ internalOrigin: `http://127.0.0.1:${internal.port}`,
47
+ internalPort: internal.port,
48
+ url: (path) => `http://${publicHost}:${publicSide.port}${path}`,
49
+ internalHits: () => internalHits,
50
+ publicHits: () => publicHits,
51
+ close: async () => {
52
+ for (const timer of timers)
53
+ clearInterval(timer);
54
+ timers.clear();
55
+ await close(publicSide);
56
+ await close(internal);
57
+ },
58
+ };
59
+ }
60
+ async function listen(handler, host = '127.0.0.1') {
61
+ const sockets = new Set();
62
+ const server = createServer(handler);
63
+ server.on('connection', (socket) => {
64
+ sockets.add(socket);
65
+ socket.on('close', () => sockets.delete(socket));
66
+ });
67
+ await new Promise((resolve, reject) => {
68
+ server.once('error', reject);
69
+ server.listen(0, host, resolve);
70
+ });
71
+ return { server, port: server.address().port, sockets };
72
+ }
73
+ async function close(listener) {
74
+ for (const socket of listener.sockets)
75
+ socket.destroy();
76
+ listener.sockets.clear();
77
+ await new Promise((resolve, reject) => {
78
+ listener.server.close((error) => (error ? reject(error) : resolve()));
79
+ });
80
+ }
81
+ function handle(request, response, internalOrigin, internalPort, timers) {
82
+ const path = (request.url ?? '/').split('?')[0] ?? '/';
83
+ switch (path) {
84
+ case RANGE_PATHS.index: {
85
+ text(response, 200, Object.values(RANGE_PATHS).join('\n'));
86
+ return;
87
+ }
88
+ case RANGE_PATHS.redirectLoopback: {
89
+ redirect(response, `${internalOrigin}${INTERNAL_PATH}`);
90
+ return;
91
+ }
92
+ // 2130706433 is 127.0.0.1 written as one decimal number. A refusal that
93
+ // compares the raw host string never sees it; the URL parser does.
94
+ case RANGE_PATHS.redirectDecimalLoopback: {
95
+ redirect(response, `http://2130706433:${internalPort}${INTERNAL_PATH}`);
96
+ return;
97
+ }
98
+ case RANGE_PATHS.redirectLinkLocal: {
99
+ redirect(response, `http://${METADATA_ADDRESS}/latest/meta-data/`);
100
+ return;
101
+ }
102
+ // 198.51.100.0/24 is TEST-NET-2, reserved for documentation, so this probe
103
+ // reaches no real device on the machine's own network.
104
+ case RANGE_PATHS.redirectPrivate: {
105
+ redirect(response, 'http://198.51.100.1/');
106
+ return;
107
+ }
108
+ // Credentials in a redirect target end up in logs and error messages.
109
+ case RANGE_PATHS.redirectCredentials: {
110
+ redirect(response, `http://lab:s3cr3t@127.0.0.1:${internalPort}${INTERNAL_PATH}`);
111
+ return;
112
+ }
113
+ // Stays on the public origin, so it tests the hop cap and nothing else.
114
+ case RANGE_PATHS.redirectLoop: {
115
+ redirect(response, RANGE_PATHS.redirectLoop);
116
+ return;
117
+ }
118
+ case RANGE_PATHS.payloadBomb: {
119
+ const body = decompressionBomb();
120
+ response.writeHead(200, {
121
+ 'content-type': 'application/octet-stream',
122
+ 'content-encoding': 'gzip',
123
+ 'content-length': String(body.byteLength),
124
+ });
125
+ response.end(body);
126
+ return;
127
+ }
128
+ case RANGE_PATHS.payloadEndless: {
129
+ endless(response, timers);
130
+ return;
131
+ }
132
+ case RANGE_PATHS.payloadSlow: {
133
+ slow(response, timers);
134
+ return;
135
+ }
136
+ default: {
137
+ text(response, 404, 'no such target');
138
+ return;
139
+ }
140
+ }
141
+ }
142
+ function text(response, status, body) {
143
+ response.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
144
+ response.end(body);
145
+ }
146
+ function redirect(response, location) {
147
+ response.writeHead(302, { location, 'content-type': 'text/plain; charset=utf-8' });
148
+ response.end(`follow me to ${location}`);
149
+ }
150
+ /**
151
+ * A body with no end and no Content-Length.
152
+ *
153
+ * A cap that reads Content-Length and trusts it learns nothing here. Only a
154
+ * running total of received bytes stops this.
155
+ */
156
+ function endless(response, timers) {
157
+ response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
158
+ const chunk = `${CANARY_ENDLESS_CHUNK}${'A'.repeat(64 * 1024)}`;
159
+ const timer = setInterval(() => {
160
+ if (response.writableEnded || response.destroyed) {
161
+ clearInterval(timer);
162
+ timers.delete(timer);
163
+ return;
164
+ }
165
+ response.write(chunk);
166
+ }, 10);
167
+ timers.add(timer);
168
+ response.on('close', () => {
169
+ clearInterval(timer);
170
+ timers.delete(timer);
171
+ });
172
+ }
173
+ /** One byte per second. Only a deadline stops this. */
174
+ function slow(response, timers) {
175
+ response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
176
+ const timer = setInterval(() => {
177
+ if (response.writableEnded || response.destroyed) {
178
+ clearInterval(timer);
179
+ timers.delete(timer);
180
+ return;
181
+ }
182
+ response.write('.');
183
+ }, 1000);
184
+ timers.add(timer);
185
+ response.on('close', () => {
186
+ clearInterval(timer);
187
+ timers.delete(timer);
188
+ });
189
+ }
190
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/range/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Classes of IP address that matter to an SSRF control.
3
+ * Everything that is not `public` points somewhere a fetcher of user-supplied
4
+ * URLs has no business reaching.
5
+ */
6
+ export type AddressClass = 'loopback' | 'link-local' | 'private' | 'unique-local' | 'unspecified' | 'multicast' | 'broadcast' | 'public';
7
+ /** Cloud instance metadata lives here on AWS, GCP and Azure. */
8
+ export declare const METADATA_ADDRESS = "169.254.169.254";
9
+ export declare class NotAnIpAddressError extends Error {
10
+ readonly value: string;
11
+ constructor(value: string);
12
+ }
13
+ /**
14
+ * Classify one IP address literal.
15
+ *
16
+ * The caller passes an address, never a hostname: a hostname has no class
17
+ * until it is resolved, and resolving it is the caller's decision.
18
+ */
19
+ export declare function classifyAddress(address: string): AddressClass;
20
+ /** True when an address must not be reached by a fetcher of untrusted URLs. */
21
+ export declare function isForbiddenAddress(address: string): boolean;
@@ -0,0 +1,143 @@
1
+ import { BlockList, isIP } from 'node:net';
2
+ /** Cloud instance metadata lives here on AWS, GCP and Azure. */
3
+ export const METADATA_ADDRESS = '169.254.169.254';
4
+ export class NotAnIpAddressError extends Error {
5
+ value;
6
+ constructor(value) {
7
+ super(`not an IP address: ${value}`);
8
+ this.value = value;
9
+ this.name = 'NotAnIpAddressError';
10
+ }
11
+ }
12
+ /**
13
+ * Special-purpose ranges. The first match wins, so a range that sits inside
14
+ * another must come first: 255.255.255.255 is inside 240.0.0.0/4.
15
+ */
16
+ const RANGES = [
17
+ ['255.255.255.255/32', 'ipv4', 'broadcast'],
18
+ ['0.0.0.0/8', 'ipv4', 'unspecified'],
19
+ ['127.0.0.0/8', 'ipv4', 'loopback'],
20
+ ['169.254.0.0/16', 'ipv4', 'link-local'],
21
+ ['10.0.0.0/8', 'ipv4', 'private'],
22
+ ['172.16.0.0/12', 'ipv4', 'private'],
23
+ ['192.168.0.0/16', 'ipv4', 'private'],
24
+ // Carrier-grade NAT. Reachable inside many hosting networks.
25
+ ['100.64.0.0/10', 'ipv4', 'private'],
26
+ // IETF protocol assignments, documentation and benchmarking.
27
+ ['192.0.0.0/24', 'ipv4', 'private'],
28
+ ['192.0.2.0/24', 'ipv4', 'private'],
29
+ ['198.18.0.0/15', 'ipv4', 'private'],
30
+ ['198.51.100.0/24', 'ipv4', 'private'],
31
+ ['203.0.113.0/24', 'ipv4', 'private'],
32
+ ['224.0.0.0/4', 'ipv4', 'multicast'],
33
+ ['240.0.0.0/4', 'ipv4', 'private'],
34
+ ['::/128', 'ipv6', 'unspecified'],
35
+ ['::1/128', 'ipv6', 'loopback'],
36
+ // NAT64 for local use; the well-known prefix 64:ff9b::/96 is handled as an
37
+ // embedded IPv4 address instead.
38
+ ['64:ff9b:1::/48', 'ipv6', 'private'],
39
+ // Discard-only.
40
+ ['100::/64', 'ipv6', 'private'],
41
+ // IETF protocol assignments (Teredo, benchmarking, ...) and documentation,
42
+ // the IPv6 counterparts of the IPv4 ranges above.
43
+ ['2001::/23', 'ipv6', 'private'],
44
+ ['2001:db8::/32', 'ipv6', 'private'],
45
+ ['fc00::/7', 'ipv6', 'unique-local'],
46
+ ['fe80::/10', 'ipv6', 'link-local'],
47
+ // Deprecated site-local, the predecessor of fc00::/7.
48
+ ['fec0::/10', 'ipv6', 'private'],
49
+ ['ff00::/8', 'ipv6', 'multicast'],
50
+ ];
51
+ /**
52
+ * `BlockList` compares parsed numbers, not text, so no spelling of an address
53
+ * can slip past a pattern. That is the gap this file once shared with
54
+ * `ssrf-agent`: a regex on the text missed `0:0:0:0:0:ffff:7f00:1`.
55
+ */
56
+ const COMPILED = RANGES.map(([cidr, family, label]) => {
57
+ const [network = '', prefix = ''] = cidr.split('/');
58
+ const list = new BlockList();
59
+ list.addSubnet(network, Number(prefix), family);
60
+ return { family, label, list };
61
+ });
62
+ /**
63
+ * Classify one IP address literal.
64
+ *
65
+ * The caller passes an address, never a hostname: a hostname has no class
66
+ * until it is resolved, and resolving it is the caller's decision.
67
+ */
68
+ export function classifyAddress(address) {
69
+ const family = isIP(address);
70
+ if (family === 4)
71
+ return matchRange(address, 'ipv4');
72
+ if (family === 6)
73
+ return classifyIpv6(address);
74
+ throw new NotAnIpAddressError(address);
75
+ }
76
+ /** True when an address must not be reached by a fetcher of untrusted URLs. */
77
+ export function isForbiddenAddress(address) {
78
+ return classifyAddress(address) !== 'public';
79
+ }
80
+ function matchRange(address, family) {
81
+ for (const range of COMPILED) {
82
+ if (range.family === family && range.list.check(address, family))
83
+ return range.label;
84
+ }
85
+ return 'public';
86
+ }
87
+ function classifyIpv6(address) {
88
+ const { canonical, hextets } = parseIpv6(address);
89
+ // An address that carries an IPv4 address is judged by that IPv4 address,
90
+ // because that is where a connection to it can end up.
91
+ const embedded = embeddedIpv4(hextets);
92
+ if (embedded !== null)
93
+ return matchRange(embedded, 'ipv4');
94
+ return matchRange(canonical, 'ipv6');
95
+ }
96
+ /**
97
+ * Parse an IPv6 literal into its 8 groups.
98
+ *
99
+ * The WHATWG URL parser validates the address and writes it in one canonical
100
+ * form: lowercase, at most one `::`, and any dotted IPv4 tail rewritten in hex.
101
+ * A zone ID (`%eth0`) names an interface, not a different address.
102
+ */
103
+ function parseIpv6(address) {
104
+ const bare = address.split('%')[0] ?? '';
105
+ let canonical;
106
+ try {
107
+ canonical = new URL(`http://[${bare}]/`).hostname.slice(1, -1);
108
+ }
109
+ catch {
110
+ throw new NotAnIpAddressError(address);
111
+ }
112
+ const [head = '', tail] = canonical.split('::');
113
+ const left = head === '' ? [] : head.split(':');
114
+ const right = tail === undefined || tail === '' ? [] : tail.split(':');
115
+ const zeros = tail === undefined ? [] : Array(8 - left.length - right.length).fill('0');
116
+ const hextets = [...left, ...zeros, ...right].map((group) => Number.parseInt(group, 16));
117
+ return { canonical, hextets };
118
+ }
119
+ /**
120
+ * The IPv4 address inside an IPv6 address, for the forms that carry one.
121
+ *
122
+ * - `::ffff:a.b.c.d` IPv4-mapped
123
+ * - `::ffff:0:a.b.c.d` IPv4-translated
124
+ * - `::a.b.c.d` IPv4-compatible (deprecated); `::` and `::1` excluded
125
+ * - `64:ff9b::a.b.c.d` NAT64 well-known prefix
126
+ * - `2002:aabb:ccdd::` 6to4
127
+ */
128
+ function embeddedIpv4(h) {
129
+ const zero = (from, to) => h.slice(from, to).every((group) => group === 0);
130
+ const dotted = (high = 0, low = 0) => [high >> 8, high & 0xff, low >> 8, low & 0xff].join('.');
131
+ if (zero(0, 5) && h[5] === 0xffff)
132
+ return dotted(h[6], h[7]);
133
+ if (zero(0, 4) && h[4] === 0xffff && h[5] === 0)
134
+ return dotted(h[6], h[7]);
135
+ if (zero(0, 6) && !(h[6] === 0 && (h[7] ?? 0) <= 1))
136
+ return dotted(h[6], h[7]);
137
+ if (h[0] === 0x64 && h[1] === 0xff9b && zero(2, 6))
138
+ return dotted(h[6], h[7]);
139
+ if (h[0] === 0x2002)
140
+ return dotted(h[1], h[2]);
141
+ return null;
142
+ }
143
+ //# sourceMappingURL=addresses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addresses.js","sourceRoot":"","sources":["../../src/util/addresses.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { LookupFunction } from 'node:net';
2
+ /**
3
+ * A resolver that changes its answer between calls.
4
+ *
5
+ * This is DNS rebinding compressed into one process: the first answer is the
6
+ * one a pre-check sees, and every later answer is the one a socket gets.
7
+ *
8
+ * The rebinding here is SIMULATED. It tests the address used at connection
9
+ * time, not real DNS caching, TTL handling, or recursive resolvers.
10
+ */
11
+ export declare function flipLookup(answers: readonly string[]): LookupFunction;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A resolver that changes its answer between calls.
3
+ *
4
+ * This is DNS rebinding compressed into one process: the first answer is the
5
+ * one a pre-check sees, and every later answer is the one a socket gets.
6
+ *
7
+ * The rebinding here is SIMULATED. It tests the address used at connection
8
+ * time, not real DNS caching, TTL handling, or recursive resolvers.
9
+ */
10
+ export function flipLookup(answers) {
11
+ let call = 0;
12
+ return ((_hostname, options, callback) => {
13
+ const done = (typeof options === 'function' ? options : callback);
14
+ const index = Math.min(call, answers.length - 1);
15
+ call += 1;
16
+ const address = answers[index] ?? '127.0.0.1';
17
+ const family = address.includes(':') ? 6 : 4;
18
+ const wantsAll = typeof options === 'object' && options !== null && 'all' in options
19
+ ? Boolean(options.all)
20
+ : false;
21
+ // Match an asynchronous resolver. A synchronous callback can let an
22
+ // immediate connection failure surface before http.request's caller has
23
+ // attached its error listener, especially in a network-isolated test.
24
+ process.nextTick(() => {
25
+ if (wantsAll)
26
+ done(null, [{ address, family }]);
27
+ else
28
+ done(null, address, family);
29
+ });
30
+ });
31
+ }
32
+ //# sourceMappingURL=flipLookup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flipLookup.js","sourceRoot":"","sources":["../../src/util/flipLookup.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,IAAI,IAAI,GAAG,CAAC,CAAA;IAEZ,OAAO,CAAC,CAAC,SAAiB,EAAE,OAAgB,EAAE,QAAiB,EAAE,EAAE;QACjE,MAAM,IAAI,GAAG,CAAC,OAAO,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAIvD,CAAA;QAET,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAChD,IAAI,IAAI,CAAC,CAAA;QACT,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,WAAW,CAAA;QAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAE5C,MAAM,QAAQ,GAAG,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,OAAO;YAClF,CAAC,CAAC,OAAO,CAAE,OAA6B,CAAC,GAAG,CAAC;YAC7C,CAAC,CAAC,KAAK,CAAA;QAET,oEAAoE;QACpE,wEAAwE;QACxE,sEAAsE;QACtE,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE;YACpB,IAAI,QAAQ;gBAAE,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;;gBAC1C,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;QAClC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAmB,CAAA;AACtB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "ssrf-lab",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Test whether your Node.js fetcher actually blocks SSRF or just got lucky: a local attack range with an independent hit counter, for CI.",
5
+ "keywords": [
6
+ "ssrf",
7
+ "security",
8
+ "dns-rebinding",
9
+ "zip-bomb",
10
+ "http-client",
11
+ "testing",
12
+ "ssrf-protection",
13
+ "security-testing",
14
+ "appsec",
15
+ "webhook",
16
+ "link-preview",
17
+ "github-actions"
18
+ ],
19
+ "license": "MIT",
20
+ "author": "Roshan Amble",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/roshan-amble/ssrf-lab.git"
24
+ },
25
+ "homepage": "https://github.com/roshan-amble/ssrf-lab#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/roshan-amble/ssrf-lab/issues"
28
+ },
29
+ "type": "module",
30
+ "engines": {
31
+ "node": ">=22.12.0"
32
+ },
33
+ "bin": {
34
+ "ssrf-lab": "./dist/cli.js"
35
+ },
36
+ "main": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "files": [
39
+ "dist",
40
+ "!dist/**/*.test.*"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.json",
44
+ "typecheck": "tsc -p tsconfig.json --noEmit",
45
+ "test": "vitest run",
46
+ "scan": "tsx src/cli.ts",
47
+ "prepack": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && npm run build"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "22.20.2",
51
+ "guarded-fetch": "0.1.5",
52
+ "request-filtering-agent": "3.2.1",
53
+ "ssrf-agent": "1.0.5",
54
+ "ssrf-req-filter": "1.1.1",
55
+ "tsx": "4.23.13",
56
+ "typescript": "5.9.3",
57
+ "vitest": "5.0.3"
58
+ }
59
+ }