Skip to content

fix(engine): accept bracketed IPv6 and port-suffixed entries in X-Forwarded-For#4634

Open
BootstrapperSBL wants to merge 1 commit intogin-gonic:masterfrom
BootstrapperSBL:fix/forwarded-for-ipv6-port
Open

fix(engine): accept bracketed IPv6 and port-suffixed entries in X-Forwarded-For#4634
BootstrapperSBL wants to merge 1 commit intogin-gonic:masterfrom
BootstrapperSBL:fix/forwarded-for-ipv6-port

Conversation

@BootstrapperSBL
Copy link
Copy Markdown

What

Context.ClientIP() now parses X-Forwarded-For entries in the bracketed and port-suffixed forms produced by IIS/ARR and several cloud load balancers.

Closes #4572.

Why

validateHeader in gin.go handed each comma-split item straight to net.ParseIP, which silently rejects anything other than a bare 1.2.3.4 or 2001:db8::1. In practice proxies often emit:

Input header v1.12.0 ClientIP() After this PR
X-Forwarded-For: 192.168.8.39 192.168.8.39 192.168.8.39 (unchanged)
X-Forwarded-For: 240e:318:2f4a:de56::240 240e:318:2f4a:de56::240 240e:318:2f4a:de56::240 (unchanged)
X-Forwarded-For: [240e:318:2f4a:de56::240] falls back to RemoteAddr 240e:318:2f4a:de56::240
X-Forwarded-For: 192.168.8.39:38792 falls back to RemoteAddr 192.168.8.39
X-Forwarded-For: [240e:318:2f4a:de56::240]:38792 falls back to RemoteAddr 240e:318:2f4a:de56::240

The last three rows are the failure modes from the report.

How

A small parseForwardedForItem helper:

  1. Tries net.SplitHostPort first — this handles ip:port and [ipv6]:port and already strips the brackets for us.
  2. Falls back to stripping optional surrounding [] and calling net.ParseIP — covers the bare [ipv6] case.
  3. Returns the normalised IP string (no brackets, no port) so ClientIP()'s output shape is identical across all proxy flavours.

validateHeader's trusted-proxy reverse-walk logic is untouched — only the per-item parse changes.

Test plan

Table-driven test TestValidateHeaderForwardedForForms covers:

  • All four forms the reporter listed
  • [::1] (bracketed loopback)
  • Mixed chain with a port on the rightmost entry
  • Empty / garbage / [garbage] negative cases

Also verified:

  • go build ./...
  • go vet ./...
  • go test -race ./... (whole tree green, including TestContextClientIP*)
  • golangci-lint run --verbose (v2.11.4, matches CI) — 0 issues

…warded-For

`validateHeader` called `net.ParseIP` directly on each comma-split item, so
anything with brackets or a `:port` suffix got rejected silently and
`ClientIP()` fell through to `RemoteAddr` — which means a client coming in
through IIS/ARR or certain cloud LBs would show up as the reverse proxy
instead of the real caller.

The four forms called out in gin-gonic#4572 are all normal real-world outputs:

  - "192.168.8.39"
  - "240e:318:2f4a:de56::240"
  - "[240e:318:2f4a:de56::240]"
  - "192.168.8.39:38792"
  - "[240e:318:2f4a:de56::240]:38792"

Extract a small `parseForwardedForItem` helper that tries `net.SplitHostPort`
first (handles the two `:port` variants and strips brackets in the process)
and falls back to bracket-stripping + `net.ParseIP` for bare `[ipv6]`. The
returned `clientIP` is now always the bare IP regardless of which proxy
produced the header, which keeps the shape of `ClientIP()` stable.

Table tests cover all four reporter-listed forms, plus a chain with a port
on the last entry and a couple of garbage inputs.

Closes gin-gonic#4572
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.37%. Comparing base (3dc1cd6) to head (2dc3c02).
⚠️ Report is 275 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4634      +/-   ##
==========================================
- Coverage   99.21%   98.37%   -0.84%     
==========================================
  Files          42       48       +6     
  Lines        3182     3148      -34     
==========================================
- Hits         3157     3097      -60     
- Misses         17       42      +25     
- Partials        8        9       +1     
Flag Coverage Δ
?
--ldflags="-checklinkname=0" -tags sonic 98.36% <100.00%> (?)
-tags go_json 98.30% <100.00%> (?)
-tags nomsgpack 98.35% <100.00%> (?)
go-1.18 ?
go-1.19 ?
go-1.20 ?
go-1.21 ?
go-1.25 98.37% <100.00%> (?)
go-1.26 98.37% <100.00%> (?)
macos-latest 98.37% <100.00%> (-0.84%) ⬇️
ubuntu-latest 98.37% <100.00%> (-0.84%) ⬇️

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

☔ View full report in Codecov by Sentry.
📢 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.

@BootstrapperSBL
Copy link
Copy Markdown
Author

Heads up on the failing codecov/project check — the codecov report itself notes the base is 275 commits behind master, so the project-coverage comparison is against a stale snapshot. Patch coverage on the new lines is 100% per the same report.

@BootstrapperSBL
Copy link
Copy Markdown
Author

Hey, just checking if you've had a chance to look at this — happy to adjust anything if needed.

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.

Non-standard X-Forwarded-For header content is not supported

1 participant