A multi-platform SSRF prevention library with a fuzz-tested Rust core, pluggable protocol transports, pluggable audit logging, and bindings for Go, Python, and Node.js.
ressrf (pronounced "resurf") validates network destinations against configurable deny/allow policies before any connection is made. It blocks access to private networks, cloud metadata endpoints (AWS IMDS, Azure Wireserver, GCP metadata), link-local addresses, and other internal ranges by default. Protocol adapters integrate at DNS resolution and connection establishment, providing transparent protection for HTTP clients, TCP connections, and SSH sessions.
- Deny-first policy engine with presets, custom allow/deny CIDR lists, URL rules, and cloud provider modules
- Default deny list sourced from IANA special-purpose registries, kept fresh by automated monthly updates
- Protocol adapters for HTTP (redirect re-validation), TCP (DNS-pinned dialing), and SSH in every language
- Pluggable audit logging via simple callback interface, consistent across all bindings
- Cross-language conformance guaranteed by shared JSON test vectors
- Fuzz-tested with cargo-fuzz (weekly CI runs)
| Package | Language | Integration | Docs |
|---|---|---|---|
ressrf-core |
Rust | Direct dependency | README |
ressrf-tcp |
Rust | DNS-pinned TCP dialing | README |
ressrf-http |
Rust | Tower Layer/Service | README |
ressrf-ssh |
Rust | Guard for russh/async-ssh2 | README |
ressrf-tracing |
Rust | TracingSink audit adapter | README |
ressrf-wasm |
Rust | WASM ABI for Go/Node.js | README |
go/ressrf |
Go | wazero WASM runtime | README |
go-native/ressrf |
Go | Native port (no WASM, no CGO) | README |
python/ |
Python | PyO3 native extension | README |
node/ |
TypeScript | WebAssembly API | README |
┌─────────────────────────┐
│ ressrf-core │
│ (policy, CIDR, URI, │
│ audit, cloud, trie) │ ┌──────────────────────┐
└───────┬─────────────────┘ │ Go (native) │
│ │ pure-Go port │
┌─────────────────┼─────────────────┐ │ │
│ │ │ │ Consumes only the │
▼ ▼ ▼ │ shared JSON: │
┌────────────────┐ ┌─────────────┐ ┌────────────────┐ │ • config/*.json │
│ ressrf-wasm │ │ ressrf-http │ │ ressrf-ssh │ │ • tests/vectors │
│ (WASM ABI) │ │ (Tower) │ │ (russh) │ │ │
└───────┬────────┘ └──────┬──────┘ └───────┬────────┘ │ No Rust dependency; │
│ │ │ │ pinned to ressrf- │
┌─────┴─────┐ └───────┬─────────┘ │ core via differen- │
│ │ │ │ tial fuzz against │
▼ ▼ ▼ │ ressrf-wasm. │
┌──────────┐ ┌──────────┐ ┌──────────────┐ └──────────────────────┘
│Go(wazero)│ │ Node.js │ │ Python │
│ binding │ │ (WASM) │ │ (PyO3) │
└──────────┘ └──────────┘ └──────────────┘
The native Go port lives at go-native/ressrf/. It's useful for Go shops that prefer native debuggability (pprof, delve) and a contribution flow that doesn't pull the Rust toolchain into PRs.
use ressrf_core::{PolicyBuilder, UriValidator};
let policy = PolicyBuilder::external_only().build();
assert!(policy.is_network_allowed(&["10.0.0.1".parse().unwrap()]).is_err());
assert!(policy.is_network_allowed(&["93.184.216.34".parse().unwrap()]).is_ok());policy, _ := ressrf.NewPolicyBuilder(ressrf.PresetExternalOnly).
WithCloudProviders("aws", "azure", "gcp").
Build(ctx)
defer policy.Close(ctx)
err := policy.IsAllowed(ctx, "http://169.254.169.254/latest/meta-data/")
// err: blockedpolicy, _ := ressrf.NewPolicy(ressrf.PresetExternalOnly,
ressrf.WithCloudProviderDenies(ressrf.CloudAWS, ressrf.CloudAzure, ressrf.CloudGCP),
)
err := policy.IsAllowed(ctx, "http://169.254.169.254/latest/meta-data/")
// err: blockedfrom ressrf import Policy, RessrfBlockedError
policy = Policy.external_only(cloud=["aws"])
try:
policy.validate_url("http://169.254.169.254/latest/meta-data/")
except RessrfBlockedError as e:
print(f"Blocked: {e.reason}")import { Policy, isBlocked } from "ressrf";
const policy = await Policy.externalOnly({ cloud: ["aws"] });
try {
policy.isAllowed("http://169.254.169.254/latest/meta-data/");
} catch (err) {
if (isBlocked(err)) console.log("Blocked:", err.reason);
}| Language | Command | Requirements |
|---|---|---|
| Rust | cargo add ressrf-core |
Rust 1.75+ |
| Go (wazero) | go get github.com/timescale/ressrf/go/ressrf |
Go 1.26+ |
| Go (native) | go get github.com/timescale/ressrf/go-native/ressrf |
Go 1.25+ |
| Python | pip install ressrf |
Python 3.10+ |
| Node.js | npm install ressrf |
Node.js 20+ |
See each package's README for optional extras (protocol adapters, feature flags).
Allow overrides deny. Punch holes for specific CIDRs while keeping the rest of private address space blocked:
// Rust
PolicyBuilder::external_only().add_allowed(&["10.42.0.0/16"]).build();// Go (wazero)
NewPolicyBuilder(PresetExternalOnly).WithAllowedCIDRs("10.42.0.0/16").Build(ctx)// Go (native)
ressrf.NewPolicy(ressrf.PresetExternalOnly, ressrf.WithAllowedCIDRs("10.42.0.0/16"))# Python
Policy.external_only(allowed=["10.42.0.0/16"])// Node.js
await Policy.externalOnly({ allowCidrs: ["10.42.0.0/16"] });For URL-level allow/deny beyond CIDR-based filtering. Rules use glob patterns for host (* = single DNS label) and path (* = single segment, ** = any depth), with optional regex for complex patterns:
// Rust
PolicyBuilder::external_only()
.url_allow(UrlRule::glob("https", "*.stripe.com", "/v1/**"))
.url_deny(UrlRule::host("*.internal"))
.build();// Go (wazero)
NewPolicyBuilder(PresetExternalOnly).
WithURLAllow(URLRule{Scheme: "https", Host: "*.stripe.com", Path: "/v1/**"}).
WithURLDeny(URLRule{Host: "*.internal"}).
Build(ctx)// Go (native)
ressrf.NewPolicy(ressrf.PresetExternalOnly,
ressrf.WithURLAllow(ressrf.URLRuleGlob("https", "*.stripe.com", "/v1/**")),
ressrf.WithURLDeny(ressrf.URLRuleGlob("", "*.internal", "")),
)# Python
PolicyBuilder("external_only") \
.url_allow(scheme="https", host="*.stripe.com", path="/v1/**") \
.url_deny(host="*.internal") \
.build()// Node.js
new PolicyBuilder("external_only")
.urlAllow({ scheme: "https", host: "*.stripe.com", path: "/v1/**" })
.urlDeny({ host: "*.internal" })
.build();Deny rules are checked first. When allow rules are configured, URLs not matching any allow rule are blocked. Set bypass_ip_check: true on an allow rule to skip the IP-level check for trusted endpoints.
All bindings expose the same pluggable interface. The library emits structured events but never dictates which logging framework to use:
// Rust: implement the AuditSink trait
let policy = PolicyBuilder::external_only()
.audit_sink(Box::new(my_sink))
.build();// Go: any function works
sink := ressrf.AuditFunc(func(ctx context.Context, e *ressrf.AuditEvent) {
slog.InfoContext(ctx, "ressrf", "kind", e.Kind)
})# Python: any callable works
sink = AuditFunc(lambda event: print(f"[{event.event_type}] {event.fields}"))// Node.js: any object with emit() works
const sink = new AuditFunc((event) => console.log(event.kind, event.fields));scripts/generate_ip_ranges.py fetches upstream IP range data from IANA, AWS, Azure, and GCP. A monthly CI workflow validates changes and opens a PR automatically. Service ranges are queryable at runtime via ServiceRangeTable (trie-backed, O(log n) lookups).
python scripts/generate_ip_ranges.py # full update
python scripts/generate_ip_ranges.py --iana-only # skip cloud service ranges
python scripts/generate_ip_ranges.py --validate-onlyShared test vectors in tests/vectors/ ensure identical behavior across all languages, including a 92-case ssrf_techniques.json covering the full SSRF bypass technique taxonomy (IP representation tricks, IPv6 variants, parser confusion, protocol smuggling, cloud metadata, Unicode/IDN, and more):
cargo test --workspace --all-features # Rust
cd go/ressrf && go test -race ./... # Go (wazero)
cd go-native/ressrf && go test -race ./... # Go (native)
cd python && uv run pytest tests/ -v # Python
cd node && npx tsx --test tests/*.test.ts # Node.jsA Tier 2 end-to-end suite under crates/ressrf-tcp/tests/ssrf_e2e.rs exercises DNS-rebinding pinning and redirect chains through the full network stack using CoreDNS and WireMock containers (tests/containers/). It is gated behind the e2e Cargo feature and requires Docker; tests skip with a notice when Docker is unavailable:
cargo test --features e2e -p ressrf-tcp --test ssrf_e2e- Rust: check, fmt, clippy, test (Linux/macOS/Windows), WASM build
- Go (wazero + native): test (multi-OS, race detector), vet, golangci-lint; the native port additionally runs a differential-fuzz job against the wazero binding's WASM oracle
- Python: pytest (multi-OS), ruff, ty
- Node.js: node:test (multi-OS), tsc
- SSRF e2e: Linux-only Tier 2 job spins up CoreDNS + WireMock to verify DNS-based and redirect-based bypasses end-to-end
- Security: cargo audit, govulncheck, cargo-fuzz (weekly), zizmor
- IP ranges: monthly upstream fetch, validate, test, auto-PR
If you would like to see integration with your favorite programming language, cloud provider, protocol, or library, feel free to open an issue or submit a pull request. Contributions of all kinds are welcome.
See HACKING.md for development setup, testing, and step-by-step guides for adding new cloud providers, language bindings, protocol adapters, and client library integrations.
MIT
