@data-fair/lib-node 2.14.0 → 2.15.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/events-queue.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { internalError } from '@data-fair/lib-node/observer.js';
2
- import axios from './axios.js';
2
+ // the events service is part of our own infrastructure (configuration, not user input)
3
+ import { privateAxiosInstance as axios } from './axios.js';
3
4
  import Debug from 'debug';
4
5
  const debug = Debug('events-queue');
5
6
  export class EventsQueue {
@@ -1 +1 @@
1
- {"version":3,"file":"events-queue.js","sourceRoot":"","sources":["events-queue.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Event } from '@data-fair/lib-common-types/event/index.js'\nimport { type Notification } from '@data-fair/lib-common-types/notification/index.js'\nimport { internalError } from '@data-fair/lib-node/observer.js'\nimport axios from './axios.js'\nimport Debug from 'debug'\nimport { SessionState } from '@data-fair/lib-common-types/session/index.js'\n\nconst debug = Debug('events-queue')\n\nexport type EventsQueueOptions = {\n eventsUrl: string,\n eventsSecret: string,\n inactive?: boolean\n}\n\nexport type PushEvent = Omit<Event, 'date'>\nexport type PushNotification = Omit<Notification, 'date'>\n\nexport class EventsQueue {\n _options: EventsQueueOptions | undefined\n eventsQueue: Event[] = []\n notificationsQueue: Notification[] = []\n stopped = false\n currentDrainPromise: Promise<void> | undefined\n\n options () {\n if (!this._options) throw new Error('events queue was not initialized')\n return this._options\n }\n\n start = async (options: EventsQueueOptions) => {\n if (this._options) return // already initialized\n this._options = options\n this.loop()\n }\n\n stop = async () => {\n this.stopped = true\n if (this.currentDrainPromise) await this.currentDrainPromise\n await this.drain()\n }\n\n async loop () {\n while (!this.stopped) {\n await new Promise(resolve => setTimeout(resolve, 3000))\n this.currentDrainPromise = this.drain()\n await this.currentDrainPromise\n this.currentDrainPromise = undefined\n }\n }\n\n async drain () {\n if (this.eventsQueue.length) {\n const events = this.eventsQueue\n this.eventsQueue = []\n debug('drain events', events.length)\n try {\n await axios.post(this.options().eventsUrl + '/api/events', events, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-event', err)\n // retry later\n if (err.status >= 500 && events.length < 1000) this.eventsQueue = events.concat(this.eventsQueue)\n }\n }\n if (this.notificationsQueue.length) {\n const notifications = this.notificationsQueue\n this.notificationsQueue = []\n debug('drain notifications', notifications.length)\n for (const notification of notifications) {\n try {\n await axios.post(this.options().eventsUrl + '/api/notifications', notification, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-notif', err)\n // retry later\n if (err.status >= 500 && (notifications.length + this.notificationsQueue.length) < 1000) {\n this.notificationsQueue.unshift(notification)\n }\n }\n }\n }\n }\n\n pushEvent (event: PushEvent, sessionState?: SessionState) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('events queue has been stopped');\n (event as Event).date = new Date().toISOString()\n debug('pushEvent', event)\n if (!event.originator && sessionState?.user && sessionState.account) {\n if (sessionState.user.asAdmin) {\n event.originator = {\n user: { ...sessionState.user.asAdmin, admin: true }\n }\n } else if (sessionState.user.adminMode && (sessionState.account.type !== event.sender?.type || sessionState.account.id !== event.sender?.id)) {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name, admin: true }\n }\n } else {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name }\n }\n if (sessionState.organization) {\n event.originator.organization = {\n id: sessionState.organization.id,\n name: sessionState.organization.name,\n department: sessionState.organization.department,\n departmentName: sessionState.organization.departmentName\n }\n }\n }\n }\n this.eventsQueue.push(event as Event)\n }\n\n pushNotification (notification: PushNotification) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('notifications queue has been stopped');\n (notification as Notification).date = new Date().toISOString()\n debug('pushNotification', notification)\n this.notificationsQueue.push(notification as Notification)\n }\n}\n\nconst eventsQueue = new EventsQueue()\n\nexport default eventsQueue\n"]}
1
+ {"version":3,"file":"events-queue.js","sourceRoot":"","sources":["events-queue.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Event } from '@data-fair/lib-common-types/event/index.js'\nimport { type Notification } from '@data-fair/lib-common-types/notification/index.js'\nimport { internalError } from '@data-fair/lib-node/observer.js'\n// the events service is part of our own infrastructure (configuration, not user input)\nimport { privateAxiosInstance as axios } from './axios.js'\nimport Debug from 'debug'\nimport { SessionState } from '@data-fair/lib-common-types/session/index.js'\n\nconst debug = Debug('events-queue')\n\nexport type EventsQueueOptions = {\n eventsUrl: string,\n eventsSecret: string,\n inactive?: boolean\n}\n\nexport type PushEvent = Omit<Event, 'date'>\nexport type PushNotification = Omit<Notification, 'date'>\n\nexport class EventsQueue {\n _options: EventsQueueOptions | undefined\n eventsQueue: Event[] = []\n notificationsQueue: Notification[] = []\n stopped = false\n currentDrainPromise: Promise<void> | undefined\n\n options () {\n if (!this._options) throw new Error('events queue was not initialized')\n return this._options\n }\n\n start = async (options: EventsQueueOptions) => {\n if (this._options) return // already initialized\n this._options = options\n this.loop()\n }\n\n stop = async () => {\n this.stopped = true\n if (this.currentDrainPromise) await this.currentDrainPromise\n await this.drain()\n }\n\n async loop () {\n while (!this.stopped) {\n await new Promise(resolve => setTimeout(resolve, 3000))\n this.currentDrainPromise = this.drain()\n await this.currentDrainPromise\n this.currentDrainPromise = undefined\n }\n }\n\n async drain () {\n if (this.eventsQueue.length) {\n const events = this.eventsQueue\n this.eventsQueue = []\n debug('drain events', events.length)\n try {\n await axios.post(this.options().eventsUrl + '/api/events', events, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-event', err)\n // retry later\n if (err.status >= 500 && events.length < 1000) this.eventsQueue = events.concat(this.eventsQueue)\n }\n }\n if (this.notificationsQueue.length) {\n const notifications = this.notificationsQueue\n this.notificationsQueue = []\n debug('drain notifications', notifications.length)\n for (const notification of notifications) {\n try {\n await axios.post(this.options().eventsUrl + '/api/notifications', notification, { headers: { 'x-secret-key': this.options().eventsSecret } })\n } catch (err: any) {\n internalError('events-queue-push-notif', err)\n // retry later\n if (err.status >= 500 && (notifications.length + this.notificationsQueue.length) < 1000) {\n this.notificationsQueue.unshift(notification)\n }\n }\n }\n }\n }\n\n pushEvent (event: PushEvent, sessionState?: SessionState) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('events queue has been stopped');\n (event as Event).date = new Date().toISOString()\n debug('pushEvent', event)\n if (!event.originator && sessionState?.user && sessionState.account) {\n if (sessionState.user.asAdmin) {\n event.originator = {\n user: { ...sessionState.user.asAdmin, admin: true }\n }\n } else if (sessionState.user.adminMode && (sessionState.account.type !== event.sender?.type || sessionState.account.id !== event.sender?.id)) {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name, admin: true }\n }\n } else {\n event.originator = {\n user: { id: sessionState.user.id, name: sessionState.user.name }\n }\n if (sessionState.organization) {\n event.originator.organization = {\n id: sessionState.organization.id,\n name: sessionState.organization.name,\n department: sessionState.organization.department,\n departmentName: sessionState.organization.departmentName\n }\n }\n }\n }\n this.eventsQueue.push(event as Event)\n }\n\n pushNotification (notification: PushNotification) {\n const options = this.options()\n if (options.inactive) return\n if (this.stopped) throw new Error('notifications queue has been stopped');\n (notification as Notification).date = new Date().toISOString()\n debug('pushNotification', notification)\n this.notificationsQueue.push(notification as Notification)\n }\n}\n\nconst eventsQueue = new EventsQueue()\n\nexport default eventsQueue\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import { strict as assert } from 'assert';
3
+ import http from 'node:http';
4
+ import { EventsQueue } from './events-queue.js';
5
+ describe('events queue', () => {
6
+ let server;
7
+ let port;
8
+ const received = [];
9
+ before(async () => {
10
+ server = http.createServer((req, res) => {
11
+ let body = '';
12
+ req.on('data', chunk => { body += chunk; });
13
+ req.on('end', () => {
14
+ received.push({ url: req.url, body: JSON.parse(body) });
15
+ res.end();
16
+ });
17
+ });
18
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
19
+ port = server.address().port;
20
+ });
21
+ after(() => { server.close(); });
22
+ // the events service is configured, not user provided: it must stay reachable on a private address
23
+ // even though the default http agents refuse those (SSRF protection)
24
+ it('should push events to a private events service', async () => {
25
+ assert.ok(!process.env.SSRF_PUBLIC_IPS, 'the test must run without SSRF_PUBLIC_IPS');
26
+ const queue = new EventsQueue();
27
+ queue._options = { eventsUrl: `http://127.0.0.1:${port}`, eventsSecret: 'secret' };
28
+ queue.pushEvent({ title: 'test event', topic: { key: 'test' }, sender: { type: 'user', id: 'user1' } });
29
+ await queue.drain();
30
+ assert.equal(received.length, 1);
31
+ assert.equal(received[0].url, '/api/events');
32
+ assert.equal(received[0].body[0].title, 'test event');
33
+ });
34
+ });
35
+ //# sourceMappingURL=events-queue.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-queue.test.js","sourceRoot":"","sources":["events-queue.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AACvD,OAAO,EAAE,MAAM,IAAI,MAAM,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,IAAI,MAAmB,CAAA;IACvB,IAAI,IAAY,CAAA;IAChB,MAAM,QAAQ,GAAkC,EAAE,CAAA;IAElD,MAAM,CAAC,KAAK,IAAI,EAAE;QAChB,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACtC,IAAI,IAAI,GAAG,EAAE,CAAA;YACb,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,IAAI,IAAI,KAAK,CAAA,CAAC,CAAC,CAAC,CAAA;YAC1C,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;gBACjB,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gBACvD,GAAG,CAAC,GAAG,EAAE,CAAA;YACX,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QACF,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAA;QAC1E,IAAI,GAAI,MAAM,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAA;IAC/C,CAAC,CAAC,CAAA;IAEF,KAAK,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,CAAA,CAAC,CAAC,CAAC,CAAA;IAE/B,mGAAmG;IACnG,qEAAqE;IACrE,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,2CAA2C,CAAC,CAAA;QACpF,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAA;QAC/B,KAAK,CAAC,QAAQ,GAAG,EAAE,SAAS,EAAE,oBAAoB,IAAI,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAA;QAClF,KAAK,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAS,CAAC,CAAA;QAC9G,MAAM,KAAK,CAAC,KAAK,EAAE,CAAA;QACnB,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;QAChC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAC5C,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,YAAY,CAAC,CAAA;IACvD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { describe, it, before, after } from 'node:test'\nimport { strict as assert } from 'assert'\nimport http from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport { EventsQueue } from './events-queue.js'\n\ndescribe('events queue', () => {\n let server: http.Server\n let port: number\n const received: { url?: string, body: any }[] = []\n\n before(async () => {\n server = http.createServer((req, res) => {\n let body = ''\n req.on('data', chunk => { body += chunk })\n req.on('end', () => {\n received.push({ url: req.url, body: JSON.parse(body) })\n res.end()\n })\n })\n await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))\n port = (server.address() as AddressInfo).port\n })\n\n after(() => { server.close() })\n\n // the events service is configured, not user provided: it must stay reachable on a private address\n // even though the default http agents refuse those (SSRF protection)\n it('should push events to a private events service', async () => {\n assert.ok(!process.env.SSRF_PUBLIC_IPS, 'the test must run without SSRF_PUBLIC_IPS')\n const queue = new EventsQueue()\n queue._options = { eventsUrl: `http://127.0.0.1:${port}`, eventsSecret: 'secret' }\n queue.pushEvent({ title: 'test event', topic: { key: 'test' }, sender: { type: 'user', id: 'user1' } } as any)\n await queue.drain()\n assert.equal(received.length, 1)\n assert.equal(received[0].url, '/api/events')\n assert.equal(received[0].body[0].title, 'test event')\n })\n})\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-fair/lib-node",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "Small nodejs related utility functions for the data-fair stack.",
5
5
  "main": "index.js",
6
6
  "files": [
package/ssrf.d.ts CHANGED
@@ -19,6 +19,19 @@ export declare const getSsrfRules: (env?: NodeJS.ProcessEnv) => SsrfRules;
19
19
  * hostnames are not checked here, only the addresses they resolve to
20
20
  */
21
21
  export declare const checkAddress: (address: string, host: string | undefined, rules: SsrfRules) => SsrfError | undefined;
22
+ /**
23
+ * Resolve a host and check all its addresses, returns the address to connect to.
24
+ * Connect to the returned address (with the original host as TLS servername), not to the host:
25
+ * resolving the host again could give a different address (DNS rebinding).
26
+ * Throws a SsrfError if the host is or resolves to a non public address.
27
+ */
28
+ export declare const resolvePublicAddress: (host: string, rules?: SsrfRules) => Promise<string>;
29
+ /**
30
+ * A dns.lookup compatible function that refuses non public addresses, for any client accepting a
31
+ * `lookup` option (net.connect, tls.connect, undici connect options, got dnsLookup...).
32
+ * WARNING: node does not call lookup for IP literals, check those with resolvePublicAddress.
33
+ */
34
+ export declare const publicLookup: (hostname: string, options: any, callback?: (...args: any[]) => void) => void;
22
35
  export declare class SsrfHttpAgent extends HttpAgent {
23
36
  constructor(opts: HttpAgent.HttpOptions, rules: SsrfRules);
24
37
  }
package/ssrf.js CHANGED
@@ -82,6 +82,46 @@ const guardLookup = (lookup, rules) => {
82
82
  });
83
83
  };
84
84
  };
85
+ // helpers for clients that do not use our http agents (ftp, sftp/ssh2, raw sockets, SDKs, undici...)
86
+ // unlike the agents they are not disabled by a proxy env variable: these clients connect directly
87
+ let envRules;
88
+ const getEnvRules = () => {
89
+ envRules = envRules ?? getSsrfRules();
90
+ return envRules;
91
+ };
92
+ /**
93
+ * Resolve a host and check all its addresses, returns the address to connect to.
94
+ * Connect to the returned address (with the original host as TLS servername), not to the host:
95
+ * resolving the host again could give a different address (DNS rebinding).
96
+ * Throws a SsrfError if the host is or resolves to a non public address.
97
+ */
98
+ export const resolvePublicAddress = async (host, rules = getEnvRules()) => {
99
+ const bareHost = host.replace(/^\[(.*)\]$/, '$1');
100
+ if (net.isIP(bareHost)) {
101
+ const ssrfError = checkAddress(bareHost, undefined, rules);
102
+ if (ssrfError)
103
+ throw ssrfError;
104
+ return bareHost;
105
+ }
106
+ const addresses = await dns.promises.lookup(bareHost, { all: true });
107
+ for (const { address } of addresses) {
108
+ const ssrfError = checkAddress(address, bareHost, rules);
109
+ if (ssrfError)
110
+ throw ssrfError;
111
+ }
112
+ return addresses[0].address;
113
+ };
114
+ /**
115
+ * A dns.lookup compatible function that refuses non public addresses, for any client accepting a
116
+ * `lookup` option (net.connect, tls.connect, undici connect options, got dnsLookup...).
117
+ * WARNING: node does not call lookup for IP literals, check those with resolvePublicAddress.
118
+ */
119
+ export const publicLookup = (hostname, options, callback) => {
120
+ // support the dns.lookup(hostname, callback) signature
121
+ if (typeof options === 'function')
122
+ return publicLookup(hostname, {}, options);
123
+ return guardLookup(dns.lookup, getEnvRules())(hostname, options ?? {}, callback);
124
+ };
85
125
  // the check happens when the connection is created, on the exact address that will be used:
86
126
  // IP literals are checked directly (no DNS lookup happens for them),
87
127
  // hostnames are checked on every address returned by the lookup (no DNS rebinding),
package/ssrf.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ssrf.js","sourceRoot":"","sources":["ssrf.ts"],"names":[],"mappings":"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","sourcesContent":["// protection against server side request forgery: refuse to connect to non public addresses\n// the address checks are adapted from https://github.com/azu/request-filtering-agent\n// they are applied on top of our keepalive agents instead of node's default agent\n\nimport net from 'node:net'\nimport dns from 'node:dns'\nimport ipaddr from 'ipaddr.js'\nimport HttpAgent, { HttpsAgent } from 'agentkeepalive'\n\ntype IPRange = [ipaddr.IPv4 | ipaddr.IPv6, number]\n\nexport type SsrfRules = {\n publicIPs: IPRange[],\n privateIPs: IPRange[]\n}\n\nexport class SsrfError extends Error {\n code = 'ERR_SSRF_BLOCKED'\n constructor (message: string) {\n super(message)\n this.name = 'SsrfError'\n }\n}\n\n/**\n * Parse a comma separated list of IP addresses and CIDR ranges, as found in SSRF_PUBLIC_IPS and SSRF_PRIVATE_IPS\n */\nexport const parseIPList = (value: string | undefined, name: string): IPRange[] => {\n if (!value) return []\n return value.split(',').map(s => s.trim()).filter(Boolean).map(s => {\n try {\n if (net.isIP(s)) {\n const addr = ipaddr.process(s)\n return [addr, addr.kind() === 'ipv4' ? 32 : 128] as IPRange\n }\n const [addr, bits] = ipaddr.parseCIDR(s)\n if (addr.kind() === 'ipv6' && (addr as ipaddr.IPv6).isIPv4MappedAddress()) {\n return [(addr as ipaddr.IPv6).toIPv4Address(), bits - 96] as IPRange\n }\n return [addr, bits] as IPRange\n } catch (err) {\n throw new Error(`invalid IP address or CIDR range \"${s}\" in ${name}`, { cause: err })\n }\n })\n}\n\nexport const getSsrfRules = (env: NodeJS.ProcessEnv = process.env): SsrfRules => ({\n publicIPs: parseIPList(env.SSRF_PUBLIC_IPS, 'SSRF_PUBLIC_IPS'),\n privateIPs: parseIPList(env.SSRF_PRIVATE_IPS, 'SSRF_PRIVATE_IPS')\n})\n\nconst matchList = (addr: ipaddr.IPv4 | ipaddr.IPv6, list: IPRange[]) => {\n return list.some(([range, bits]) => {\n if (addr.kind() !== range.kind()) return false\n if (addr.kind() === 'ipv4') return (addr as ipaddr.IPv4).match(range as ipaddr.IPv4, bits)\n return (addr as ipaddr.IPv6).match(range as ipaddr.IPv6, bits)\n })\n}\n\n/**\n * Check that an address is public, returns an error if it is not\n * hostnames are not checked here, only the addresses they resolve to\n */\nexport const checkAddress = (address: string, host: string | undefined, rules: SsrfRules): SsrfError | undefined => {\n if (!net.isIP(address)) return\n // ipv4 mapped ipv6 addresses (::ffff:127.0.0.1) are checked as the ipv4 address they represent\n const addr = ipaddr.process(address)\n const target = host && host !== address ? `${host} (${addr.toString()})` : addr.toString()\n if (matchList(addr, rules.publicIPs)) return\n if (addr.range() !== 'unicast') {\n return new SsrfError(`connection to ${target} refused, it is not a public address (${addr.range()}), if this target is legitimate add it to SSRF_PUBLIC_IPS`)\n }\n if (matchList(addr, rules.privateIPs)) {\n return new SsrfError(`connection to ${target} refused, the address is declared private in SSRF_PRIVATE_IPS`)\n }\n}\n\ntype LookupFunction = (hostname: string, options: any, callback: (...args: any[]) => void) => void\n\nconst guardLookup = (lookup: LookupFunction, rules: SsrfRules): LookupFunction => {\n return (hostname, options, callback) => {\n lookup(hostname, options, (err: Error | null, address: string | dns.LookupAddress[], family?: number) => {\n if (err) return callback(err)\n const addresses = Array.isArray(address) ? address.map(a => a.address) : [address]\n for (const a of addresses) {\n const ssrfError = checkAddress(a, hostname, rules)\n if (ssrfError) return callback(ssrfError)\n }\n callback(null, address, family)\n })\n }\n}\n\n// the check happens when the connection is created, on the exact address that will be used:\n// IP literals are checked directly (no DNS lookup happens for them),\n// hostnames are checked on every address returned by the lookup (no DNS rebinding),\n// and each redirection creates a new connection that is checked again\nconst guardedCreateConnection = (rules: SsrfRules, createConnection: (options: any, oncreate: any) => any) => {\n return (options: any, oncreate: (err: Error | null, socket?: net.Socket) => void) => {\n const host = options.host ?? options.hostname\n if (host) {\n // brackets of ipv6 literals are removed by node before reaching the agent, but be defensive\n const ssrfError = checkAddress(host.replace(/^\\[(.*)\\]$/, '$1'), undefined, rules)\n if (ssrfError) {\n oncreate(ssrfError)\n return\n }\n }\n return createConnection({ ...options, lookup: guardLookup(options.lookup ?? dns.lookup, rules) }, oncreate)\n }\n}\n\nexport class SsrfHttpAgent extends HttpAgent {\n constructor (opts: HttpAgent.HttpOptions, rules: SsrfRules) {\n super(opts)\n // @ts-ignore createConnection is not declared in the agentkeepalive typings\n this.createConnection = guardedCreateConnection(rules, super.createConnection.bind(this))\n }\n}\n\nexport class SsrfHttpsAgent extends HttpsAgent {\n constructor (opts: HttpAgent.HttpsOptions, rules: SsrfRules) {\n super(opts)\n // @ts-ignore createConnection is not declared in the agentkeepalive typings\n this.createConnection = guardedCreateConnection(rules, super.createConnection.bind(this))\n }\n}\n\nconst proxyEnvNames = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy']\n\n/**\n * The name of the proxy env variable that disables the SSRF protection, if any\n * behind a proxy our agents connect to the proxy, not to the target, so they cannot check the target\n */\nexport const getProxyEnvName = (env: NodeJS.ProcessEnv = process.env) => proxyEnvNames.find(name => !!env[name])\n"]}
1
+ {"version":3,"file":"ssrf.js","sourceRoot":"","sources":["ssrf.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,qFAAqF;AACrF,kFAAkF;AAElF,OAAO,GAAG,MAAM,UAAU,CAAA;AAC1B,OAAO,GAAG,MAAM,UAAU,CAAA;AAC1B,OAAO,MAAM,MAAM,WAAW,CAAA;AAC9B,OAAO,SAAS,EAAE,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAStD,MAAM,OAAO,SAAU,SAAQ,KAAK;IAClC,IAAI,GAAG,kBAAkB,CAAA;IACzB,YAAa,OAAe;QAC1B,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,WAAW,CAAA;IACzB,CAAC;CACF;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAyB,EAAE,IAAY,EAAa,EAAE;IAChF,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAA;IACrB,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QACjE,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;gBAC9B,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAY,CAAA;YAC7D,CAAC;YACD,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;YACxC,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM,IAAK,IAAoB,CAAC,mBAAmB,EAAE,EAAE,CAAC;gBAC1E,OAAO,CAAE,IAAoB,CAAC,aAAa,EAAE,EAAE,IAAI,GAAG,EAAE,CAAY,CAAA;YACtE,CAAC;YACD,OAAO,CAAC,IAAI,EAAE,IAAI,CAAY,CAAA;QAChC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,QAAQ,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;QACvF,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAa,EAAE,CAAC,CAAC;IAChF,SAAS,EAAE,WAAW,CAAC,GAAG,CAAC,eAAe,EAAE,iBAAiB,CAAC;IAC9D,UAAU,EAAE,WAAW,CAAC,GAAG,CAAC,gBAAgB,EAAE,kBAAkB,CAAC;CAClE,CAAC,CAAA;AAEF,MAAM,SAAS,GAAG,CAAC,IAA+B,EAAE,IAAe,EAAE,EAAE;IACrE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE;QACjC,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,CAAC,IAAI,EAAE;YAAE,OAAO,KAAK,CAAA;QAC9C,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM;YAAE,OAAQ,IAAoB,CAAC,KAAK,CAAC,KAAoB,EAAE,IAAI,CAAC,CAAA;QAC1F,OAAQ,IAAoB,CAAC,KAAK,CAAC,KAAoB,EAAE,IAAI,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAe,EAAE,IAAwB,EAAE,KAAgB,EAAyB,EAAE;IACjH,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAM;IAC9B,+FAA+F;IAC/F,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACpC,MAAM,MAAM,GAAG,IAAI,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAA;IAC1F,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC;QAAE,OAAM;IAC5C,IAAI,IAAI,CAAC,KAAK,EAAE,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,SAAS,CAAC,iBAAiB,MAAM,yCAAyC,IAAI,CAAC,KAAK,EAAE,2DAA2D,CAAC,CAAA;IAC/J,CAAC;IACD,IAAI,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,SAAS,CAAC,iBAAiB,MAAM,+DAA+D,CAAC,CAAA;IAC9G,CAAC;AACH,CAAC,CAAA;AAID,MAAM,WAAW,GAAG,CAAC,MAAsB,EAAE,KAAgB,EAAkB,EAAE;IAC/E,OAAO,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;QACrC,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,GAAiB,EAAE,OAAqC,EAAE,MAAe,EAAE,EAAE;YACtG,IAAI,GAAG;gBAAE,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAA;YAC7B,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;YAClF,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;gBAC1B,MAAM,SAAS,GAAG,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAA;gBAClD,IAAI,SAAS;oBAAE,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAA;YAC3C,CAAC;YACD,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;QACjC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC,CAAA;AAED,qGAAqG;AACrG,kGAAkG;AAElG,IAAI,QAA+B,CAAA;AACnC,MAAM,WAAW,GAAG,GAAG,EAAE;IACvB,QAAQ,GAAG,QAAQ,IAAI,YAAY,EAAE,CAAA;IACrC,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EAAE,IAAY,EAAE,QAAmB,WAAW,EAAE,EAAmB,EAAE;IAC5G,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAA;IACjD,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,YAAY,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;QAC1D,IAAI,SAAS;YAAE,MAAM,SAAS,CAAA;QAC9B,OAAO,QAAQ,CAAA;IACjB,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;IACpE,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,SAAS,EAAE,CAAC;QACpC,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAA;QACxD,IAAI,SAAS;YAAE,MAAM,SAAS,CAAA;IAChC,CAAC;IACD,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;AAC7B,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAE,OAAY,EAAE,QAAmC,EAAQ,EAAE;IACxG,uDAAuD;IACvD,IAAI,OAAO,OAAO,KAAK,UAAU;QAAE,OAAO,YAAY,CAAC,QAAQ,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;IAC7E,OAAO,WAAW,CAAC,GAAG,CAAC,MAAwB,EAAE,WAAW,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,IAAI,EAAE,EAAE,QAAoC,CAAC,CAAA;AAChI,CAAC,CAAA;AAED,4FAA4F;AAC5F,qEAAqE;AACrE,oFAAoF;AACpF,sEAAsE;AACtE,MAAM,uBAAuB,GAAG,CAAC,KAAgB,EAAE,gBAAsD,EAAE,EAAE;IAC3G,OAAO,CAAC,OAAY,EAAE,QAA0D,EAAE,EAAE;QAClF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAA;QAC7C,IAAI,IAAI,EAAE,CAAC;YACT,4FAA4F;YAC5F,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;YAClF,IAAI,SAAS,EAAE,CAAC;gBACd,QAAQ,CAAC,SAAS,CAAC,CAAA;gBACnB,OAAM;YACR,CAAC;QACH,CAAC;QACD,OAAO,gBAAgB,CAAC,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAA;IAC7G,CAAC,CAAA;AACH,CAAC,CAAA;AAED,MAAM,OAAO,aAAc,SAAQ,SAAS;IAC1C,YAAa,IAA2B,EAAE,KAAgB;QACxD,KAAK,CAAC,IAAI,CAAC,CAAA;QACX,4EAA4E;QAC5E,IAAI,CAAC,gBAAgB,GAAG,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3F,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,UAAU;IAC5C,YAAa,IAA4B,EAAE,KAAgB;QACzD,KAAK,CAAC,IAAI,CAAC,CAAA;QACX,4EAA4E;QAC5E,IAAI,CAAC,gBAAgB,GAAG,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3F,CAAC;CACF;AAED,MAAM,aAAa,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,CAAC,CAAA;AAE1G;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA","sourcesContent":["// protection against server side request forgery: refuse to connect to non public addresses\n// the address checks are adapted from https://github.com/azu/request-filtering-agent\n// they are applied on top of our keepalive agents instead of node's default agent\n\nimport net from 'node:net'\nimport dns from 'node:dns'\nimport ipaddr from 'ipaddr.js'\nimport HttpAgent, { HttpsAgent } from 'agentkeepalive'\n\ntype IPRange = [ipaddr.IPv4 | ipaddr.IPv6, number]\n\nexport type SsrfRules = {\n publicIPs: IPRange[],\n privateIPs: IPRange[]\n}\n\nexport class SsrfError extends Error {\n code = 'ERR_SSRF_BLOCKED'\n constructor (message: string) {\n super(message)\n this.name = 'SsrfError'\n }\n}\n\n/**\n * Parse a comma separated list of IP addresses and CIDR ranges, as found in SSRF_PUBLIC_IPS and SSRF_PRIVATE_IPS\n */\nexport const parseIPList = (value: string | undefined, name: string): IPRange[] => {\n if (!value) return []\n return value.split(',').map(s => s.trim()).filter(Boolean).map(s => {\n try {\n if (net.isIP(s)) {\n const addr = ipaddr.process(s)\n return [addr, addr.kind() === 'ipv4' ? 32 : 128] as IPRange\n }\n const [addr, bits] = ipaddr.parseCIDR(s)\n if (addr.kind() === 'ipv6' && (addr as ipaddr.IPv6).isIPv4MappedAddress()) {\n return [(addr as ipaddr.IPv6).toIPv4Address(), bits - 96] as IPRange\n }\n return [addr, bits] as IPRange\n } catch (err) {\n throw new Error(`invalid IP address or CIDR range \"${s}\" in ${name}`, { cause: err })\n }\n })\n}\n\nexport const getSsrfRules = (env: NodeJS.ProcessEnv = process.env): SsrfRules => ({\n publicIPs: parseIPList(env.SSRF_PUBLIC_IPS, 'SSRF_PUBLIC_IPS'),\n privateIPs: parseIPList(env.SSRF_PRIVATE_IPS, 'SSRF_PRIVATE_IPS')\n})\n\nconst matchList = (addr: ipaddr.IPv4 | ipaddr.IPv6, list: IPRange[]) => {\n return list.some(([range, bits]) => {\n if (addr.kind() !== range.kind()) return false\n if (addr.kind() === 'ipv4') return (addr as ipaddr.IPv4).match(range as ipaddr.IPv4, bits)\n return (addr as ipaddr.IPv6).match(range as ipaddr.IPv6, bits)\n })\n}\n\n/**\n * Check that an address is public, returns an error if it is not\n * hostnames are not checked here, only the addresses they resolve to\n */\nexport const checkAddress = (address: string, host: string | undefined, rules: SsrfRules): SsrfError | undefined => {\n if (!net.isIP(address)) return\n // ipv4 mapped ipv6 addresses (::ffff:127.0.0.1) are checked as the ipv4 address they represent\n const addr = ipaddr.process(address)\n const target = host && host !== address ? `${host} (${addr.toString()})` : addr.toString()\n if (matchList(addr, rules.publicIPs)) return\n if (addr.range() !== 'unicast') {\n return new SsrfError(`connection to ${target} refused, it is not a public address (${addr.range()}), if this target is legitimate add it to SSRF_PUBLIC_IPS`)\n }\n if (matchList(addr, rules.privateIPs)) {\n return new SsrfError(`connection to ${target} refused, the address is declared private in SSRF_PRIVATE_IPS`)\n }\n}\n\ntype LookupFunction = (hostname: string, options: any, callback: (...args: any[]) => void) => void\n\nconst guardLookup = (lookup: LookupFunction, rules: SsrfRules): LookupFunction => {\n return (hostname, options, callback) => {\n lookup(hostname, options, (err: Error | null, address: string | dns.LookupAddress[], family?: number) => {\n if (err) return callback(err)\n const addresses = Array.isArray(address) ? address.map(a => a.address) : [address]\n for (const a of addresses) {\n const ssrfError = checkAddress(a, hostname, rules)\n if (ssrfError) return callback(ssrfError)\n }\n callback(null, address, family)\n })\n }\n}\n\n// helpers for clients that do not use our http agents (ftp, sftp/ssh2, raw sockets, SDKs, undici...)\n// unlike the agents they are not disabled by a proxy env variable: these clients connect directly\n\nlet envRules: SsrfRules | undefined\nconst getEnvRules = () => {\n envRules = envRules ?? getSsrfRules()\n return envRules\n}\n\n/**\n * Resolve a host and check all its addresses, returns the address to connect to.\n * Connect to the returned address (with the original host as TLS servername), not to the host:\n * resolving the host again could give a different address (DNS rebinding).\n * Throws a SsrfError if the host is or resolves to a non public address.\n */\nexport const resolvePublicAddress = async (host: string, rules: SsrfRules = getEnvRules()): Promise<string> => {\n const bareHost = host.replace(/^\\[(.*)\\]$/, '$1')\n if (net.isIP(bareHost)) {\n const ssrfError = checkAddress(bareHost, undefined, rules)\n if (ssrfError) throw ssrfError\n return bareHost\n }\n const addresses = await dns.promises.lookup(bareHost, { all: true })\n for (const { address } of addresses) {\n const ssrfError = checkAddress(address, bareHost, rules)\n if (ssrfError) throw ssrfError\n }\n return addresses[0].address\n}\n\n/**\n * A dns.lookup compatible function that refuses non public addresses, for any client accepting a\n * `lookup` option (net.connect, tls.connect, undici connect options, got dnsLookup...).\n * WARNING: node does not call lookup for IP literals, check those with resolvePublicAddress.\n */\nexport const publicLookup = (hostname: string, options: any, callback?: (...args: any[]) => void): void => {\n // support the dns.lookup(hostname, callback) signature\n if (typeof options === 'function') return publicLookup(hostname, {}, options)\n return guardLookup(dns.lookup as LookupFunction, getEnvRules())(hostname, options ?? {}, callback as (...args: any[]) => void)\n}\n\n// the check happens when the connection is created, on the exact address that will be used:\n// IP literals are checked directly (no DNS lookup happens for them),\n// hostnames are checked on every address returned by the lookup (no DNS rebinding),\n// and each redirection creates a new connection that is checked again\nconst guardedCreateConnection = (rules: SsrfRules, createConnection: (options: any, oncreate: any) => any) => {\n return (options: any, oncreate: (err: Error | null, socket?: net.Socket) => void) => {\n const host = options.host ?? options.hostname\n if (host) {\n // brackets of ipv6 literals are removed by node before reaching the agent, but be defensive\n const ssrfError = checkAddress(host.replace(/^\\[(.*)\\]$/, '$1'), undefined, rules)\n if (ssrfError) {\n oncreate(ssrfError)\n return\n }\n }\n return createConnection({ ...options, lookup: guardLookup(options.lookup ?? dns.lookup, rules) }, oncreate)\n }\n}\n\nexport class SsrfHttpAgent extends HttpAgent {\n constructor (opts: HttpAgent.HttpOptions, rules: SsrfRules) {\n super(opts)\n // @ts-ignore createConnection is not declared in the agentkeepalive typings\n this.createConnection = guardedCreateConnection(rules, super.createConnection.bind(this))\n }\n}\n\nexport class SsrfHttpsAgent extends HttpsAgent {\n constructor (opts: HttpAgent.HttpsOptions, rules: SsrfRules) {\n super(opts)\n // @ts-ignore createConnection is not declared in the agentkeepalive typings\n this.createConnection = guardedCreateConnection(rules, super.createConnection.bind(this))\n }\n}\n\nconst proxyEnvNames = ['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy']\n\n/**\n * The name of the proxy env variable that disables the SSRF protection, if any\n * behind a proxy our agents connect to the proxy, not to the target, so they cannot check the target\n */\nexport const getProxyEnvName = (env: NodeJS.ProcessEnv = process.env) => proxyEnvNames.find(name => !!env[name])\n"]}
package/ssrf.test.js CHANGED
@@ -6,7 +6,8 @@ import { promisify } from 'node:util';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import axios from 'axios';
8
8
  import CacheableLookup from 'cacheable-lookup';
9
- import { checkAddress, parseIPList, getSsrfRules, getProxyEnvName, SsrfHttpAgent, SsrfHttpsAgent } from './ssrf.js';
9
+ import net from 'node:net';
10
+ import { checkAddress, parseIPList, getSsrfRules, getProxyEnvName, resolvePublicAddress, publicLookup, SsrfHttpAgent, SsrfHttpsAgent } from './ssrf.js';
10
11
  const execFileAsync = promisify(execFile);
11
12
  const noRules = { publicIPs: [], privateIPs: [] };
12
13
  describe('ssrf address checks', () => {
@@ -110,4 +111,27 @@ describe('ssrf agents', () => {
110
111
  assert.match(stderr, /WARNING: HTTPS_PROXY is defined, the protection against server side request forgery \(SSRF\) is disabled/);
111
112
  });
112
113
  });
114
+ describe('ssrf helpers for other clients', () => {
115
+ it('should resolve and check hosts', async () => {
116
+ await assert.rejects(resolvePublicAddress('localhost', noRules), { code: 'ERR_SSRF_BLOCKED' });
117
+ await assert.rejects(resolvePublicAddress('127.0.0.1', noRules), { code: 'ERR_SSRF_BLOCKED' });
118
+ await assert.rejects(resolvePublicAddress('[::1]', noRules), { code: 'ERR_SSRF_BLOCKED' });
119
+ await assert.rejects(resolvePublicAddress('169.254.169.254'), { code: 'ERR_SSRF_BLOCKED' });
120
+ assert.equal(await resolvePublicAddress('8.8.8.8', noRules), '8.8.8.8');
121
+ assert.equal(await resolvePublicAddress('[2001:4860:4860::8888]', noRules), '2001:4860:4860::8888');
122
+ const loopback = getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1,::1' });
123
+ assert.ok(['127.0.0.1', '::1'].includes(await resolvePublicAddress('localhost', loopback)));
124
+ });
125
+ it('should refuse non public addresses in a lookup option', async () => {
126
+ assert.ok(!process.env.SSRF_PUBLIC_IPS, 'the test must run without SSRF_PUBLIC_IPS');
127
+ const err = await new Promise((resolve) => {
128
+ const socket = net.connect({ host: 'localhost', port: 1, lookup: publicLookup });
129
+ socket.on('error', resolve);
130
+ socket.on('connect', () => { socket.destroy(); resolve(undefined); });
131
+ });
132
+ assert.equal(err?.code, 'ERR_SSRF_BLOCKED');
133
+ const legacyErr = await new Promise(resolve => publicLookup('localhost', resolve));
134
+ assert.equal(legacyErr?.code, 'ERR_SSRF_BLOCKED');
135
+ });
136
+ });
113
137
  //# sourceMappingURL=ssrf.test.js.map
package/ssrf.test.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ssrf.test.js","sourceRoot":"","sources":["ssrf.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, before, after } from 'node:test'\nimport { strict as assert } from 'assert'\nimport http from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { fileURLToPath } from 'node:url'\nimport axios from 'axios'\nimport CacheableLookup from 'cacheable-lookup'\nimport { checkAddress, parseIPList, getSsrfRules, getProxyEnvName, SsrfHttpAgent, SsrfHttpsAgent, type SsrfRules } from './ssrf.js'\n\nconst execFileAsync = promisify(execFile)\n\nconst noRules: SsrfRules = { publicIPs: [], privateIPs: [] }\n\ndescribe('ssrf address checks', () => {\n it('should refuse non public addresses', () => {\n for (const address of [\n '127.0.0.1', '10.1.2.3', '172.16.0.1', '192.168.1.1', '100.64.0.1', '169.254.169.254', '0.0.0.0',\n '255.255.255.255', '224.0.0.1', '::1', '::', 'fd00::1', 'fe80::1', '::ffff:127.0.0.1', '::ffff:169.254.169.254',\n '64:ff9b::a9fe:a9fe', '2002:a9fe:a9fe::1'\n ]) {\n assert.ok(checkAddress(address, undefined, noRules), `${address} should be refused`)\n }\n })\n\n it('should accept public addresses and ignore hostnames', () => {\n for (const address of ['8.8.8.8', '141.95.149.122', '2001:4860:4860::8888', '::ffff:8.8.8.8', 'koumoul.com']) {\n assert.equal(checkAddress(address, undefined, noRules), undefined, `${address} should be accepted`)\n }\n })\n\n it('should extend public and private addresses from env variables', () => {\n const rules = getSsrfRules({ SSRF_PUBLIC_IPS: '10.0.0.0/8, 192.168.1.1', SSRF_PRIVATE_IPS: '141.95.149.120/30,2001:4860:4860::8888' })\n assert.equal(checkAddress('10.1.2.3', undefined, rules), undefined)\n assert.equal(checkAddress('::ffff:10.1.2.3', undefined, rules), undefined)\n assert.equal(checkAddress('192.168.1.1', undefined, rules), undefined)\n assert.ok(checkAddress('192.168.1.2', undefined, rules))\n assert.ok(checkAddress('141.95.149.122', undefined, rules)?.message.includes('SSRF_PRIVATE_IPS'))\n assert.equal(checkAddress('141.95.149.124', undefined, rules), undefined)\n assert.ok(checkAddress('2001:4860:4860::8888', undefined, rules))\n })\n\n it('should name the env variable to change in the error message', () => {\n const err = checkAddress('10.1.2.3', 'internal.example.com', noRules)\n assert.ok(err?.message.includes('internal.example.com (10.1.2.3)'))\n assert.ok(err?.message.includes('SSRF_PUBLIC_IPS'))\n assert.equal(err?.code, 'ERR_SSRF_BLOCKED')\n })\n\n it('should reject invalid lists', () => {\n assert.throws(() => parseIPList('10.0.0.0/8,not-an-ip', 'SSRF_PUBLIC_IPS'), /not-an-ip.*SSRF_PUBLIC_IPS/)\n assert.deepEqual(parseIPList(' , ', 'SSRF_PUBLIC_IPS'), [])\n })\n\n it('should detect proxy env variables', () => {\n assert.equal(getProxyEnvName({}), undefined)\n assert.equal(getProxyEnvName({ https_proxy: 'http://proxy:3128' }), 'https_proxy')\n })\n})\n\ndescribe('ssrf agents', () => {\n let server: http.Server\n let port: number\n\n before(async () => {\n server = http.createServer((req, res) => {\n if (req.url === '/redirect') {\n res.writeHead(302, { location: `http://127.0.0.2:${port}/` })\n return res.end()\n }\n res.end('ok')\n })\n await new Promise<void>(resolve => server.listen(0, '0.0.0.0', resolve))\n port = (server.address() as AddressInfo).port\n })\n\n after(() => { server.close() })\n\n // same setup as http-agents.ts\n const buildAgents = (rules: SsrfRules) => {\n const httpAgent = new SsrfHttpAgent({ keepAlive: true }, rules)\n const httpsAgent = new SsrfHttpsAgent({ keepAlive: true }, rules)\n const cacheableLookup = new CacheableLookup()\n cacheableLookup.install(httpAgent)\n cacheableLookup.install(httpsAgent)\n return { httpAgent, httpsAgent, proxy: false as const }\n }\n\n it('should refuse IP literals before connecting', async () => {\n await assert.rejects(axios.get(`http://127.0.0.1:${port}/`, buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(axios.get('https://169.254.169.254/', buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n })\n\n it('should refuse hostnames resolving to non public addresses', async () => {\n await assert.rejects(axios.get(`http://localhost:${port}/`, buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n })\n\n it('should accept addresses declared public', async () => {\n const agents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1,::1' }))\n assert.equal((await axios.get(`http://127.0.0.1:${port}/`, agents)).data, 'ok')\n assert.equal((await axios.get(`http://localhost:${port}/`, agents)).data, 'ok')\n })\n\n it('should check every redirection', async () => {\n const agents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1' }))\n await assert.rejects(axios.get(`http://127.0.0.1:${port}/redirect`, agents), { code: 'ERR_SSRF_BLOCKED' })\n const allAgents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.0/8' }))\n assert.equal((await axios.get(`http://127.0.0.1:${port}/redirect`, allAgents)).data, 'ok')\n })\n\n it('should guard the default lib instance, not the private one', async () => {\n const script = `\n import { axiosInstance, privateAxiosInstance } from ${JSON.stringify(fileURLToPath(new URL('./axios.ts', import.meta.url)))}\n const url = 'http://localhost:${port}/'\n try { await axiosInstance.get(url); console.log('public:allowed') } catch (err) { console.log('public:' + err.code) }\n console.log('private:' + (await privateAxiosInstance.get(url)).data)\n `\n const env = { ...process.env, SSRF_PUBLIC_IPS: '', HTTP_PROXY: '', HTTPS_PROXY: '', ALL_PROXY: '', http_proxy: '', https_proxy: '', all_proxy: '' }\n const { stdout } = await execFileAsync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', script], { env, timeout: 10000 })\n assert.match(stdout, /public:ERR_SSRF_BLOCKED/)\n assert.match(stdout, /private:ok/)\n })\n\n it('should disable the protection with a warning behind a proxy', async () => {\n const script = `import ${JSON.stringify(fileURLToPath(new URL('./http-agents.ts', import.meta.url)))}`\n const env = { ...process.env, HTTPS_PROXY: 'http://proxy.example.com:3128' }\n const { stderr } = await execFileAsync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', script], { env, timeout: 10000 })\n assert.match(stderr, /WARNING: HTTPS_PROXY is defined, the protection against server side request forgery \\(SSRF\\) is disabled/)\n })\n})\n"]}
1
+ {"version":3,"file":"ssrf.test.js","sourceRoot":"","sources":["ssrf.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, before, after } from 'node:test'\nimport { strict as assert } from 'assert'\nimport http from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport { execFile } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport { fileURLToPath } from 'node:url'\nimport axios from 'axios'\nimport CacheableLookup from 'cacheable-lookup'\nimport net from 'node:net'\nimport { checkAddress, parseIPList, getSsrfRules, getProxyEnvName, resolvePublicAddress, publicLookup, SsrfHttpAgent, SsrfHttpsAgent, type SsrfRules } from './ssrf.js'\n\nconst execFileAsync = promisify(execFile)\n\nconst noRules: SsrfRules = { publicIPs: [], privateIPs: [] }\n\ndescribe('ssrf address checks', () => {\n it('should refuse non public addresses', () => {\n for (const address of [\n '127.0.0.1', '10.1.2.3', '172.16.0.1', '192.168.1.1', '100.64.0.1', '169.254.169.254', '0.0.0.0',\n '255.255.255.255', '224.0.0.1', '::1', '::', 'fd00::1', 'fe80::1', '::ffff:127.0.0.1', '::ffff:169.254.169.254',\n '64:ff9b::a9fe:a9fe', '2002:a9fe:a9fe::1'\n ]) {\n assert.ok(checkAddress(address, undefined, noRules), `${address} should be refused`)\n }\n })\n\n it('should accept public addresses and ignore hostnames', () => {\n for (const address of ['8.8.8.8', '141.95.149.122', '2001:4860:4860::8888', '::ffff:8.8.8.8', 'koumoul.com']) {\n assert.equal(checkAddress(address, undefined, noRules), undefined, `${address} should be accepted`)\n }\n })\n\n it('should extend public and private addresses from env variables', () => {\n const rules = getSsrfRules({ SSRF_PUBLIC_IPS: '10.0.0.0/8, 192.168.1.1', SSRF_PRIVATE_IPS: '141.95.149.120/30,2001:4860:4860::8888' })\n assert.equal(checkAddress('10.1.2.3', undefined, rules), undefined)\n assert.equal(checkAddress('::ffff:10.1.2.3', undefined, rules), undefined)\n assert.equal(checkAddress('192.168.1.1', undefined, rules), undefined)\n assert.ok(checkAddress('192.168.1.2', undefined, rules))\n assert.ok(checkAddress('141.95.149.122', undefined, rules)?.message.includes('SSRF_PRIVATE_IPS'))\n assert.equal(checkAddress('141.95.149.124', undefined, rules), undefined)\n assert.ok(checkAddress('2001:4860:4860::8888', undefined, rules))\n })\n\n it('should name the env variable to change in the error message', () => {\n const err = checkAddress('10.1.2.3', 'internal.example.com', noRules)\n assert.ok(err?.message.includes('internal.example.com (10.1.2.3)'))\n assert.ok(err?.message.includes('SSRF_PUBLIC_IPS'))\n assert.equal(err?.code, 'ERR_SSRF_BLOCKED')\n })\n\n it('should reject invalid lists', () => {\n assert.throws(() => parseIPList('10.0.0.0/8,not-an-ip', 'SSRF_PUBLIC_IPS'), /not-an-ip.*SSRF_PUBLIC_IPS/)\n assert.deepEqual(parseIPList(' , ', 'SSRF_PUBLIC_IPS'), [])\n })\n\n it('should detect proxy env variables', () => {\n assert.equal(getProxyEnvName({}), undefined)\n assert.equal(getProxyEnvName({ https_proxy: 'http://proxy:3128' }), 'https_proxy')\n })\n})\n\ndescribe('ssrf agents', () => {\n let server: http.Server\n let port: number\n\n before(async () => {\n server = http.createServer((req, res) => {\n if (req.url === '/redirect') {\n res.writeHead(302, { location: `http://127.0.0.2:${port}/` })\n return res.end()\n }\n res.end('ok')\n })\n await new Promise<void>(resolve => server.listen(0, '0.0.0.0', resolve))\n port = (server.address() as AddressInfo).port\n })\n\n after(() => { server.close() })\n\n // same setup as http-agents.ts\n const buildAgents = (rules: SsrfRules) => {\n const httpAgent = new SsrfHttpAgent({ keepAlive: true }, rules)\n const httpsAgent = new SsrfHttpsAgent({ keepAlive: true }, rules)\n const cacheableLookup = new CacheableLookup()\n cacheableLookup.install(httpAgent)\n cacheableLookup.install(httpsAgent)\n return { httpAgent, httpsAgent, proxy: false as const }\n }\n\n it('should refuse IP literals before connecting', async () => {\n await assert.rejects(axios.get(`http://127.0.0.1:${port}/`, buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(axios.get('https://169.254.169.254/', buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n })\n\n it('should refuse hostnames resolving to non public addresses', async () => {\n await assert.rejects(axios.get(`http://localhost:${port}/`, buildAgents(noRules)), { code: 'ERR_SSRF_BLOCKED' })\n })\n\n it('should accept addresses declared public', async () => {\n const agents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1,::1' }))\n assert.equal((await axios.get(`http://127.0.0.1:${port}/`, agents)).data, 'ok')\n assert.equal((await axios.get(`http://localhost:${port}/`, agents)).data, 'ok')\n })\n\n it('should check every redirection', async () => {\n const agents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1' }))\n await assert.rejects(axios.get(`http://127.0.0.1:${port}/redirect`, agents), { code: 'ERR_SSRF_BLOCKED' })\n const allAgents = buildAgents(getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.0/8' }))\n assert.equal((await axios.get(`http://127.0.0.1:${port}/redirect`, allAgents)).data, 'ok')\n })\n\n it('should guard the default lib instance, not the private one', async () => {\n const script = `\n import { axiosInstance, privateAxiosInstance } from ${JSON.stringify(fileURLToPath(new URL('./axios.ts', import.meta.url)))}\n const url = 'http://localhost:${port}/'\n try { await axiosInstance.get(url); console.log('public:allowed') } catch (err) { console.log('public:' + err.code) }\n console.log('private:' + (await privateAxiosInstance.get(url)).data)\n `\n const env = { ...process.env, SSRF_PUBLIC_IPS: '', HTTP_PROXY: '', HTTPS_PROXY: '', ALL_PROXY: '', http_proxy: '', https_proxy: '', all_proxy: '' }\n const { stdout } = await execFileAsync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', script], { env, timeout: 10000 })\n assert.match(stdout, /public:ERR_SSRF_BLOCKED/)\n assert.match(stdout, /private:ok/)\n })\n\n it('should disable the protection with a warning behind a proxy', async () => {\n const script = `import ${JSON.stringify(fileURLToPath(new URL('./http-agents.ts', import.meta.url)))}`\n const env = { ...process.env, HTTPS_PROXY: 'http://proxy.example.com:3128' }\n const { stderr } = await execFileAsync(process.execPath, ['--experimental-strip-types', '--input-type=module', '--eval', script], { env, timeout: 10000 })\n assert.match(stderr, /WARNING: HTTPS_PROXY is defined, the protection against server side request forgery \\(SSRF\\) is disabled/)\n })\n})\n\ndescribe('ssrf helpers for other clients', () => {\n it('should resolve and check hosts', async () => {\n await assert.rejects(resolvePublicAddress('localhost', noRules), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(resolvePublicAddress('127.0.0.1', noRules), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(resolvePublicAddress('[::1]', noRules), { code: 'ERR_SSRF_BLOCKED' })\n await assert.rejects(resolvePublicAddress('169.254.169.254'), { code: 'ERR_SSRF_BLOCKED' })\n assert.equal(await resolvePublicAddress('8.8.8.8', noRules), '8.8.8.8')\n assert.equal(await resolvePublicAddress('[2001:4860:4860::8888]', noRules), '2001:4860:4860::8888')\n const loopback = getSsrfRules({ SSRF_PUBLIC_IPS: '127.0.0.1,::1' })\n assert.ok(['127.0.0.1', '::1'].includes(await resolvePublicAddress('localhost', loopback)))\n })\n\n it('should refuse non public addresses in a lookup option', async () => {\n assert.ok(!process.env.SSRF_PUBLIC_IPS, 'the test must run without SSRF_PUBLIC_IPS')\n const err = await new Promise<any>((resolve) => {\n const socket = net.connect({ host: 'localhost', port: 1, lookup: publicLookup })\n socket.on('error', resolve)\n socket.on('connect', () => { socket.destroy(); resolve(undefined) })\n })\n assert.equal(err?.code, 'ERR_SSRF_BLOCKED')\n const legacyErr = await new Promise<any>(resolve => publicLookup('localhost', resolve))\n assert.equal(legacyErr?.code, 'ERR_SSRF_BLOCKED')\n })\n})\n"]}