ssrf-agent-guard 0.1.7 → 0.1.8
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/API.md +896 -0
- package/README.md +31 -13
- package/package.json +1 -1
package/API.md
ADDED
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Complete API documentation for `ssrf-agent-guard`.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Quick Start](#quick-start)
|
|
8
|
+
- [Main Function](#main-function)
|
|
9
|
+
- [ssrfAgentGuard](#ssrfagentguard)
|
|
10
|
+
- [Configuration](#configuration)
|
|
11
|
+
- [Options](#options)
|
|
12
|
+
- [PolicyOptions](#policyoptions)
|
|
13
|
+
- [Types](#types)
|
|
14
|
+
- [BlockReason](#blockreason)
|
|
15
|
+
- [BlockEvent](#blockevent)
|
|
16
|
+
- [ValidationResult](#validationresult)
|
|
17
|
+
- [Utility Functions](#utility-functions)
|
|
18
|
+
- [validateHost](#validatehost)
|
|
19
|
+
- [isCloudMetadata](#iscloudmetadata)
|
|
20
|
+
- [validatePolicy](#validatepolicy)
|
|
21
|
+
- [matchesDomain](#matchesdomain)
|
|
22
|
+
- [getTLD](#gettld)
|
|
23
|
+
- [Constants](#constants)
|
|
24
|
+
- [CLOUD_METADATA_HOSTS](#cloud_metadata_hosts)
|
|
25
|
+
- [Advanced Usage](#advanced-usage)
|
|
26
|
+
- [Operation Modes](#operation-modes)
|
|
27
|
+
- [Custom Policies](#custom-policies)
|
|
28
|
+
- [Custom Logging](#custom-logging)
|
|
29
|
+
- [Integration Examples](#integration-examples)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import ssrfAgentGuard from 'ssrf-agent-guard';
|
|
37
|
+
import axios from 'axios';
|
|
38
|
+
|
|
39
|
+
const url = 'https://api.example.com/data';
|
|
40
|
+
|
|
41
|
+
// Basic usage - blocks private IPs, cloud metadata, and DNS rebinding attacks
|
|
42
|
+
const response = await axios.get(url, {
|
|
43
|
+
httpsAgent: ssrfAgentGuard(url)
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Main Function
|
|
50
|
+
|
|
51
|
+
### ssrfAgentGuard
|
|
52
|
+
|
|
53
|
+
Creates a patched HTTP/HTTPS Agent with SSRF protection.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
function ssrfAgentGuard(url: string, options?: Options): HttpAgent | HttpsAgent
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Parameters
|
|
60
|
+
|
|
61
|
+
| Parameter | Type | Required | Description |
|
|
62
|
+
|-----------|------|----------|-------------|
|
|
63
|
+
| `url` | `string` | Yes | The URL or protocol hint (e.g., `'https://...'` or `'https'`) used to determine agent type |
|
|
64
|
+
| `options` | `Options` | No | Configuration options for SSRF protection |
|
|
65
|
+
|
|
66
|
+
#### Returns
|
|
67
|
+
|
|
68
|
+
Returns an `http.Agent` or `https.Agent` instance with patched `createConnection` method that performs SSRF validation.
|
|
69
|
+
|
|
70
|
+
#### Example
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import ssrfAgentGuard from 'ssrf-agent-guard';
|
|
74
|
+
|
|
75
|
+
// HTTPS agent
|
|
76
|
+
const httpsAgent = ssrfAgentGuard('https://example.com');
|
|
77
|
+
|
|
78
|
+
// HTTP agent
|
|
79
|
+
const httpAgent = ssrfAgentGuard('http://example.com');
|
|
80
|
+
|
|
81
|
+
// With options
|
|
82
|
+
const agent = ssrfAgentGuard('https://example.com', {
|
|
83
|
+
mode: 'block',
|
|
84
|
+
blockCloudMetadata: true,
|
|
85
|
+
detectDnsRebinding: true,
|
|
86
|
+
policy: {
|
|
87
|
+
denyDomains: ['evil.com'],
|
|
88
|
+
denyTLD: ['local', 'internal']
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Behavior
|
|
94
|
+
|
|
95
|
+
1. **Pre-DNS Validation**: Before DNS resolution, validates the hostname against:
|
|
96
|
+
- Cloud metadata endpoints
|
|
97
|
+
- Policy rules (allow/deny lists)
|
|
98
|
+
- Private IP addresses
|
|
99
|
+
- Invalid domain syntax
|
|
100
|
+
|
|
101
|
+
2. **Post-DNS Validation**: After DNS resolution, validates resolved IPs to detect:
|
|
102
|
+
- DNS rebinding attacks (legitimate domain resolving to private IP)
|
|
103
|
+
- Cloud metadata IPs
|
|
104
|
+
|
|
105
|
+
3. **Error Handling**: When a request is blocked (in `'block'` mode), throws an `Error` with a descriptive message.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
### Options
|
|
112
|
+
|
|
113
|
+
Main configuration interface for SSRF protection.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
interface Options {
|
|
117
|
+
protocol?: string;
|
|
118
|
+
metadataHosts?: string[];
|
|
119
|
+
mode?: 'block' | 'report' | 'allow';
|
|
120
|
+
policy?: PolicyOptions;
|
|
121
|
+
blockCloudMetadata?: boolean;
|
|
122
|
+
detectDnsRebinding?: boolean;
|
|
123
|
+
logger?: (level: 'info' | 'warn' | 'error', msg: string, meta?: BlockEvent) => void;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Properties
|
|
128
|
+
|
|
129
|
+
| Property | Type | Default | Description |
|
|
130
|
+
|----------|------|---------|-------------|
|
|
131
|
+
| `protocol` | `string` | Inferred from URL | Protocol hint (`'http'` or `'https'`). Usually inferred automatically from the URL parameter. |
|
|
132
|
+
| `metadataHosts` | `string[]` | `[]` | Additional cloud metadata hosts to block. These are merged with the built-in `CLOUD_METADATA_HOSTS`. |
|
|
133
|
+
| `mode` | `'block' \| 'report' \| 'allow'` | `'block'` | Operation mode. See [Operation Modes](#operation-modes). |
|
|
134
|
+
| `policy` | `PolicyOptions` | `undefined` | Domain/TLD filtering rules. See [PolicyOptions](#policyoptions). |
|
|
135
|
+
| `blockCloudMetadata` | `boolean` | `true` | Whether to block requests to cloud metadata endpoints. |
|
|
136
|
+
| `detectDnsRebinding` | `boolean` | `true` | Whether to validate resolved IPs after DNS lookup to detect rebinding attacks. |
|
|
137
|
+
| `logger` | `function` | `undefined` | Callback function for logging blocked requests and warnings. |
|
|
138
|
+
|
|
139
|
+
#### Example
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const options: Options = {
|
|
143
|
+
mode: 'block',
|
|
144
|
+
blockCloudMetadata: true,
|
|
145
|
+
detectDnsRebinding: true,
|
|
146
|
+
metadataHosts: ['custom-metadata.internal'],
|
|
147
|
+
policy: {
|
|
148
|
+
allowDomains: ['trusted-api.com', '*.mycompany.com'],
|
|
149
|
+
denyTLD: ['local']
|
|
150
|
+
},
|
|
151
|
+
logger: (level, msg, meta) => {
|
|
152
|
+
console.log(`[${level.toUpperCase()}] ${msg}`, meta);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### PolicyOptions
|
|
160
|
+
|
|
161
|
+
Domain-based filtering rules for fine-grained access control.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
interface PolicyOptions {
|
|
165
|
+
allowDomains?: string[];
|
|
166
|
+
denyDomains?: string[];
|
|
167
|
+
denyTLD?: string[];
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Properties
|
|
172
|
+
|
|
173
|
+
| Property | Type | Default | Description |
|
|
174
|
+
|----------|------|---------|-------------|
|
|
175
|
+
| `allowDomains` | `string[]` | `undefined` | Explicit allowlist of domains. When specified, **only** these domains are allowed (acts as a strict allowlist). Supports wildcards. |
|
|
176
|
+
| `denyDomains` | `string[]` | `undefined` | Domains to explicitly deny. Supports wildcards. |
|
|
177
|
+
| `denyTLD` | `string[]` | `undefined` | Top-level domains to deny (e.g., `['local', 'internal', 'test']`). |
|
|
178
|
+
|
|
179
|
+
#### Domain Pattern Matching
|
|
180
|
+
|
|
181
|
+
All domain lists support these matching patterns:
|
|
182
|
+
|
|
183
|
+
- **Exact match**: `'example.com'` matches only `example.com`
|
|
184
|
+
- **Subdomain match**: `'example.com'` also matches `sub.example.com`, `a.b.example.com`
|
|
185
|
+
- **Wildcard match**: `'*.example.com'` matches `sub.example.com` but also `example.com`
|
|
186
|
+
|
|
187
|
+
#### Evaluation Order
|
|
188
|
+
|
|
189
|
+
1. If `allowDomains` is specified and non-empty:
|
|
190
|
+
- If hostname matches any allowed domain → **Allow**
|
|
191
|
+
- Otherwise → **Block** (reason: `'not_allowed_domain'`)
|
|
192
|
+
2. If hostname matches any `denyDomains` → **Block** (reason: `'denied_domain'`)
|
|
193
|
+
3. If hostname's TLD is in `denyTLD` → **Block** (reason: `'denied_tld'`)
|
|
194
|
+
4. Otherwise → **Allow** (continue to other checks)
|
|
195
|
+
|
|
196
|
+
#### Examples
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Strict allowlist - only allow specific domains
|
|
200
|
+
const strictPolicy: PolicyOptions = {
|
|
201
|
+
allowDomains: ['api.mycompany.com', '*.trusted-partner.com']
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Denylist approach - block specific domains/TLDs
|
|
205
|
+
const denyPolicy: PolicyOptions = {
|
|
206
|
+
denyDomains: ['evil.com', '*.malicious.net'],
|
|
207
|
+
denyTLD: ['local', 'internal', 'localhost', 'test']
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Combined (allowDomains takes precedence)
|
|
211
|
+
const combinedPolicy: PolicyOptions = {
|
|
212
|
+
allowDomains: ['api.mycompany.com'],
|
|
213
|
+
denyDomains: ['evil.com'], // This is ignored when allowDomains is set
|
|
214
|
+
denyTLD: ['local'] // This is also ignored when allowDomains is set
|
|
215
|
+
};
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Types
|
|
221
|
+
|
|
222
|
+
### BlockReason
|
|
223
|
+
|
|
224
|
+
Union type representing the reason a request was blocked.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
type BlockReason =
|
|
228
|
+
| 'private_ip'
|
|
229
|
+
| 'cloud_metadata'
|
|
230
|
+
| 'invalid_domain'
|
|
231
|
+
| 'dns_rebinding'
|
|
232
|
+
| 'denied_domain'
|
|
233
|
+
| 'denied_tld'
|
|
234
|
+
| 'not_allowed_domain';
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### Values
|
|
238
|
+
|
|
239
|
+
| Value | Description |
|
|
240
|
+
|-------|-------------|
|
|
241
|
+
| `'private_ip'` | Request targets a private/internal IP address (loopback, link-local, private ranges) |
|
|
242
|
+
| `'cloud_metadata'` | Request targets a cloud provider metadata endpoint |
|
|
243
|
+
| `'invalid_domain'` | Domain name has invalid syntax |
|
|
244
|
+
| `'dns_rebinding'` | DNS rebinding attack detected (domain resolved to unsafe IP) |
|
|
245
|
+
| `'denied_domain'` | Domain is in the `denyDomains` policy list |
|
|
246
|
+
| `'denied_tld'` | Domain's TLD is in the `denyTLD` policy list |
|
|
247
|
+
| `'not_allowed_domain'` | Domain is not in the `allowDomains` policy list (when allowlist is active) |
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### BlockEvent
|
|
252
|
+
|
|
253
|
+
Event data passed to the logger callback when a request is blocked or flagged.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
interface BlockEvent {
|
|
257
|
+
url: string;
|
|
258
|
+
reason: BlockReason;
|
|
259
|
+
ip?: string;
|
|
260
|
+
hostname?: string;
|
|
261
|
+
timestamp: number;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### Properties
|
|
266
|
+
|
|
267
|
+
| Property | Type | Description |
|
|
268
|
+
|----------|------|-------------|
|
|
269
|
+
| `url` | `string` | The original URL or hostname that was blocked |
|
|
270
|
+
| `reason` | `BlockReason` | The reason for blocking |
|
|
271
|
+
| `ip` | `string \| undefined` | The resolved IP address (available for DNS rebinding detections) |
|
|
272
|
+
| `hostname` | `string \| undefined` | The original hostname before DNS resolution |
|
|
273
|
+
| `timestamp` | `number` | Unix timestamp (milliseconds) when the event occurred |
|
|
274
|
+
|
|
275
|
+
#### Example
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const logger = (level: string, msg: string, meta?: BlockEvent) => {
|
|
279
|
+
if (meta) {
|
|
280
|
+
console.log({
|
|
281
|
+
level,
|
|
282
|
+
message: msg,
|
|
283
|
+
url: meta.url,
|
|
284
|
+
reason: meta.reason,
|
|
285
|
+
ip: meta.ip,
|
|
286
|
+
hostname: meta.hostname,
|
|
287
|
+
timestamp: new Date(meta.timestamp).toISOString()
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### ValidationResult
|
|
296
|
+
|
|
297
|
+
Result returned by validation functions.
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
interface ValidationResult {
|
|
301
|
+
safe: boolean;
|
|
302
|
+
reason?: BlockReason;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Properties
|
|
307
|
+
|
|
308
|
+
| Property | Type | Description |
|
|
309
|
+
|----------|------|-------------|
|
|
310
|
+
| `safe` | `boolean` | `true` if the host passed validation, `false` if blocked |
|
|
311
|
+
| `reason` | `BlockReason \| undefined` | The reason for blocking (only present when `safe` is `false`) |
|
|
312
|
+
|
|
313
|
+
#### Example
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { validateHost, ValidationResult } from 'ssrf-agent-guard';
|
|
317
|
+
|
|
318
|
+
const result: ValidationResult = validateHost('192.168.1.1');
|
|
319
|
+
if (!result.safe) {
|
|
320
|
+
console.log(`Blocked: ${result.reason}`); // "Blocked: private_ip"
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Utility Functions
|
|
327
|
+
|
|
328
|
+
These functions are exported for advanced use cases where you need to perform validation outside of the HTTP agent context.
|
|
329
|
+
|
|
330
|
+
### validateHost
|
|
331
|
+
|
|
332
|
+
High-level validation for hostnames and IP addresses.
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
function validateHost(hostname: string, options?: Options): ValidationResult
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### Parameters
|
|
339
|
+
|
|
340
|
+
| Parameter | Type | Required | Description |
|
|
341
|
+
|-----------|------|----------|-------------|
|
|
342
|
+
| `hostname` | `string` | Yes | The hostname or IP address to validate |
|
|
343
|
+
| `options` | `Options` | No | Configuration options |
|
|
344
|
+
|
|
345
|
+
#### Returns
|
|
346
|
+
|
|
347
|
+
`ValidationResult` with `safe` status and optional `reason`.
|
|
348
|
+
|
|
349
|
+
#### Validation Order
|
|
350
|
+
|
|
351
|
+
1. Check if hostname is a cloud metadata endpoint
|
|
352
|
+
2. For non-IP hostnames, check policy rules (allow/deny)
|
|
353
|
+
3. For IP addresses, check if public
|
|
354
|
+
4. For domain names, validate syntax
|
|
355
|
+
|
|
356
|
+
#### Example
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { validateHost } from 'ssrf-agent-guard';
|
|
360
|
+
|
|
361
|
+
// Validate an IP
|
|
362
|
+
validateHost('127.0.0.1');
|
|
363
|
+
// { safe: false, reason: 'private_ip' }
|
|
364
|
+
|
|
365
|
+
// Validate a domain
|
|
366
|
+
validateHost('google.com');
|
|
367
|
+
// { safe: true }
|
|
368
|
+
|
|
369
|
+
// Validate with policy
|
|
370
|
+
validateHost('evil.com', {
|
|
371
|
+
policy: { denyDomains: ['evil.com'] }
|
|
372
|
+
});
|
|
373
|
+
// { safe: false, reason: 'denied_domain' }
|
|
374
|
+
|
|
375
|
+
// Validate cloud metadata
|
|
376
|
+
validateHost('169.254.169.254');
|
|
377
|
+
// { safe: false, reason: 'cloud_metadata' }
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
### isCloudMetadata
|
|
383
|
+
|
|
384
|
+
Checks if a hostname is a cloud metadata endpoint.
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
function isCloudMetadata(hostname: string, customHosts?: string[]): boolean
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
#### Parameters
|
|
391
|
+
|
|
392
|
+
| Parameter | Type | Required | Description |
|
|
393
|
+
|-----------|------|----------|-------------|
|
|
394
|
+
| `hostname` | `string` | Yes | The hostname to check |
|
|
395
|
+
| `customHosts` | `string[]` | No | Additional custom metadata hosts to check |
|
|
396
|
+
|
|
397
|
+
#### Returns
|
|
398
|
+
|
|
399
|
+
`true` if the hostname is a cloud metadata endpoint, `false` otherwise.
|
|
400
|
+
|
|
401
|
+
#### Example
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { isCloudMetadata } from 'ssrf-agent-guard';
|
|
405
|
+
|
|
406
|
+
isCloudMetadata('169.254.169.254'); // true (AWS/Azure/GCP)
|
|
407
|
+
isCloudMetadata('metadata.google.internal'); // true (GCP)
|
|
408
|
+
isCloudMetadata('168.63.129.16'); // true (Azure)
|
|
409
|
+
isCloudMetadata('example.com'); // false
|
|
410
|
+
|
|
411
|
+
// With custom hosts
|
|
412
|
+
isCloudMetadata('custom-metadata.local', ['custom-metadata.local']); // true
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
### validatePolicy
|
|
418
|
+
|
|
419
|
+
Validates a hostname against policy options.
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
function validatePolicy(hostname: string, policy?: PolicyOptions): ValidationResult
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
#### Parameters
|
|
426
|
+
|
|
427
|
+
| Parameter | Type | Required | Description |
|
|
428
|
+
|-----------|------|----------|-------------|
|
|
429
|
+
| `hostname` | `string` | Yes | The hostname to validate |
|
|
430
|
+
| `policy` | `PolicyOptions` | No | Policy options |
|
|
431
|
+
|
|
432
|
+
#### Returns
|
|
433
|
+
|
|
434
|
+
`ValidationResult` with `safe` status and optional `reason`.
|
|
435
|
+
|
|
436
|
+
#### Example
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
import { validatePolicy } from 'ssrf-agent-guard';
|
|
440
|
+
|
|
441
|
+
// No policy - always safe
|
|
442
|
+
validatePolicy('anything.com');
|
|
443
|
+
// { safe: true }
|
|
444
|
+
|
|
445
|
+
// Allowlist mode
|
|
446
|
+
validatePolicy('allowed.com', {
|
|
447
|
+
allowDomains: ['allowed.com']
|
|
448
|
+
});
|
|
449
|
+
// { safe: true }
|
|
450
|
+
|
|
451
|
+
validatePolicy('other.com', {
|
|
452
|
+
allowDomains: ['allowed.com']
|
|
453
|
+
});
|
|
454
|
+
// { safe: false, reason: 'not_allowed_domain' }
|
|
455
|
+
|
|
456
|
+
// Denylist mode
|
|
457
|
+
validatePolicy('evil.com', {
|
|
458
|
+
denyDomains: ['evil.com']
|
|
459
|
+
});
|
|
460
|
+
// { safe: false, reason: 'denied_domain' }
|
|
461
|
+
|
|
462
|
+
// TLD filtering
|
|
463
|
+
validatePolicy('service.local', {
|
|
464
|
+
denyTLD: ['local']
|
|
465
|
+
});
|
|
466
|
+
// { safe: false, reason: 'denied_tld' }
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### matchesDomain
|
|
472
|
+
|
|
473
|
+
Checks if a hostname matches a domain pattern.
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
function matchesDomain(hostname: string, pattern: string): boolean
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### Parameters
|
|
480
|
+
|
|
481
|
+
| Parameter | Type | Required | Description |
|
|
482
|
+
|-----------|------|----------|-------------|
|
|
483
|
+
| `hostname` | `string` | Yes | The hostname to check |
|
|
484
|
+
| `pattern` | `string` | Yes | The domain pattern to match against |
|
|
485
|
+
|
|
486
|
+
#### Returns
|
|
487
|
+
|
|
488
|
+
`true` if the hostname matches the pattern, `false` otherwise.
|
|
489
|
+
|
|
490
|
+
#### Pattern Types
|
|
491
|
+
|
|
492
|
+
- **Exact match**: `example.com` matches `example.com`
|
|
493
|
+
- **Subdomain match**: `example.com` matches `sub.example.com`
|
|
494
|
+
- **Wildcard match**: `*.example.com` matches `sub.example.com` and `example.com`
|
|
495
|
+
|
|
496
|
+
#### Example
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import { matchesDomain } from 'ssrf-agent-guard';
|
|
500
|
+
|
|
501
|
+
// Exact match
|
|
502
|
+
matchesDomain('example.com', 'example.com'); // true
|
|
503
|
+
matchesDomain('other.com', 'example.com'); // false
|
|
504
|
+
|
|
505
|
+
// Subdomain match (pattern without wildcard)
|
|
506
|
+
matchesDomain('sub.example.com', 'example.com'); // true
|
|
507
|
+
matchesDomain('a.b.example.com', 'example.com'); // true
|
|
508
|
+
|
|
509
|
+
// Wildcard match
|
|
510
|
+
matchesDomain('sub.example.com', '*.example.com'); // true
|
|
511
|
+
matchesDomain('example.com', '*.example.com'); // true
|
|
512
|
+
matchesDomain('other.com', '*.example.com'); // false
|
|
513
|
+
|
|
514
|
+
// Case insensitive
|
|
515
|
+
matchesDomain('SUB.EXAMPLE.COM', 'example.com'); // true
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### getTLD
|
|
521
|
+
|
|
522
|
+
Extracts the top-level domain from a hostname.
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
function getTLD(hostname: string): string
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
#### Parameters
|
|
529
|
+
|
|
530
|
+
| Parameter | Type | Required | Description |
|
|
531
|
+
|-----------|------|----------|-------------|
|
|
532
|
+
| `hostname` | `string` | Yes | The hostname to extract TLD from |
|
|
533
|
+
|
|
534
|
+
#### Returns
|
|
535
|
+
|
|
536
|
+
The TLD (lowercase) or empty string if not found.
|
|
537
|
+
|
|
538
|
+
#### Example
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
import { getTLD } from 'ssrf-agent-guard';
|
|
542
|
+
|
|
543
|
+
getTLD('example.com'); // 'com'
|
|
544
|
+
getTLD('sub.example.co.uk'); // 'uk'
|
|
545
|
+
getTLD('localhost'); // 'localhost'
|
|
546
|
+
getTLD('service.local'); // 'local'
|
|
547
|
+
getTLD(''); // ''
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
## Constants
|
|
553
|
+
|
|
554
|
+
### CLOUD_METADATA_HOSTS
|
|
555
|
+
|
|
556
|
+
A `Set<string>` containing the default cloud metadata endpoints that are blocked.
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
const CLOUD_METADATA_HOSTS: Set<string>
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
#### Included Endpoints
|
|
563
|
+
|
|
564
|
+
| Provider | Endpoints |
|
|
565
|
+
|----------|-----------|
|
|
566
|
+
| **AWS EC2** | `169.254.169.254`, `169.254.169.253` |
|
|
567
|
+
| **AWS Fargate/ECS** | `169.254.170.2` |
|
|
568
|
+
| **GCP** | `metadata.google.internal`, `metadata.goog` |
|
|
569
|
+
| **Azure IMDS** | `169.254.169.254`, `168.63.129.16` |
|
|
570
|
+
| **Kubernetes** | `kubernetes.default`, `kubernetes.default.svc`, `kubernetes.default.svc.cluster.local` |
|
|
571
|
+
| **Oracle Cloud** | `169.254.169.254` |
|
|
572
|
+
| **DigitalOcean** | `169.254.169.254` |
|
|
573
|
+
| **Alibaba Cloud** | `100.100.100.200` |
|
|
574
|
+
| **Link-local** | `169.254.0.0` |
|
|
575
|
+
|
|
576
|
+
#### Example
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
import { CLOUD_METADATA_HOSTS } from 'ssrf-agent-guard';
|
|
580
|
+
|
|
581
|
+
// Check if a host is in the default list
|
|
582
|
+
CLOUD_METADATA_HOSTS.has('169.254.169.254'); // true
|
|
583
|
+
CLOUD_METADATA_HOSTS.has('example.com'); // false
|
|
584
|
+
|
|
585
|
+
// Iterate over all metadata hosts
|
|
586
|
+
for (const host of CLOUD_METADATA_HOSTS) {
|
|
587
|
+
console.log(host);
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Advanced Usage
|
|
594
|
+
|
|
595
|
+
### Operation Modes
|
|
596
|
+
|
|
597
|
+
The library supports three operation modes via the `mode` option:
|
|
598
|
+
|
|
599
|
+
#### Block Mode (Default)
|
|
600
|
+
|
|
601
|
+
Throws an error when an SSRF attempt is detected, preventing the request.
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
const agent = ssrfAgentGuard(url, { mode: 'block' });
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
await axios.get('http://169.254.169.254/latest/meta-data/', {
|
|
608
|
+
httpAgent: agent
|
|
609
|
+
});
|
|
610
|
+
} catch (error) {
|
|
611
|
+
console.error(error.message);
|
|
612
|
+
// "Cloud metadata endpoint 169.254.169.254 is not allowed"
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
#### Report Mode
|
|
617
|
+
|
|
618
|
+
Logs warnings but allows the request to proceed. Useful for monitoring and gradual rollout.
|
|
619
|
+
|
|
620
|
+
```typescript
|
|
621
|
+
const agent = ssrfAgentGuard(url, {
|
|
622
|
+
mode: 'report',
|
|
623
|
+
logger: (level, msg, meta) => {
|
|
624
|
+
// Send to monitoring system
|
|
625
|
+
metrics.increment('ssrf.detected', { reason: meta?.reason });
|
|
626
|
+
console.warn(`[SSRF Report] ${msg}`, meta);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Request proceeds but is logged
|
|
631
|
+
await axios.get('http://169.254.169.254/', { httpAgent: agent });
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
#### Allow Mode
|
|
635
|
+
|
|
636
|
+
Disables all SSRF checks. Use only for debugging or testing.
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
const agent = ssrfAgentGuard(url, { mode: 'allow' });
|
|
640
|
+
// No validation performed
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
### Custom Policies
|
|
646
|
+
|
|
647
|
+
#### Strict Allowlist
|
|
648
|
+
|
|
649
|
+
Only allow requests to specific trusted domains:
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
const agent = ssrfAgentGuard(url, {
|
|
653
|
+
policy: {
|
|
654
|
+
allowDomains: [
|
|
655
|
+
'api.mycompany.com',
|
|
656
|
+
'*.trusted-partner.com',
|
|
657
|
+
'cdn.provider.com'
|
|
658
|
+
]
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
#### Block Internal TLDs
|
|
664
|
+
|
|
665
|
+
Block requests to internal/development TLDs:
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
const agent = ssrfAgentGuard(url, {
|
|
669
|
+
policy: {
|
|
670
|
+
denyTLD: ['local', 'internal', 'localhost', 'test', 'example', 'invalid']
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
#### Block Specific Domains
|
|
676
|
+
|
|
677
|
+
Block known malicious or unwanted domains:
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
const agent = ssrfAgentGuard(url, {
|
|
681
|
+
policy: {
|
|
682
|
+
denyDomains: [
|
|
683
|
+
'evil.com',
|
|
684
|
+
'*.malicious.net',
|
|
685
|
+
'competitor-api.com'
|
|
686
|
+
]
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
#### Custom Metadata Hosts
|
|
692
|
+
|
|
693
|
+
Add organization-specific metadata endpoints:
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
const agent = ssrfAgentGuard(url, {
|
|
697
|
+
metadataHosts: [
|
|
698
|
+
'metadata.internal.mycompany.com',
|
|
699
|
+
'config-service.local'
|
|
700
|
+
]
|
|
701
|
+
});
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
### Custom Logging
|
|
707
|
+
|
|
708
|
+
#### Basic Console Logging
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
const agent = ssrfAgentGuard(url, {
|
|
712
|
+
logger: (level, msg, meta) => {
|
|
713
|
+
console.log(`[${level.toUpperCase()}] ${msg}`);
|
|
714
|
+
if (meta) {
|
|
715
|
+
console.log(' Details:', JSON.stringify(meta, null, 2));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
#### Integration with Logging Libraries
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
import winston from 'winston';
|
|
725
|
+
|
|
726
|
+
const logger = winston.createLogger({ /* config */ });
|
|
727
|
+
|
|
728
|
+
const agent = ssrfAgentGuard(url, {
|
|
729
|
+
logger: (level, msg, meta) => {
|
|
730
|
+
logger.log({
|
|
731
|
+
level,
|
|
732
|
+
message: msg,
|
|
733
|
+
...meta
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
#### Send to Monitoring Service
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
const agent = ssrfAgentGuard(url, {
|
|
743
|
+
mode: 'report', // Log but don't block
|
|
744
|
+
logger: async (level, msg, meta) => {
|
|
745
|
+
if (meta) {
|
|
746
|
+
await fetch('https://monitoring.mycompany.com/ssrf-events', {
|
|
747
|
+
method: 'POST',
|
|
748
|
+
body: JSON.stringify({
|
|
749
|
+
severity: level,
|
|
750
|
+
message: msg,
|
|
751
|
+
event: meta,
|
|
752
|
+
service: 'my-service',
|
|
753
|
+
environment: process.env.NODE_ENV
|
|
754
|
+
})
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
### Integration Examples
|
|
764
|
+
|
|
765
|
+
#### Axios
|
|
766
|
+
|
|
767
|
+
```typescript
|
|
768
|
+
import axios from 'axios';
|
|
769
|
+
import ssrfAgentGuard from 'ssrf-agent-guard';
|
|
770
|
+
|
|
771
|
+
const options = {
|
|
772
|
+
policy: { denyTLD: ['local'] },
|
|
773
|
+
logger: console.log
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const url = 'https://api.example.com/data';
|
|
777
|
+
|
|
778
|
+
const response = await axios.get(url, {
|
|
779
|
+
httpAgent: ssrfAgentGuard('http', options),
|
|
780
|
+
httpsAgent: ssrfAgentGuard('https', options)
|
|
781
|
+
});
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
#### node-fetch
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
import fetch from 'node-fetch';
|
|
788
|
+
import ssrfAgentGuard from 'ssrf-agent-guard';
|
|
789
|
+
|
|
790
|
+
const url = 'https://api.example.com/data';
|
|
791
|
+
|
|
792
|
+
const response = await fetch(url, {
|
|
793
|
+
agent: ssrfAgentGuard(url)
|
|
794
|
+
});
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
#### Got
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
import got from 'got';
|
|
801
|
+
import ssrfAgentGuard from 'ssrf-agent-guard';
|
|
802
|
+
|
|
803
|
+
const options = { mode: 'block' as const };
|
|
804
|
+
|
|
805
|
+
const response = await got('https://api.example.com/data', {
|
|
806
|
+
agent: {
|
|
807
|
+
http: ssrfAgentGuard('http', options),
|
|
808
|
+
https: ssrfAgentGuard('https', options)
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
#### Undici (fetch)
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
import { fetch, Agent } from 'undici';
|
|
817
|
+
import ssrfAgentGuard, { validateHost } from 'ssrf-agent-guard';
|
|
818
|
+
|
|
819
|
+
// For Undici, use validateHost for pre-request validation
|
|
820
|
+
const url = 'https://api.example.com/data';
|
|
821
|
+
const parsedUrl = new URL(url);
|
|
822
|
+
|
|
823
|
+
const validation = validateHost(parsedUrl.hostname);
|
|
824
|
+
if (!validation.safe) {
|
|
825
|
+
throw new Error(`SSRF blocked: ${validation.reason}`);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const response = await fetch(url);
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
#### Express Middleware
|
|
832
|
+
|
|
833
|
+
```typescript
|
|
834
|
+
import express from 'express';
|
|
835
|
+
import axios from 'axios';
|
|
836
|
+
import ssrfAgentGuard from 'ssrf-agent-guard';
|
|
837
|
+
|
|
838
|
+
const app = express();
|
|
839
|
+
|
|
840
|
+
app.post('/fetch-url', async (req, res) => {
|
|
841
|
+
const { url } = req.body;
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
const response = await axios.get(url, {
|
|
845
|
+
httpAgent: ssrfAgentGuard('http'),
|
|
846
|
+
httpsAgent: ssrfAgentGuard('https'),
|
|
847
|
+
timeout: 5000
|
|
848
|
+
});
|
|
849
|
+
res.json({ data: response.data });
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (error.message.includes('not allowed')) {
|
|
852
|
+
res.status(403).json({ error: 'URL not allowed' });
|
|
853
|
+
} else {
|
|
854
|
+
res.status(500).json({ error: 'Request failed' });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
## Error Messages
|
|
863
|
+
|
|
864
|
+
When a request is blocked in `'block'` mode, the following error messages are thrown:
|
|
865
|
+
|
|
866
|
+
| Block Reason | Error Message |
|
|
867
|
+
|--------------|---------------|
|
|
868
|
+
| `private_ip` | `Private IP address {target} is not allowed` |
|
|
869
|
+
| `cloud_metadata` | `Cloud metadata endpoint {target} is not allowed` |
|
|
870
|
+
| `invalid_domain` | `Invalid domain {target}` |
|
|
871
|
+
| `dns_rebinding` | `DNS rebinding attack detected for {hostname} -> {ip}` |
|
|
872
|
+
| `denied_domain` | `Domain {target} is denied by policy` |
|
|
873
|
+
| `denied_tld` | `TLD of {target} is denied by policy` |
|
|
874
|
+
| `not_allowed_domain` | `Domain {target} is not in the allowed list` |
|
|
875
|
+
|
|
876
|
+
---
|
|
877
|
+
|
|
878
|
+
## TypeScript Support
|
|
879
|
+
|
|
880
|
+
This library is written in TypeScript and includes full type definitions. All types are exported from the main module:
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
import ssrfAgentGuard, {
|
|
884
|
+
Options,
|
|
885
|
+
PolicyOptions,
|
|
886
|
+
BlockEvent,
|
|
887
|
+
BlockReason,
|
|
888
|
+
ValidationResult,
|
|
889
|
+
validateHost,
|
|
890
|
+
isCloudMetadata,
|
|
891
|
+
validatePolicy,
|
|
892
|
+
matchesDomain,
|
|
893
|
+
getTLD,
|
|
894
|
+
CLOUD_METADATA_HOSTS
|
|
895
|
+
} from 'ssrf-agent-guard';
|
|
896
|
+
```
|
package/README.md
CHANGED
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
8
|
* Block requests to internal/private IPs
|
|
9
|
-
* Detect and block cloud provider metadata endpoints (AWS, GCP, Azure)
|
|
9
|
+
* Detect and block cloud provider metadata endpoints (AWS, GCP, Azure, Oracle, DigitalOcean, Kubernetes)
|
|
10
10
|
* DNS rebinding detection
|
|
11
|
+
* Policy-based domain filtering (allowlists, denylists, TLD blocking)
|
|
12
|
+
* Multiple operation modes (block, report, allow)
|
|
13
|
+
* Custom logging support
|
|
11
14
|
* Fully written in TypeScript with type definitions
|
|
12
15
|
|
|
16
|
+
## Documentation
|
|
17
|
+
|
|
18
|
+
For complete API documentation, see [API.md](./API.md).
|
|
19
|
+
|
|
13
20
|
---
|
|
14
21
|
|
|
15
22
|
## Installation
|
|
@@ -24,20 +31,15 @@ yarn add ssrf-agent-guard
|
|
|
24
31
|
|
|
25
32
|
## Usage
|
|
26
33
|
|
|
27
|
-
`isValidDomainOptions` reference [is-valid-domain](https://github.com/miguelmota/is-valid-domain)
|
|
28
|
-
|
|
29
34
|
### axios
|
|
30
35
|
|
|
31
36
|
```ts
|
|
32
37
|
const ssrfAgentGuard = require('ssrf-agent-guard');
|
|
33
38
|
const url = 'https://127.0.0.1'
|
|
34
|
-
const isValidDomainOptions = {
|
|
35
|
-
subdomain: true,
|
|
36
|
-
wildcard: true
|
|
37
|
-
};
|
|
38
39
|
axios.get(
|
|
39
40
|
url, {
|
|
40
|
-
httpAgent: ssrfAgentGuard(url
|
|
41
|
+
httpAgent: ssrfAgentGuard(url),
|
|
42
|
+
httpsAgent: ssrfAgentGuard(url)
|
|
41
43
|
})
|
|
42
44
|
.then((response) => {
|
|
43
45
|
console.log(`Success`);
|
|
@@ -55,12 +57,8 @@ axios.get(
|
|
|
55
57
|
```ts
|
|
56
58
|
const ssrfAgentGuard = require('ssrf-agent-guard');
|
|
57
59
|
const url = 'https://127.0.0.1'
|
|
58
|
-
const isValidDomainOptions = {
|
|
59
|
-
subdomain: true,
|
|
60
|
-
wildcard: true
|
|
61
|
-
};
|
|
62
60
|
fetch(url, {
|
|
63
|
-
agent: ssrfAgentGuard(url
|
|
61
|
+
agent: ssrfAgentGuard(url)
|
|
64
62
|
})
|
|
65
63
|
.then((response) => {
|
|
66
64
|
console.log(`Success`);
|
|
@@ -70,6 +68,26 @@ fetch(url, {
|
|
|
70
68
|
});
|
|
71
69
|
```
|
|
72
70
|
|
|
71
|
+
### Advanced Configuration
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const ssrfAgentGuard = require('ssrf-agent-guard');
|
|
75
|
+
|
|
76
|
+
const agent = ssrfAgentGuard('https://api.example.com', {
|
|
77
|
+
mode: 'block', // 'block' | 'report' | 'allow'
|
|
78
|
+
blockCloudMetadata: true, // Block AWS/GCP/Azure metadata endpoints
|
|
79
|
+
detectDnsRebinding: true, // Detect DNS rebinding attacks
|
|
80
|
+
policy: {
|
|
81
|
+
allowDomains: ['*.trusted.com'], // Only allow these domains
|
|
82
|
+
denyDomains: ['evil.com'], // Block these domains
|
|
83
|
+
denyTLD: ['local', 'internal'] // Block these TLDs
|
|
84
|
+
},
|
|
85
|
+
logger: (level, msg, meta) => {
|
|
86
|
+
console.log(`[${level}] ${msg}`, meta);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
73
91
|
---
|
|
74
92
|
|
|
75
93
|
## Development
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssrf-agent-guard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "A TypeScript SSRF protection library for Node.js (express/axios) with advanced policies, DNS rebinding detection and cloud metadata protection.",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|