{"id":50344586,"url":"https://github.com/timescale/ressrf","last_synced_at":"2026-05-29T19:01:54.150Z","repository":{"id":359099820,"uuid":"1233736483","full_name":"timescale/ressrf","owner":"timescale","description":"Multi-platform SSRF prevention library","archived":false,"fork":false,"pushed_at":"2026-05-27T13:29:24.000Z","size":3166,"stargazers_count":2,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-27T15:22:00.824Z","etag":null,"topics":["go","nodejs","python","rust","security","ssrf","ssrf-protection","wasm","webassembly"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/timescale.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":"THREAT_MODEL.md","audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-09T09:43:56.000Z","updated_at":"2026-05-27T13:27:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/timescale/ressrf","commit_stats":null,"previous_names":["timescale/ressrf"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/timescale/ressrf","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timescale%2Fressrf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timescale%2Fressrf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timescale%2Fressrf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timescale%2Fressrf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/timescale","download_url":"https://codeload.github.com/timescale/ressrf/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timescale%2Fressrf/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33666290,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","nodejs","python","rust","security","ssrf","ssrf-protection","wasm","webassembly"],"created_at":"2026-05-29T19:01:53.215Z","updated_at":"2026-05-29T19:01:54.143Z","avatar_url":"https://github.com/timescale.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg src=\"assets/ressrf-logo.png\" alt=\"ressrf\" width=\"200\"\u003e\n\u003c/p\u003e\n\n[![CI](https://github.com/timescale/ressrf/actions/workflows/ci.yml/badge.svg)](https://github.com/timescale/ressrf/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)\n\nA 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.\n\nressrf (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.\n\n## Key Features\n\n- **Deny-first policy engine** with presets, custom allow/deny CIDR lists, URL rules, and cloud provider modules\n- **Default deny list** sourced from IANA special-purpose registries, kept fresh by automated monthly updates\n- **Protocol adapters** for HTTP (redirect re-validation), TCP (DNS-pinned dialing), and SSH in every language\n- **Pluggable audit logging** via simple callback interface, consistent across all bindings\n- **Cross-language conformance** guaranteed by shared JSON test vectors\n- **Fuzz-tested** with cargo-fuzz (weekly CI runs)\n\n## Packages\n\n| Package | Language | Integration | Docs |\n|---------|----------|-------------|------|\n| [`ressrf-core`](crates/ressrf-core/) | Rust | Direct dependency | [README](crates/ressrf-core/README.md) |\n| [`ressrf-tcp`](crates/ressrf-tcp/) | Rust | DNS-pinned TCP dialing | [README](crates/ressrf-tcp/README.md) |\n| [`ressrf-http`](crates/ressrf-http/) | Rust | Tower Layer/Service | [README](crates/ressrf-http/README.md) |\n| [`ressrf-ssh`](crates/ressrf-ssh/) | Rust | Guard for russh/async-ssh2 | [README](crates/ressrf-ssh/README.md) |\n| [`ressrf-tracing`](crates/ressrf-tracing/) | Rust | TracingSink audit adapter | [README](crates/ressrf-tracing/README.md) |\n| [`ressrf-wasm`](crates/ressrf-wasm/) | Rust | WASM ABI for Go/Node.js | [README](crates/ressrf-wasm/README.md) |\n| [`go/ressrf`](go/ressrf/) | Go | wazero WASM runtime | [README](go/ressrf/README.md) |\n| [`go-native/ressrf`](go-native/ressrf/) | Go | Native port (no WASM, no CGO) | [README](go-native/ressrf/README.md) |\n| [`python/`](python/) | Python | PyO3 native extension | [README](python/README.md) |\n| [`node/`](node/) | TypeScript | WebAssembly API | [README](node/README.md) |\n\n```\n                     ┌─────────────────────────┐\n                     │      ressrf-core        │\n                     │  (policy, CIDR, URI,    │\n                     │   audit, cloud, trie)   │            ┌──────────────────────┐\n                     └───────┬─────────────────┘            │     Go (native)      │\n                             │                              │   pure-Go port       │\n           ┌─────────────────┼─────────────────┐            │                      │\n           │                 │                 │            │ Consumes only the    │\n           ▼                 ▼                 ▼            │ shared JSON:         │\n   ┌────────────────┐ ┌─────────────┐  ┌────────────────┐   │ • config/*.json      │\n   │  ressrf-wasm   │ │ ressrf-http │  │  ressrf-ssh    │   │ • tests/vectors      │\n   │  (WASM ABI)    │ │ (Tower)     │  │  (russh)       │   │                      │\n   └───────┬────────┘ └──────┬──────┘  └───────┬────────┘   │ No Rust dependency;  │\n           │                 │                 │            │ pinned to ressrf-    │\n     ┌─────┴─────┐           └───────┬─────────┘            │ core via differen-   │\n     │           │                   │                      │ tial fuzz against    │\n     ▼           ▼                   ▼                      │ ressrf-wasm.         │\n┌──────────┐ ┌──────────┐      ┌──────────────┐             └──────────────────────┘\n│Go(wazero)│ │  Node.js │      │    Python    │\n│  binding │ │  (WASM)  │      │   (PyO3)     │\n└──────────┘ └──────────┘      └──────────────┘\n```\n\nThe native Go port lives at [`go-native/ressrf/`](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.\n\n## Quick Start\n\n### Rust\n\n```rust\nuse ressrf_core::{PolicyBuilder, UriValidator};\n\nlet policy = PolicyBuilder::external_only().build();\n\nassert!(policy.is_network_allowed(\u0026[\"10.0.0.1\".parse().unwrap()]).is_err());\nassert!(policy.is_network_allowed(\u0026[\"93.184.216.34\".parse().unwrap()]).is_ok());\n```\n\n### Go (wazero, shared Rust engine)\n\n```go\npolicy, _ := ressrf.NewPolicyBuilder(ressrf.PresetExternalOnly).\n    WithCloudProviders(\"aws\", \"azure\", \"gcp\").\n    Build(ctx)\ndefer policy.Close(ctx)\n\nerr := policy.IsAllowed(ctx, \"http://169.254.169.254/latest/meta-data/\")\n// err: blocked\n```\n\n### Go (native)\n\n```go\npolicy, _ := ressrf.NewPolicy(ressrf.PresetExternalOnly,\n    ressrf.WithCloudProviderDenies(ressrf.CloudAWS, ressrf.CloudAzure, ressrf.CloudGCP),\n)\n\nerr := policy.IsAllowed(ctx, \"http://169.254.169.254/latest/meta-data/\")\n// err: blocked\n```\n\n### Python\n\n```python\nfrom ressrf import Policy, RessrfBlockedError\n\npolicy = Policy.external_only(cloud=[\"aws\"])\n\ntry:\n    policy.validate_url(\"http://169.254.169.254/latest/meta-data/\")\nexcept RessrfBlockedError as e:\n    print(f\"Blocked: {e.reason}\")\n```\n\n### Node.js\n\n```typescript\nimport { Policy, isBlocked } from \"ressrf\";\n\nconst policy = await Policy.externalOnly({ cloud: [\"aws\"] });\n\ntry {\n  policy.isAllowed(\"http://169.254.169.254/latest/meta-data/\");\n} catch (err) {\n  if (isBlocked(err)) console.log(\"Blocked:\", err.reason);\n}\n```\n\n## Installation\n\n| Language | Command | Requirements |\n|----------|---------|--------------|\n| Rust | `cargo add ressrf-core` | Rust 1.75+ |\n| Go (wazero) | `go get github.com/timescale/ressrf/go/ressrf` | Go 1.26+ |\n| Go (native) | `go get github.com/timescale/ressrf/go-native/ressrf` | Go 1.25+ |\n| Python | `pip install ressrf` | Python 3.10+ |\n| Node.js | `npm install ressrf` | Node.js 20+ |\n\nSee each package's README for optional extras (protocol adapters, feature flags).\n\n## Allow Lists\n\nAllow overrides deny. Punch holes for specific CIDRs while keeping the rest of private address space blocked:\n\n```rust\n// Rust\nPolicyBuilder::external_only().add_allowed(\u0026[\"10.42.0.0/16\"]).build();\n```\n\n```go\n// Go (wazero)\nNewPolicyBuilder(PresetExternalOnly).WithAllowedCIDRs(\"10.42.0.0/16\").Build(ctx)\n```\n\n```go\n// Go (native)\nressrf.NewPolicy(ressrf.PresetExternalOnly, ressrf.WithAllowedCIDRs(\"10.42.0.0/16\"))\n```\n\n```python\n# Python\nPolicy.external_only(allowed=[\"10.42.0.0/16\"])\n```\n\n```typescript\n// Node.js\nawait Policy.externalOnly({ allowCidrs: [\"10.42.0.0/16\"] });\n```\n\n## URL Rules\n\nFor 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:\n\n```rust\n// Rust\nPolicyBuilder::external_only()\n    .url_allow(UrlRule::glob(\"https\", \"*.stripe.com\", \"/v1/**\"))\n    .url_deny(UrlRule::host(\"*.internal\"))\n    .build();\n```\n\n```go\n// Go (wazero)\nNewPolicyBuilder(PresetExternalOnly).\n    WithURLAllow(URLRule{Scheme: \"https\", Host: \"*.stripe.com\", Path: \"/v1/**\"}).\n    WithURLDeny(URLRule{Host: \"*.internal\"}).\n    Build(ctx)\n```\n\n```go\n// Go (native)\nressrf.NewPolicy(ressrf.PresetExternalOnly,\n    ressrf.WithURLAllow(ressrf.URLRuleGlob(\"https\", \"*.stripe.com\", \"/v1/**\")),\n    ressrf.WithURLDeny(ressrf.URLRuleGlob(\"\", \"*.internal\", \"\")),\n)\n```\n\n```python\n# Python\nPolicyBuilder(\"external_only\") \\\n    .url_allow(scheme=\"https\", host=\"*.stripe.com\", path=\"/v1/**\") \\\n    .url_deny(host=\"*.internal\") \\\n    .build()\n```\n\n```typescript\n// Node.js\nnew PolicyBuilder(\"external_only\")\n  .urlAllow({ scheme: \"https\", host: \"*.stripe.com\", path: \"/v1/**\" })\n  .urlDeny({ host: \"*.internal\" })\n  .build();\n```\n\nDeny 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.\n\n## Audit Logging\n\nAll bindings expose the same pluggable interface. The library emits structured events but never dictates which logging framework to use:\n\n```rust\n// Rust: implement the AuditSink trait\nlet policy = PolicyBuilder::external_only()\n    .audit_sink(Box::new(my_sink))\n    .build();\n```\n\n```go\n// Go: any function works\nsink := ressrf.AuditFunc(func(ctx context.Context, e *ressrf.AuditEvent) {\n    slog.InfoContext(ctx, \"ressrf\", \"kind\", e.Kind)\n})\n```\n\n```python\n# Python: any callable works\nsink = AuditFunc(lambda event: print(f\"[{event.event_type}] {event.fields}\"))\n```\n\n```typescript\n// Node.js: any object with emit() works\nconst sink = new AuditFunc((event) =\u003e console.log(event.kind, event.fields));\n```\n\n## IP Ranges Codegen\n\n`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).\n\n```bash\npython scripts/generate_ip_ranges.py              # full update\npython scripts/generate_ip_ranges.py --iana-only  # skip cloud service ranges\npython scripts/generate_ip_ranges.py --validate-only\n```\n\n## Testing\n\nShared 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):\n\n```bash\ncargo test --workspace --all-features          # Rust\ncd go/ressrf \u0026\u0026 go test -race ./...            # Go (wazero)\ncd go-native/ressrf \u0026\u0026 go test -race ./...     # Go (native)\ncd python \u0026\u0026 uv run pytest tests/ -v           # Python\ncd node \u0026\u0026 npx tsx --test tests/*.test.ts      # Node.js\n```\n\nA 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:\n\n```bash\ncargo test --features e2e -p ressrf-tcp --test ssrf_e2e\n```\n\n## CI/CD\n\n- **Rust:** check, fmt, clippy, test (Linux/macOS/Windows), WASM build\n- **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\n- **Python:** pytest (multi-OS), ruff, ty\n- **Node.js:** node:test (multi-OS), tsc\n- **SSRF e2e:** Linux-only Tier 2 job spins up CoreDNS + WireMock to verify DNS-based and redirect-based bypasses end-to-end\n- **Security:** cargo audit, govulncheck, cargo-fuzz (weekly), zizmor\n- **IP ranges:** monthly upstream fetch, validate, test, auto-PR\n\n## Contributing\n\nIf 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.\n\nSee [HACKING.md](HACKING.md) for development setup, testing, and step-by-step guides for adding new cloud providers, language bindings, protocol adapters, and client library integrations.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimescale%2Fressrf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftimescale%2Fressrf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimescale%2Fressrf/lists"}