Skip to content

timescale/ressrf

Repository files navigation

ressrf

CI License: MIT

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.

Key Features

  • 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)

Packages

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.

Quick Start

Rust

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());

Go (wazero, shared Rust engine)

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: blocked

Go (native)

policy, _ := 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: blocked

Python

from 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}")

Node.js

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);
}

Installation

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 Lists

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"] });

URL Rules

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.

Audit Logging

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));

IP Ranges Codegen

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-only

Testing

Shared 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.js

A 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

CI/CD

  • 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

Contributing

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.

License

MIT

Releases

No releases published

Packages

 
 
 

Contributors