Skip to content

Commit 39095e5

Browse files
committed
docs: update specs, README, and AGENTS.md for SSRF protection
- specs/initial.md: add SSRF prevention section, update error messages, add block_private_ips to tool builder, add SSRF test requirements - specs/fetchers.md: add dns_policy to FetchOptions, add SSRF section, add dns.rs to module structure - AGENTS.md: add threat-model.md to available specs list - README.md: add SSRF protection to features, add Security section with usage examples https://claude.ai/code/session_011H7YzDQ8VXbaNT7nXGQgmE
1 parent 2ea0797 commit 39095e5

4 files changed

Lines changed: 57 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Available specs:
3535
- `specs/initial.md` - WebFetch tool specification (types, behavior, conversions, error handling)
3636
- `specs/fetchers.md` - Pluggable fetcher system for URL-specific handling
3737
- `specs/maintenance.md` - Periodic maintenance checklist (deps, docs, spec-code alignment)
38+
- `specs/threat-model.md` - Security threat model (SSRF, network, input validation, DoS)
3839

3940
Specification format: Abstract and Requirements sections.
4041

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ AI-friendly web content fetching tool designed for LLM consumption. Rust library
1010
- **Binary detection** - Returns metadata only for images, PDFs, etc.
1111
- **Timeout handling** - 1s first-byte, 30s body with partial content on timeout
1212
- **URL filtering** - Allow/block lists for controlled access
13+
- **SSRF protection** - Resolve-then-check blocks private IPs by default
1314
- **MCP server** - Model Context Protocol support for AI tool integration
1415

1516
## Installation
@@ -171,6 +172,26 @@ Errors are returned in the `error` field:
171172
- `ContentError` - Failed to read body
172173
- `BinaryContent` - Binary content not supported
173174

175+
## Security
176+
177+
FetchKit blocks connections to private/reserved IP ranges by default, preventing SSRF attacks when used in server-side or AI agent contexts.
178+
179+
**Blocked by default:** loopback, private networks (10.x, 172.16-31.x, 192.168.x), link-local (169.254.x including cloud metadata), IPv6 equivalents, multicast, and other reserved ranges.
180+
181+
```rust
182+
// Default: private IPs blocked (safe for production)
183+
let tool = Tool::default();
184+
185+
// Explicit opt-out for local development only
186+
let tool = Tool::builder()
187+
.block_private_ips(false)
188+
.build();
189+
```
190+
191+
DNS pinning prevents DNS rebinding attacks. IPv6-mapped IPv4 addresses are canonicalized before validation.
192+
193+
See [`specs/threat-model.md`](specs/threat-model.md) for the full threat model.
194+
174195
## Configuration
175196

176197
### Timeouts

specs/fetchers.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Fetchers receive `FetchOptions` for:
6666
- `block_prefixes` - URL prefix block list
6767
- `enable_markdown` - Enable markdown conversion
6868
- `enable_text` - Enable text conversion
69+
- `dns_policy` - DNS resolution policy for SSRF prevention (default: block private IPs)
6970

7071
### Extensibility
7172

@@ -82,10 +83,20 @@ Design supports hundreds of fetchers by:
8283
- `FetchError::FetcherError(String)` for fetcher-specific errors
8384
- GitHub API errors return response with error field set
8485

86+
### SSRF Protection
87+
88+
Both built-in fetchers integrate resolve-then-check DNS validation:
89+
- Resolve hostname to IP before connecting
90+
- Validate IP against blocked ranges (private, loopback, link-local, etc.)
91+
- Pin validated IP via `reqwest::ClientBuilder::resolve()` to prevent DNS rebinding
92+
- Enabled by default via `DnsPolicy::default()` (blocks private IPs)
93+
- See `specs/threat-model.md` for threat IDs: TM-SSRF-001 through TM-SSRF-010
94+
8595
## Module Structure
8696

8797
```
8898
crates/fetchkit/src/
99+
├── dns.rs # DnsPolicy - SSRF prevention via resolve-then-check
89100
├── fetchers/
90101
│ ├── mod.rs # Fetcher trait, FetcherRegistry
91102
│ ├── default.rs # DefaultFetcher

specs/initial.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Provide a builder to configure tool options, including:
4848
- Support allow/block list of URL prefixes.
4949
- Support enabling/disabling request options (feature gating).
5050
- Support User-Agent override (e.g., `allow_ua`).
51+
- Support `block_private_ips(bool)` for SSRF prevention (default: `true`).
5152

5253
#### Types
5354

@@ -74,6 +75,7 @@ Provide a builder to configure tool options, including:
7475
- Missing url
7576
- Invalid url scheme
7677
- Invalid method
78+
- Blocked URL (prefix list or DNS policy)
7779
- Client build failure
7880
- Request error (timeout/connect/other)
7981
- `ToolStatus` (or equivalent)
@@ -134,6 +136,17 @@ Provide a builder to configure tool options, including:
134136
- If allow list is non-empty, URL must match at least one allow prefix.
135137
- If block list matches, request is denied even if allow list matches.
136138

139+
### SSRF Prevention (DNS Policy)
140+
141+
By default, FetchKit blocks connections to private/reserved IP ranges:
142+
- Resolves hostnames to IP addresses before connecting (resolve-then-check).
143+
- Validates resolved IPs against blocked ranges (loopback, private, link-local,
144+
cloud metadata, carrier-grade NAT, documentation, benchmarking, multicast, broadcast).
145+
- Handles IPv6-mapped IPv4 addresses via canonicalization.
146+
- Pins validated IP via `reqwest::ClientBuilder::resolve()` to prevent DNS rebinding.
147+
- Blocked by default; opt out via `ToolBuilder::block_private_ips(false)`.
148+
- See `specs/threat-model.md` for full threat analysis.
149+
137150
### HTTP Behavior
138151

139152
- User-Agent: configurable via tool builder or CLI/MCP/Python options
@@ -237,7 +250,7 @@ Content is HTML if:
237250
- Missing url -> tool error string "Missing required parameter: url".
238251
- Invalid URL -> tool error string "Invalid URL: must start with http:// or https://".
239252
- Invalid method -> tool error string "Invalid method: must be GET or HEAD".
240-
- Blocked prefix -> tool error string "Blocked URL: prefix not allowed".
253+
- Blocked URL (prefix or DNS policy) -> tool error string "Blocked URL: not allowed by policy".
241254
- First-byte timeout -> "Request timed out: server did not respond within 1 second".
242255
- Connect error -> "Failed to connect to server".
243256
- Other request errors -> "Request failed: <error>".
@@ -263,6 +276,7 @@ Unit:
263276
- Binary content detection.
264277
- HTML conversion, entity decoding.
265278
- Newline filtering behavior.
279+
- DNS policy IP range blocking (IPv4, IPv6, mapped addresses).
266280

267281
Integration (mock HTTP server):
268282
- GET/HEAD with expected fields.
@@ -272,3 +286,12 @@ Integration (mock HTTP server):
272286
- Last-Modified extraction.
273287
- Size correctness for text and binary.
274288
- Body timeout truncation.
289+
290+
SSRF security:
291+
- Private IP blocking (loopback, 10.x, 172.16.x, 192.168.x).
292+
- Cloud metadata endpoint blocking (169.254.169.254).
293+
- IPv6 loopback/mapped address blocking.
294+
- Non-HTTP scheme blocking (file, ftp, data, gopher).
295+
- Default-blocks-loopback verification.
296+
- Explicit opt-out verification.
297+
- Script stripping in converted content.

0 commit comments

Comments
 (0)