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.
Files changed (3) hide show
  1. package/API.md +896 -0
  2. package/README.md +31 -13
  3. 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, isValidDomainOptions), httpsAgent: ssrfAgentGuard(url, isValidDomainOptions)
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, isValidDomainOptions)
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.7",
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",