Skip to content

feat(ssrf): implement SSRF protection for spec loading and external $ref resolution#7

Merged
frontegg-david merged 2 commits into
mainfrom
fix/ssrf-dns-pin
Jun 21, 2026
Merged

feat(ssrf): implement SSRF protection for spec loading and external $ref resolution#7
frontegg-david merged 2 commits into
mainfrom
fix/ssrf-dns-pin

Conversation

@alexmercerpo

@alexmercerpo alexmercerpo commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • Security

    • Added SSRF protections for spec loading and external reference resolution, including internal IP blocking and DNS validation of all redirect hops.
    • New SsrfError exception type for security-related failures.
  • New Features

    • Exported SSRF validation and URL safety utilities for external use.
  • Tests

    • Added comprehensive SSRF protection test coverage.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a new src/ssrf.ts module implementing SSRF protection: IP-range blocking for IPv4/IPv6 literals, DNS-resolved address validation, and a redirect-re-validating safeFetch. A new SsrfError class is defined. OpenAPIToolGenerator.fromURL and the $ref external resolver in buildRefParserOptions are refactored to use these helpers. All new types and functions are re-exported via src/index.ts. The package is released as version 2.5.0.

Changes

SSRF Hardening (GHSA-65h7-9wrw-629c)

Layer / File(s) Summary
SSRF types, IP blocking primitives, and options normalization
src/ssrf.ts
Defines ResolvedSsrfOptions, ResolvedAddress, SsrfHostLookup, BLOCKED_HOSTNAMES, normalizeSsrfOptions, decodeIpv4MappedIpv6, IPv4/IPv6 range-check helpers (isBlockedIpv4, isBlockedIpv6), isBlockedAddress, isBlockedHostname, and defaultLookup (Node DNS resolver).
SsrfError, assertUrlSafe, and safeFetch
src/errors.ts, src/ssrf.ts
Introduces SsrfError extends LoadError. assertUrlSafe validates protocol, allowedHosts, synchronous IP blocking, and DNS-resolved address rejection. safeFetch wraps fetch with timeout, manual redirect mode, and hop-by-hop assertUrlSafe re-validation up to maxRedirects.
Generator refactored to use ssrf helpers
src/generator.ts
fromURL replaces direct fetch+AbortController with safeFetch configured via normalizeSsrfOptions. buildRefParserOptions switches canRead to isBlockedHostname and rewrites the external $ref resolver read to use safeFetch with followRedirects: false. Removed in-file SSRF blocklist implementation.
Public API exports and updated type docs
src/index.ts, src/types.ts
index.ts re-exports SsrfError, all ssrf helper functions/constants, and ssrf option/result types. types.ts JSDoc for RefResolutionOptions and LoadOptions is expanded to document DNS resolution, IPv4-mapped IPv6 normalization, redirect-hop re-validation, and unified host policy.
SSRF module and generator integration tests
src/__tests__/ssrf.spec.ts, src/__tests__/generator.spec.ts
Full Jest suite for isBlockedAddress, decodeIpv4MappedIpv6, isBlockedHostname, normalizeSsrfOptions, assertUrlSafe, and safeFetch. Generator tests extended for fromURL SSRF rejection/bypass and $ref resolver safe-read assertion.
Version bump and changelog
package.json, CHANGELOG.md
package.json version bumped from 2.1.2 to 2.5.0. CHANGELOG.md adds a 2.5.0 security section enumerating SSRF changes, newly exported helpers, and a DNS-rebinding residual caveat.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant fromURL as OpenAPIToolGenerator.fromURL
  participant safeFetch
  participant assertUrlSafe
  participant DNS as defaultLookup (DNS)
  participant RemoteServer

  Client->>fromURL: url, LoadOptions
  fromURL->>safeFetch: url, ssrf + timeout opts
  safeFetch->>assertUrlSafe: initial URL, ResolvedSsrfOptions
  assertUrlSafe->>DNS: resolve hostname
  DNS-->>assertUrlSafe: ResolvedAddress[]
  assertUrlSafe-->>safeFetch: throws SsrfError (if blocked)
  safeFetch->>RemoteServer: GET url (redirect: manual)
  RemoteServer-->>safeFetch: 3xx Location header
  safeFetch->>assertUrlSafe: redirect target URL
  assertUrlSafe-->>safeFetch: throws SsrfError (if redirect blocked)
  safeFetch->>RemoteServer: GET redirect target
  RemoteServer-->>safeFetch: 200 OK + body
  safeFetch-->>fromURL: Response
  fromURL-->>Client: parsed OpenAPI spec / LoadError
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop, hop — I checked each IP!
No sneaky internal address gets past me.
DNS resolved? I'll peek at each hop,
Private ranges? I give them a stop.
SsrfError flies clean through the air,
With safeFetch and guards everywhere. 🛡️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing SSRF protection for spec loading and external $ref resolution, which is the primary focus across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ssrf-dns-pin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/__tests__/generator.spec.ts (1)

204-210: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Inconsistent error matchers across assertions.

Line 206 uses a regex /blocked/i while lines 207-208 use the LoadError class. Since SsrfError extends LoadError, both technically pass, but using consistent matchers improves test clarity about which error type is expected.

♻️ Suggested fix for consistency
     it('should refuse a spec URL pointing at a literal internal IP (SSRF guard)', async () => {
       // Literal internal IPs are rejected synchronously, before any fetch.
-      await expect(OpenAPIToolGenerator.fromURL('http://127.0.0.1:8080/openapi.json')).rejects.toThrow(/blocked/i);
-      await expect(OpenAPIToolGenerator.fromURL('http://169.254.169.254/openapi.json')).rejects.toThrow(LoadError);
-      await expect(OpenAPIToolGenerator.fromURL('http://[::1]/openapi.json')).rejects.toThrow(LoadError);
+      await expect(OpenAPIToolGenerator.fromURL('http://127.0.0.1:8080/openapi.json')).rejects.toThrow(SsrfError);
+      await expect(OpenAPIToolGenerator.fromURL('http://169.254.169.254/openapi.json')).rejects.toThrow(SsrfError);
+      await expect(OpenAPIToolGenerator.fromURL('http://[::1]/openapi.json')).rejects.toThrow(SsrfError);
       expect(mockFetch).not.toHaveBeenCalled();
     });

Add SsrfError to the imports at the top of the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/generator.spec.ts` around lines 204 - 210, The test method
`should refuse a spec URL pointing at a literal internal IP (SSRF guard)` has
inconsistent error matchers across its three assertions. Line 206 uses a regex
pattern `/blocked/i` while lines 207-208 use the LoadError class matcher. To
improve clarity and consistency, import SsrfError at the top of the file if not
already present, and update all three expect statements to use SsrfError as the
expected error type instead of mixing regex and class matchers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/__tests__/generator.spec.ts`:
- Around line 204-210: The test method `should refuse a spec URL pointing at a
literal internal IP (SSRF guard)` has inconsistent error matchers across its
three assertions. Line 206 uses a regex pattern `/blocked/i` while lines 207-208
use the LoadError class matcher. To improve clarity and consistency, import
SsrfError at the top of the file if not already present, and update all three
expect statements to use SsrfError as the expected error type instead of mixing
regex and class matchers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9fa817b1-1736-463f-829b-af3e5c77600e

📥 Commits

Reviewing files that changed from the base of the PR and between 101373e and be3409c.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • package.json
  • src/__tests__/generator.spec.ts
  • src/__tests__/ssrf.spec.ts
  • src/errors.ts
  • src/generator.ts
  • src/index.ts
  • src/ssrf.ts
  • src/types.ts

@frontegg-david frontegg-david merged commit 1c16b25 into main Jun 21, 2026
4 checks passed
@frontegg-david frontegg-david deleted the fix/ssrf-dns-pin branch June 21, 2026 21:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants