Skip to content

Commit b5f2f2c

Browse files
authored
Security Fix Blind SSRF (#82)
* fix for blind ssrf * add tests * Improve JSDoc documentation for security features * update browser tests * update integration tests * add dns lookup when in node-like environments * security documentation
1 parent d5951c3 commit b5f2f2c

File tree

7 files changed

+1294
-167
lines changed

7 files changed

+1294
-167
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ A modern, TypeScript-based WebFinger client that runs in both browsers and Node.
1010
## Features
1111

1212
**Modern ES6+ support** - Built with TypeScript, works with modern JavaScript
13-
🔒 **Security-first** - Defaults to TLS-only connections
13+
🔒 **Security-first** - SSRF protection, blocks private/internal addresses by default
14+
🛡️ **Production-ready** - Prevents localhost/LAN access per ActivityPub security guidelines
1415
🔄 **Flexible fallbacks** - Supports host-meta and WebFist fallback mechanisms
1516
🌐 **Universal** - Works in browsers and Node.js
1617
📦 **Zero dependencies** - Lightweight and self-contained
@@ -64,6 +65,11 @@ bun run lint # Code linting
6465

6566
See the [Development Guide](docs/DEVELOPMENT.md) for detailed testing information and individual test commands.
6667

68+
## Security
69+
70+
webfinger.js includes comprehensive SSRF protection, blocking private networks and validating redirects by default. For detailed security information, see **[Security Documentation](docs/SECURITY.md)**.
71+
72+
6773
## Contributing
6874

6975
Contributions are welcome! Please see the [Development Guide](docs/DEVELOPMENT.md) for setup instructions, coding guidelines, and contribution workflow.

docs/SECURITY.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Security
2+
3+
webfinger.js prioritizes security and includes comprehensive protection against common attack vectors that can affect WebFinger implementations.
4+
5+
## SSRF Protection
6+
7+
This library includes robust protection against Server-Side Request Forgery (SSRF) attacks by default:
8+
9+
- **Private address blocking**: Prevents requests to localhost, private IP ranges, and internal networks
10+
- **DNS resolution protection**: Resolves domain names in Node.js environments to block domains that resolve to private IPs
11+
- **Path injection prevention**: Validates host formats to prevent directory traversal attacks
12+
- **Redirect validation**: Prevents redirect-based SSRF attacks to private networks
13+
- **ActivityPub compliance**: Follows [ActivityPub security guidelines](https://www.w3.org/TR/activitypub/#security-considerations) (Section B.3)
14+
15+
### Blocked Addresses
16+
17+
The following address ranges are blocked by default to prevent SSRF attacks:
18+
19+
#### Localhost
20+
- `localhost`, `localhost.localdomain`
21+
- `127.x.x.x` (IPv4 loopback)
22+
- `::1` (IPv6 loopback)
23+
24+
#### Private IPv4 Ranges
25+
- `10.x.x.x` (Class A private)
26+
- `172.16.x.x` - `172.31.x.x` (Class B private)
27+
- `192.168.x.x` (Class C private)
28+
29+
#### Link-Local Addresses
30+
- `169.254.x.x` (IPv4 link-local)
31+
- `fe80::/10` (IPv6 link-local)
32+
33+
#### Multicast Addresses
34+
- `224.x.x.x` - `239.x.x.x` (IPv4 multicast)
35+
- `ff00::/8` (IPv6 multicast)
36+
37+
### DNS Resolution Protection
38+
39+
In Node.js environments, the library performs DNS resolution to prevent attacks using domains that resolve to private IP addresses:
40+
41+
- **Domain resolution**: All domain names are resolved to IP addresses before making requests
42+
- **Private IP detection**: Resolved IPs are checked against the private address blacklist
43+
- **Attack prevention**: Blocks requests to public domains like `localtest.me` that resolve to `127.0.0.1`
44+
- **Browser compatibility**: DNS resolution is skipped in browser environments where it's not available
45+
46+
**Example blocked domains:**
47+
- `localtest.me``127.0.0.1` (blocked)
48+
- `10.0.0.1.nip.io``10.0.0.1` (blocked)
49+
- Custom domains configured to resolve to private networks
50+
51+
**Note**: This protection only applies in Node.js environments. Browser environments rely on the browser's built-in protections against private network access.
52+
53+
### Redirect Protection
54+
55+
The library implements manual redirect handling to validate redirect destinations:
56+
57+
- **Redirect limits**: Maximum of 3 redirects to prevent redirect loops
58+
- **Destination validation**: All redirect targets are checked against the private address blacklist
59+
- **Malformed response handling**: Invalid or missing Location headers are rejected
60+
- **URL validation**: Redirect URLs are parsed and validated before following
61+
62+
This prevents attacks where a public domain's WebFinger endpoint redirects to private network resources.
63+
64+
## Development Override
65+
66+
⚠️ **CAUTION**: The following configuration should **ONLY** be used in development or testing environments!
67+
68+
```typescript
69+
const webfinger = new WebFinger({
70+
allow_private_addresses: true // Disables SSRF protection - DANGEROUS in production!
71+
});
72+
73+
// This will now work (but should never be used in production)
74+
await webfinger.lookup('user@localhost:3000');
75+
```
76+
77+
### When to Use Development Override
78+
79+
- **Local development**: Testing against localhost services
80+
- **Internal testing**: Validating against private network services
81+
- **Unit testing**: Creating controlled test environments
82+
83+
### Production Security
84+
85+
**Never** set `allow_private_addresses: true` in production environments. This completely disables SSRF protection and opens your application to serious security vulnerabilities.
86+
87+
## Security Best Practices
88+
89+
When integrating webfinger.js into your application:
90+
91+
1. **Keep defaults**: Use the default secure configuration in production
92+
2. **Validate inputs**: Always validate user-provided addresses before lookup
93+
3. **Handle errors gracefully**: Don't expose internal network details in error messages
94+
4. **Monitor requests**: Log WebFinger lookups for security monitoring
95+
5. **Update regularly**: Keep the library updated to receive security patches
96+
97+
## Reporting Security Issues
98+
99+
If you discover a security vulnerability in webfinger.js, please report it responsibly:
100+
101+
1. **Do not** create a public GitHub issue
102+
2. Email security concerns to the maintainer
103+
3. Include detailed reproduction steps
104+
4. Allow reasonable time for fixes before public disclosure
105+
106+
## Compliance
107+
108+
This library's security implementation follows:
109+
110+
- [ActivityPub Security Considerations](https://www.w3.org/TR/activitypub/#security-considerations) (Section B.3)
111+
- [RFC 7033 WebFinger](https://tools.ietf.org/html/rfc7033) security guidelines
112+
- Common SSRF prevention best practices
113+
114+
The security model is designed to be safe by default while providing necessary flexibility for legitimate use cases.

spec/browser/browser.integration.js

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ describe('WebFinger Browser Tests', () => {
2626
webfinger = new WebFinger({
2727
webfist_fallback: true,
2828
uri_fallback: true,
29-
request_timeout: 5000
29+
request_timeout: 5000,
30+
allow_private_addresses: true // Allow localhost for browser tests
3031
});
3132
});
3233

@@ -107,7 +108,8 @@ describe('WebFinger Browser Tests', () => {
107108
it('should perform successful WebFinger lookup with mock server', async () => {
108109
const wf = new WebFinger({
109110
tls_only: false,
110-
request_timeout: 5000
111+
request_timeout: 5000,
112+
allow_private_addresses: true
111113
});
112114

113115
const result = await wf.lookup(`test@localhost:${serverPort}`);
@@ -120,7 +122,8 @@ describe('WebFinger Browser Tests', () => {
120122
it('should handle server errors gracefully', async () => {
121123
const wf = new WebFinger({
122124
tls_only: false,
123-
request_timeout: 5000
125+
request_timeout: 5000,
126+
allow_private_addresses: true
124127
});
125128

126129
try {
@@ -150,7 +153,8 @@ describe('WebFinger Browser Tests', () => {
150153
it('should return properly structured JRD response', async () => {
151154
const wf = new WebFinger({
152155
tls_only: false,
153-
request_timeout: 5000
156+
request_timeout: 5000,
157+
allow_private_addresses: true
154158
});
155159

156160
const result = await wf.lookup(`test@localhost:${serverPort}`);
@@ -170,7 +174,8 @@ describe('WebFinger Browser Tests', () => {
170174
it('should handle JRD with properties', async () => {
171175
const wf = new WebFinger({
172176
tls_only: false,
173-
request_timeout: 5000
177+
request_timeout: 5000,
178+
allow_private_addresses: true
174179
});
175180

176181
const result = await wf.lookup(`user@localhost:${serverPort}`);
@@ -198,7 +203,8 @@ describe('WebFinger Browser Tests', () => {
198203
it('should find specific link relations', async () => {
199204
const wf = new WebFinger({
200205
tls_only: false,
201-
request_timeout: 5000
206+
request_timeout: 5000,
207+
allow_private_addresses: true
202208
});
203209

204210
const profileLink = await wf.lookupLink(`test@localhost:${serverPort}`, 'profile');
@@ -211,7 +217,8 @@ describe('WebFinger Browser Tests', () => {
211217
it('should return null for non-existent link relations', async () => {
212218
const wf = new WebFinger({
213219
tls_only: false,
214-
request_timeout: 5000
220+
request_timeout: 5000,
221+
allow_private_addresses: true
215222
});
216223

217224
try {
@@ -242,7 +249,8 @@ describe('WebFinger Browser Tests', () => {
242249
const wf = new WebFinger({
243250
tls_only: false,
244251
uri_fallback: true,
245-
request_timeout: 5000
252+
request_timeout: 5000,
253+
allow_private_addresses: true
246254
});
247255

248256
const result = await wf.lookup(`test@localhost:${serverPort}`);
@@ -256,7 +264,8 @@ describe('WebFinger Browser Tests', () => {
256264

257265
const wf = new WebFinger({
258266
tls_only: false,
259-
request_timeout: 1000
267+
request_timeout: 1000,
268+
allow_private_addresses: true
260269
});
261270

262271
try {
@@ -267,6 +276,37 @@ describe('WebFinger Browser Tests', () => {
267276
}
268277
});
269278
});
279+
280+
describe('Security Features', () => {
281+
it('should block private addresses when allow_private_addresses is false', async () => {
282+
const secureWf = new WebFinger({
283+
tls_only: false,
284+
allow_private_addresses: false, // Security enabled
285+
request_timeout: 1000
286+
});
287+
288+
try {
289+
await secureWf.lookup('test@localhost:8080');
290+
throw new Error('Should have thrown');
291+
} catch (err) {
292+
expect(err.message).to.include('private or internal addresses are not allowed');
293+
}
294+
});
295+
296+
it('should block private IPv4 addresses', async () => {
297+
const secureWf = new WebFinger({
298+
allow_private_addresses: false,
299+
request_timeout: 1000
300+
});
301+
302+
try {
303+
await secureWf.lookup('[email protected]');
304+
throw new Error('Should have thrown');
305+
} catch (err) {
306+
expect(err.message).to.include('private or internal addresses are not allowed');
307+
}
308+
});
309+
});
270310
});
271311

272312
// Mock server creation function

spec/integration/local-server.integration.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ describe('WebFinger Controlled Tests', () => {
1616
tls_only: false, // Use HTTP for test server
1717
webfist_fallback: false,
1818
uri_fallback: true,
19-
request_timeout: 5000
19+
request_timeout: 5000,
20+
allow_private_addresses: true // Allow localhost for integration tests
2021
});
2122
});
2223

@@ -71,7 +72,8 @@ describe('WebFinger Controlled Tests', () => {
7172
tls_only: false,
7273
webfist_fallback: false,
7374
uri_fallback: false, // Disable fallbacks to test direct 404
74-
request_timeout: 5000
75+
request_timeout: 5000,
76+
allow_private_addresses: true // Allow localhost for integration tests
7577
});
7678

7779
await expect(noFallbackWebfinger.lookup(`nonexistent@localhost:${serverPort}`))
@@ -95,7 +97,8 @@ describe('WebFinger Controlled Tests', () => {
9597
tls_only: false,
9698
uri_fallback: true,
9799
webfist_fallback: false,
98-
request_timeout: 3000
100+
request_timeout: 3000,
101+
allow_private_addresses: true // Allow localhost for integration tests
99102
});
100103

101104
// Should work even if first endpoint fails
@@ -108,7 +111,8 @@ describe('WebFinger Controlled Tests', () => {
108111
tls_only: false,
109112
uri_fallback: false,
110113
webfist_fallback: false,
111-
request_timeout: 3000
114+
request_timeout: 3000,
115+
allow_private_addresses: true // Allow localhost for integration tests
112116
});
113117

114118
const result = await noFallbackWf.lookup(`test@localhost:${serverPort}`);

0 commit comments

Comments
 (0)