Skip to content

🐛 fix(proxy): guard runtime helpers against DNS-rebinding#4518

Merged
ReneWerner87 merged 9 commits into
mainfrom
claude/dns-rebinding-ssrf-proxy-cfb0ko
Jul 11, 2026
Merged

🐛 fix(proxy): guard runtime helpers against DNS-rebinding#4518
ReneWerner87 merged 9 commits into
mainfrom
claude/dns-rebinding-ssrf-proxy-cfb0ko

Conversation

@gaby

@gaby gaby commented Jul 10, 2026

Copy link
Copy Markdown
Member

Description

middleware/proxy's SecurityPolicy (default AllowPrivateIPs: false) is documented as blocking SSRF against loopback, RFC 1918, link-local, CGNAT, and cloud-metadata addresses. In practice that guarantee only held for Balancer, which installs a dial-time guard (newSSRFDialer) that re-validates the resolved IP atomically with the connection.

Every other public entry point — Do, DoRedirects, DoTimeout, DoDeadline, Forward, DomainForward, BalancerForward — validated the host only once, up front, then dispatched through a shared/user-supplied *fasthttp.Client whose own dialer re-resolved the hostname independently, with no validation. A rebinding resolver that answers with a public IP on the first lookup and a private IP on the second passed the up-front check and then connected to the private address anyway (classic DNS rebinding / check-use gap).

This PR closes that gap for all runtime helpers by giving them the same dial-time re-validation Balancer already has, so AllowPrivateIPs=false genuinely blocks rebinding for every entry point.

Fixes the DNS-rebinding SSRF in proxy.Do/Forward/DoRedirects/DoTimeout/DoDeadline/DomainForward/BalancerForward (☢️ Bug).

How the guard is installed

The guard rides on fasthttp's Client.ConfigureClient hook, which runs once per HostClient at creation — so it is present before the first dial to each host and, crucially, covers both dial code paths (callDialFunc prefers DialTimeout over Dial, so guarding only Dial would leave a DialTimeout-configured client unvalidated).

  • installHostClientGuard wraps both hc.DialTimeout (preserving its per-request timeout) and hc.Dial.
  • guardedDial (shared core) + newGuardedClientDialer / newGuardedClientDialerWithTimeout: when AllowPrivateIPs is true the guard delegates unchanged; otherwise it resolves the host, rejects the whole resolution on any blocked IP, and dials the exact validated IP (through the caller's dialer when set). Policy is read fresh on every dial, since a client outlives any single policy snapshot. Reuses resolveAndValidateHost and dialValidatedIPs.
  • ensureClientGuarded installs the hook idempotently, serialized under a mutex so the guard is in place before any concurrent guarder observes the client. It composes with a user's existing ConfigureClient (running the user hook first, then the guard, so a hook that sets its own hc.Dial cannot overwrite the guard). Wired into init (default client) and WithClient before first use, and on the dispatch path only for a per-call variadic client (the global client is already guarded, so the common request path stays lock-free). Clients with a nil ConfigureClient are matched by identity and never retained.

Because a pooled keep-alive connection is always bound to an already-validated IP, connection reuse cannot be rebound; only a fresh dial reaches a new IP, and every fresh dial runs the guard. The only path left to the caller is a custom Balancer Config.Client (*fasthttp.LBClient), whose underlying dialers are not reachable for wrapping — documented on SecurityPolicy.AllowPrivateIPs.

As defense-in-depth, isBlockedIP now also unwraps IPv4-compatible (::a.b.c.d) and NAT64 (64:ff9b::a.b.c.d) IPv6 literals to the embedded IPv4 and applies the same blocklist (the IPv4-mapped ::ffff: form was already covered), so a private target can't be smuggled past as an IPv6 literal.

Changes introduced

  • middleware/proxy/security.go: installHostClientGuard, guardedDial, newGuardedClientDialer, newGuardedClientDialerWithTimeout; harden isBlockedIP with embeddedIPv4/allZero; update SecurityPolicy.AllowPrivateIPs docs.
  • middleware/proxy/proxy.go: guardConfigureClient/ensureClientGuarded plumbing (mutex-serialized, identity-idempotent), wired into init/WithClient/doActionWithPolicy; refresh the per-helper SSRF doc comments and the DomainForward construction-validation note.
  • Tests (security_rebinding_test.go, security_test.go):
    • DNS-rebinding regression via an in-process fake resolver (public answer first, loopback after) asserting the internal server is never reached — for the default client, a per-call custom client, a client configured with DialTimeout, and DoRedirects; plus a DNS A-query counter asserting the block happens at dial time, not up front.
    • Unit tests for the dialers (block loopback, delegate when private allowed, compose with a caller dialer, validated-hostname dials through to the resolved IP), ConfigureClient compose/ordering + idempotency, a -race concurrency regression test, and extended isBlockedIP coverage (IPv4-mapped / IPv4-compatible / NAT64, private blocked, public allowed).
  • Benchmarks: none added. The common no-variadic request path takes no new lock; the guard adds one resolve + validated dial per new connection when AllowPrivateIPs=false (mirrors the existing Balancer guard).
  • Documentation Update: doc comments in the changed files only; no /docs/ changes.
  • Changelog/What's New: security fix — SSRF/DNS-rebinding protection now applies to all proxy runtime helpers, not just Balancer.
  • Migration Guide: none — behavior only changes for previously-unprotected SSRF cases under the secure default.
  • API Alignment with Express: n/a.
  • API Longevity: no public API/signature changes; the guard is internal.
  • Examples: n/a.

Type of change

  • Code consistency (non-breaking change which improves code reliability and robustness) — security bug fix, no public API change.

Checklist

  • Conducted a self-review of the code and provided comments for complex or critical parts.
  • Added or updated unit tests to validate the effectiveness of the changes.
  • Ensured that new and existing unit tests pass locally (go test -race ./middleware/proxy/..., go vet, gofmt).
  • No new dependencies (golang.org/x/net/dns/dnsmessage, used only in tests, was already a direct module dependency).
  • Updated the documentation in /docs/ (not applicable — no user-facing docs affected).
  • Provided benchmarks for the new code.

🤖 Generated with Claude Code

claude added 5 commits July 9, 2026 13:17
The proxy SecurityPolicy (default AllowPrivateIPs=false) is documented as
blocking SSRF against loopback, RFC 1918, link-local, CGNAT, and cloud
metadata addresses. That guarantee only truly held for Balancer, which
installs a dial-time guard (newSSRFDialer) that re-validates the resolved
IP atomically with the connection.

The runtime helpers — Do, DoRedirects, DoTimeout, DoDeadline, Forward,
DomainForward, BalancerForward — validated the host only once, up front,
then dispatched through a shared/user-supplied *fasthttp.Client whose own
dialer re-resolved the hostname a second time with no validation. A
rebinding resolver returning a public IP on the first lookup and a private
IP on the second passed the check and then connected to the private
address anyway.

Install a policy-aware, dial-time SSRF guard on every *fasthttp.Client the
runtime helpers dispatch through:

- newGuardedClientDialer wraps an existing dialer: when AllowPrivateIPs is
  true it delegates (preserving prior behavior); otherwise it resolves,
  validates against the blocklist, and dials the exact validated IP,
  composing with any caller-supplied dialer. It reuses resolveAndValidateHost
  and dialValidatedIPs and reads the active policy at dial time.
- ensureClientGuarded wraps each client's Dial exactly once (mutex + seen
  set), wired into init (default client), WithClient (global override), and
  doActionWithPolicy (per-call variadic clients) before any dispatch, so the
  field is never mutated concurrently with a dial.

The only path left to the caller is a custom Balancer Config.Client
(*fasthttp.LBClient), whose underlying dialers are not reachable for
wrapping; this is documented.

Adds a regression test that drives a fake rebinding resolver (public answer
first, loopback after) and asserts the internal server is never reached, for
both the default and a custom client, plus unit tests for the new dialer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSa22vkkDLDTZctuKkmZ1A
…ypass, leak, and lock

Code review of the initial dial-time guard surfaced several defects in how
the guard was attached to the dispatching *fasthttp.Client. Re-work the
attachment to use fasthttp's ConfigureClient hook, which runs once per
HostClient at creation, and address the findings:

- DialTimeout bypass (SSRF): fasthttp's callDialFunc prefers a client's
  DialTimeout over Dial, so guarding only Dial left DialTimeout-configured
  clients completely unvalidated. installHostClientGuard now wraps both
  DialTimeout (preserving its per-request timeout) and Dial.
- Timing: installing at HostClient creation (ConfigureClient) means the
  guard is present before the first dial to each host, instead of mutating
  Client.Dial after HostClients may already have been cached with the old
  dialer. The default client (init) and WithClient clients (before use) are
  fully guarded.
- Memory/resource leak: the previous global map keyed by *fasthttp.Client
  pinned every client (and its connection pool) forever. Guarding is now
  idempotent by identity (a stable package-level ConfigureClient hook), so
  the common case retains no client reference; only clients that already
  carry their own ConfigureClient use a sync.Map, composed once.
- Hot-path lock: dropped the process-global mutex acquired on every proxy
  request in favor of the lock-free identity check.

Refactor the dialer into a shared guardedDial core with DialFunc and
DialFuncWithTimeout variants. Add regression tests: a custom client that
sets DialTimeout is now blocked under DNS-rebinding, plus unit coverage for
ConfigureClient composition/idempotency and DialTimeout guarding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSa22vkkDLDTZctuKkmZ1A
Code review of the ConfigureClient-based guard found three defects:

- Compose order (SSRF re-open): the composed hook ran installHostClientGuard
  before the user's existing ConfigureClient, so a user hook that sets
  hc.Dial/hc.DialTimeout (the idiomatic reason to use ConfigureClient)
  overwrote the guard. Run the user's hook first, then install the guard, so
  it wraps whatever dialer the user finally sets.
- TOCTOU / data race: guarding published a "handled" flag before the guard
  field was written, letting a concurrent caller dispatch through a
  still-unguarded client; the field write was also unsynchronized. Serialize
  the whole read-modify-write under a mutex so the guard is installed before
  any other guarder observes the client.
- Hot-path cost: only guard the per-call variadic client on dispatch; the
  global/default client is guarded at registration, so the common no-variadic
  request path no longer touches the guard lock.

Tests: strengthen the compose test so it fails on the wrong order (the user
hook now installs its own hc.Dial and the guard must still block loopback and
delegate an allowed IP); add a concurrency test (race-detector regression
guard for the mutex); add a positive-path test that a validated hostname
resolves and dials through to the exact IP; add a DoRedirects rebinding case;
and key the fake resolver on the query name so a stray lookup cannot mask a
broken dial-time guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSa22vkkDLDTZctuKkmZ1A
Follow-up review hardening:

- The fake resolver now counts A queries and returns the counter; the
  rebinding test asserts >= 2 lookups, proving the block happens at dial time
  (a second, rebound lookup) rather than up front — so a broken dial-time
  guard masked by an up-front block can no longer pass silently.
- Make the guard docs precise: clients with a nil ConfigureClient (the common
  case) are matched by identity and never retained; only a client carrying
  its own ConfigureClient is stored (one bounded entry per distinct client),
  with guidance to reuse such clients or register them via WithClient. Note
  that the per-request guardMu on a fixed variadic client is a deliberate
  correctness-over-throughput choice.

No behavior change; verified with go test -race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSa22vkkDLDTZctuKkmZ1A
Defense-in-depth from the final review pass. isBlockedIP already caught the
IPv4-mapped form (::ffff:a.b.c.d, which net.IP.To4 surfaces) but not the
IPv4-compatible (::a.b.c.d) or NAT64 well-known-prefix (64:ff9b::a.b.c.d)
forms, so a literal like ::127.0.0.1 or 64:ff9b::a9fe:a9fe (the metadata
address) resolved to false. Some stacks route these to the embedded IPv4, so
unwrap them and apply the same blocklist to the embedded address. Public
embedded addresses stay allowed. Table test extended with mapped, compatible,
and NAT64 cases (private blocked, public allowed).

Also correct the DomainForward doc: it validates (and resolves) the addr at
construction, which — unlike Balancer, which defers DNS — panics at startup on
an unresolvable addr. The previous "matches the Balancer contract" wording was
imprecise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSa22vkkDLDTZctuKkmZ1A
Copilot AI review requested due to automatic review settings July 10, 2026 04:44
@gaby
gaby requested a review from a team as a code owner July 10, 2026 04:44
@ReneWerner87 ReneWerner87 added this to v3 Jul 10, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone Jul 10, 2026
@gaby gaby changed the title fix(proxy): guard runtime helpers against DNS-rebinding SSRF 🐛 fix(proxy): guard runtime helpers against DNS-rebinding Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Dial-time SSRF protection now guards shared and per-call fasthttp clients, validates host resolutions during connection establishment, handles wrapped IPv4 forms, and adds DNS-rebinding, composition, concurrency, and policy regression tests.

Changes

SSRF and DNS-rebinding protection

Layer / File(s) Summary
Policy-aware dial validation
middleware/proxy/security.go
Host-client dialing revalidates resolved addresses under strict policy, preserves custom dialers, and unwraps IPv4-compatible and NAT64 addresses for blocklist checks.
Client guard installation and dispatch
middleware/proxy/proxy.go
Global and per-call fasthttp clients receive idempotent guards, while proxy dispatch uses dial-time validation for upstream requests and redirects.
Rebinding and guard regression coverage
middleware/proxy/security_rebinding_test.go, middleware/proxy/security_test.go
Tests cover DNS rebinding, guard composition, concurrency, dial variants, private-IP policy behavior, and wrapped-IP blocklist cases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProxyDispatch
  participant FasthttpClient
  participant HostClient
  participant Resolver
  ProxyDispatch->>FasthttpClient: Dispatch upstream request
  FasthttpClient->>HostClient: Open connection
  HostClient->>Resolver: Resolve hostname at dial time
  Resolver-->>HostClient: Return current IP addresses
  HostClient->>HostClient: Validate resolved IP
  HostClient-->>FasthttpClient: Block or dial validated IP
  FasthttpClient-->>ProxyDispatch: Return response or error
Loading

Possibly related PRs

  • gofiber/fiber#4405: Both changes address redirect downgrade handling in proxy redirect flows, with this PR also adding dial-time SSRF and DNS-rebinding protection.

Suggested reviewers: sixcolors, efectn, ReneWerner87

Poem

I’m a rabbit guarding every dial,
Rebinding tricks now hit a wall.
Public paths may safely flow,
Private hops are stopped below.
With ears up high and tests in flight,
I burrow through the DNS night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding DNS-rebinding guards to proxy runtime helpers.
Description check ✅ Passed The description covers the problem, solution, changes, type of change, and checklist sections required by the template.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% 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.
✨ 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 claude/dns-rebinding-ssrf-proxy-cfb0ko

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.61111% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.22%. Comparing base (a5a8690) to head (4cfd4da).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
middleware/proxy/security.go 98.18% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4518      +/-   ##
==========================================
+ Coverage   93.13%   93.22%   +0.09%     
==========================================
  Files         140      140              
  Lines       14275    14421     +146     
==========================================
+ Hits        13295    13444     +149     
+ Misses        611      608       -3     
  Partials      369      369              
Flag Coverage Δ
unittests 93.22% <98.61%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens middleware/proxy against DNS-rebinding SSRF by ensuring the runtime helper paths (e.g., Do, Forward, DoRedirects) re-validate resolved IPs at dial time (not only at initial validation), aligning their security guarantees with the Balancer path.

Changes:

  • Add a policy-aware, dial-time SSRF guard that wraps fasthttp.HostClient dialing (Dial and DialTimeout) and install it via fasthttp.Client.ConfigureClient, with idempotent client guarding.
  • Extend the upstream IP blocklist to also catch IPv4-compatible and NAT64 IPv6 wrapper literals by unwrapping embedded IPv4s before evaluation.
  • Add regression/unit tests covering DNS rebinding behavior and guarded dialer composition (plus expanded isBlockedIP cases).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
middleware/proxy/security.go Implements dial-time guarded dialing helpers and enhances blocked-IP detection for IPv6 wrapper forms.
middleware/proxy/proxy.go Ensures all runtime-helper-dispatched *fasthttp.Clients get guarded via ConfigureClient, including WithClient and per-call overrides.
middleware/proxy/security_test.go Expands isBlockedIP test coverage for IPv4-mapped, IPv4-compatible, and NAT64 literals.
middleware/proxy/security_rebinding_test.go Adds end-to-end rebinding regression and unit tests for the new guarded client dialer and guard installation semantics.

Comment thread middleware/proxy/security_rebinding_test.go Outdated
Comment thread middleware/proxy/security_rebinding_test.go Outdated
Comment thread middleware/proxy/security_rebinding_test.go Outdated
Comment thread middleware/proxy/proxy.go
Comment thread middleware/proxy/security.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
middleware/proxy/security_rebinding_test.go (3)

347-355: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify goroutine spawning with WaitGroup.Go (modernize/revive).

♻️ Proposed fix
 			var wg sync.WaitGroup
-			for range 32 {
-				wg.Add(1)
-				go func() {
-					defer wg.Done()
-					ensureClientGuarded(cli)
-				}()
-			}
+			for range 32 {
+				wg.Go(func() {
+					ensureClientGuarded(cli)
+				})
+			}
 			wg.Wait()
🤖 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 `@middleware/proxy/security_rebinding_test.go` around lines 347 - 355, Replace
the manual wg.Add(1), goroutine wrapper, and defer wg.Done() around
ensureClientGuarded(cli) with the WaitGroup.Go helper, while preserving the loop
count and waiting behavior with wg.Wait().

Source: Linters/SAST tools


163-170: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the DialTimeout wrapper lambda with a direct reference (unlambda).

fasthttp.DialTimeout already matches the DialTimeout field signature, so the wrapping closure is flagged by lint here and again at Line 374.

♻️ Proposed fix
 			cli := &fasthttp.Client{
-				DialTimeout: func(addr string, timeout time.Duration) (net.Conn, error) {
-					return fasthttp.DialTimeout(addr, timeout)
-				},
+				DialTimeout: fasthttp.DialTimeout,
 			}
🤖 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 `@middleware/proxy/security_rebinding_test.go` around lines 163 - 170, Replace
the redundant DialTimeout closure in the “custom client with DialTimeout” test
case with a direct fasthttp.DialTimeout function reference, and apply the same
unlambda change to the corresponding occurrence near the other flagged location.

Source: Linters/SAST tools


93-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

buildDNSResponse lint failures: hugeParam (Line 93) and unwrapped errors (Lines 101, 104).

q is a 260-byte value; passing by pointer avoids the copy. The StartQuestions()/Question(q) returns are also flagged by wrapcheck. Consider passing q by pointer and wrapping (or //nolint-ing) the builder errors to clear the lint job.

🤖 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 `@middleware/proxy/security_rebinding_test.go` around lines 93 - 104, Update
buildDNSResponse to accept *dnsmessage.Question and pass the pointer through to
the builder, updating callers as needed; wrap the errors returned by
StartQuestions and Question with contextual messages (or add narrowly scoped
nolint directives) to satisfy hugeParam and wrapcheck linting.

Source: Linters/SAST tools

🤖 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.

Inline comments:
In `@middleware/proxy/security_rebinding_test.go`:
- Line 35: Add justified //nolint:errcheck comments with reasons to the
pc.Close(), pc.WriteTo(...), and ln.Close() calls in the relevant test cleanup
and write paths, preserving the existing behavior while satisfying errcheck.

---

Nitpick comments:
In `@middleware/proxy/security_rebinding_test.go`:
- Around line 347-355: Replace the manual wg.Add(1), goroutine wrapper, and
defer wg.Done() around ensureClientGuarded(cli) with the WaitGroup.Go helper,
while preserving the loop count and waiting behavior with wg.Wait().
- Around line 163-170: Replace the redundant DialTimeout closure in the “custom
client with DialTimeout” test case with a direct fasthttp.DialTimeout function
reference, and apply the same unlambda change to the corresponding occurrence
near the other flagged location.
- Around line 93-104: Update buildDNSResponse to accept *dnsmessage.Question and
pass the pointer through to the builder, updating callers as needed; wrap the
errors returned by StartQuestions and Question with contextual messages (or add
narrowly scoped nolint directives) to satisfy hugeParam and wrapcheck linting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 831deb5d-f29d-44a9-b247-9f5e2321fd08

📥 Commits

Reviewing files that changed from the base of the PR and between c6f4afb and e32dc6f.

📒 Files selected for processing (4)
  • middleware/proxy/proxy.go
  • middleware/proxy/security.go
  • middleware/proxy/security_rebinding_test.go
  • middleware/proxy/security_test.go

Comment thread middleware/proxy/security_rebinding_test.go Outdated
claude added 3 commits July 10, 2026 04:55
The Lint CI (golangci-lint v2.12.2) flagged the new test file. Fix all 13:
- errcheck (check-blank): drop `_ =` on best-effort pc.Close/pc.WriteTo/
  ln.Close and mark //nolint:errcheck, matching the repo convention.
- wrapcheck: wrap the golang.org/x/net/dns/dnsmessage builder errors with
  fmt.Errorf (that package isn't in the ignore globs).
- gocritic hugeParam: pass dnsmessage.Question by pointer to buildDNSResponse.
- gocritic unlambda: use fasthttp.DialTimeout directly instead of a wrapper.
- modernize/revive: use sync.WaitGroup.Go instead of Add/go/Done.

No behavior change; go test -race still passes and golangci-lint is clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSa22vkkDLDTZctuKkmZ1A
… race)

The macOS -race CI caught a data race: the rebinding test swaps the
process-global net.DefaultResolver, while a leaked fasthttp dial goroutine
from Test_Proxy_DoDeadline_PastDeadline reads it via net.Dialer.DialContext
(reached through the guard's delegate path now that the default client is
guarded). Mutating the stdlib global is inherently racy with background DNS
lookups anywhere in the test binary.

Add an atomic dnsResolver seam in the proxy package (defaults to
net.DefaultResolver) used by validateHostForSSRF and resolveAndValidateHost,
and have the test swap that atomically instead of net.DefaultResolver. Reads
and the test's swaps are now both atomic, and net.DefaultResolver is never
written, so the race class is eliminated by construction. Verified by running
the two tests together under -race -count=20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSa22vkkDLDTZctuKkmZ1A
…overage

Address PR review feedback:

- Document that a per-call variadic *fasthttp.Client is guarded only on first
  use (hosts it already dialed keep a cached, pre-guard HostClient), and that
  a full guarantee needs WithClient or a dedicated client before first use.
  Added to both the Do SSRF note and the SecurityPolicy.AllowPrivateIPs doc,
  which previously overstated the guarantee for custom clients.
- Add unit tests covering the previously-uncovered guard branches flagged by
  codecov: compose-hook error propagation, malformed dial address, the
  dual-stack delegate path, and the DialFuncWithTimeout delegate + validated
  paths. ensureClientGuarded/guardedDial/installHostClientGuard/
  newGuardedClientDialerWithTimeout are now fully covered.

No behavior change; golangci-lint clean and go test -race passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSa22vkkDLDTZctuKkmZ1A

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 4cfd4da Previous: f280569 Ratio
BenchmarkStripHopByHop_NoConnection (github.com/gofiber/fiber/v3/middleware/proxy) 1474 ns/op 0 B/op 0 allocs/op 929.7 ns/op 0 B/op 0 allocs/op 1.59
BenchmarkStripHopByHop_NoConnection (github.com/gofiber/fiber/v3/middleware/proxy) - ns/op 1474 ns/op 929.7 ns/op 1.59

This comment was automatically generated by workflow using github-action-benchmark.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread middleware/proxy/proxy.go Outdated
Comment thread middleware/proxy/security.go
Address PR review feedback:

- composedGuards was a package-level map keyed by *fasthttp.Client that
  retained every client carrying its own ConfigureClient, pinning those
  clients and their connection pools for the process lifetime (an unbounded
  leak under a fresh-client-per-request pattern). Replace it with a small
  guardedConfigureClient struct whose bound method (.run) is installed as the
  ConfigureClient hook: bound method values share a stable code pointer across
  receivers, so ensureClientGuarded still recognizes an already-guarded client
  by identity — but the struct is referenced only from the client's own field
  and is collected together with the client. No map, no retention. (Plain
  closures were verified not to share a code pointer, so a closure-based
  self-identifying wrapper would not work.)
- Correct the newGuardedClientDialer doc: when orig is nil the delegate path
  uses fasthttp's default dialer, but the validated (blocked-policy) path
  dials the checked IP with a net.Dialer bounded by dnsLookupTimeout (matching
  newSSRFDialer); the previous wording implied fasthttp.Dial in both paths.

Behavior unchanged; ensureClientGuarded and the guard method are fully
covered, golangci-lint is clean, and go test -race passes (including the
compose-ordering, idempotency, and concurrency regression tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSa22vkkDLDTZctuKkmZ1A

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
middleware/proxy/proxy.go (1)

178-213: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid using reflect.Value.Pointer() as the idempotency key — compare against an explicit sentinel on guardedConfigureClient instead; Go doesn’t guarantee func-pointer identity for bound method values, so this check can misclassify an already-guarded client.

🤖 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 `@middleware/proxy/proxy.go` around lines 178 - 213, Replace the
reflect.Value.Pointer-based check in ensureClientGuarded with an explicit
sentinel identifying guardedConfigureClient hooks. Update guardedConfigureClient
and its run hook so the sentinel is preserved or detectable for composed hooks,
then use that sentinel to return early when the client is already guarded;
remove the guardHookPtr dependency.

Source: Coding guidelines

🤖 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 `@middleware/proxy/proxy.go`:
- Around line 178-213: Replace the reflect.Value.Pointer-based check in
ensureClientGuarded with an explicit sentinel identifying guardedConfigureClient
hooks. Update guardedConfigureClient and its run hook so the sentinel is
preserved or detectable for composed hooks, then use that sentinel to return
early when the client is already guarded; remove the guardHookPtr dependency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f0d64a9d-7704-4890-9358-17c6e8db0f27

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab95c8 and 4cfd4da.

📒 Files selected for processing (2)
  • middleware/proxy/proxy.go
  • middleware/proxy/security.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • middleware/proxy/security.go

@gaby
gaby requested a review from Copilot July 10, 2026 14:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@ReneWerner87
ReneWerner87 merged commit a06162c into main Jul 11, 2026
20 of 21 checks passed
@ReneWerner87
ReneWerner87 deleted the claude/dns-rebinding-ssrf-proxy-cfb0ko branch July 11, 2026 18:41
@github-project-automation github-project-automation Bot moved this to Done in v3 Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants