Compare commits

...

5 Commits

Author SHA1 Message Date
Yvan Wang
fdc93e1f0f
Merge 2dc3c023731c6f8e21f658a02e4a99eb22b889de into dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9 2026-08-22 20:19:40 -07:00
Amirhf
dcaa4296d1
docs(path): fix malformed comment in cleanPath (#4723) 2026-08-15 13:44:19 +08:00
Gaurav Patil
8dd20118ba
fix(deps): bump golang.org/x/net and golang.org/x/text to patched versions (#4807)
The scheduled Trivy scan has been failing on master with two HIGH
findings, and it fails every pull request along with it:

- CVE-2026-56852, golang.org/x/text, denial of service, fixed in 0.39.0
- CVE-2026-46600, golang.org/x/net/dns/dnsmessage, denial of service,
  fixed in 0.56.0

Bump both to the first patched release. golang.org/x/crypto and
golang.org/x/sys move with them as transitive requirements of x/net.

Verified with the same settings the workflow uses:

  trivy fs --scanners vuln --severity CRITICAL,HIGH,MEDIUM \
    --ignore-unfixed --exit-code 1 .

which now exits 0. go build ./... and go test ./... pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-08-15 13:43:07 +08:00
dependabot[bot]
00cfe5aac2
chore(deps): bump the actions group across 1 directory with 4 updates (#4787)
Bumps the actions group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/setup-go](https://github.com/actions/setup-go), [actions/cache](https://github.com/actions/cache) and [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

Updates `actions/setup-go` from 6 to 7
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

Updates `actions/cache` from 5 to 6
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

Updates `codecov/codecov-action` from 6 to 7
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 13:42:11 +08:00
BootstrapperSBL
2dc3c02373 fix(engine): accept bracketed IPv6 and port-suffixed entries in X-Forwarded-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 #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 #4572
2026-04-18 00:13:37 +08:00
9 changed files with 86 additions and 25 deletions

View File

@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: "^1"
- name: Setup golangci-lint
@ -55,17 +55,17 @@ jobs:
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go }}
cache: false
- name: Checkout Code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: ${{ github.ref }}
- uses: actions/cache@v5
- uses: actions/cache@v6
with:
path: |
${{ matrix.go-build }}
@ -78,6 +78,6 @@ jobs:
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}

View File

@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: "^1"
- name: Run GoReleaser

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

33
gin.go
View File

@ -485,8 +485,8 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
}
items := strings.Split(header, ",")
for i := len(items) - 1; i >= 0; i-- {
ipStr := strings.TrimSpace(items[i])
ip := net.ParseIP(ipStr)
item := strings.TrimSpace(items[i])
ipStr, ip := parseForwardedForItem(item)
if ip == nil {
break
}
@ -500,6 +500,35 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
return "", false
}
// parseForwardedForItem normalizes a single X-Forwarded-For entry and parses it.
// It accepts the four common forms emitted by reverse proxies:
//
// - "1.2.3.4"
// - "2001:db8::1"
// - "[2001:db8::1]" (IIS/ARR style)
// - "1.2.3.4:12345" (with port, some LBs)
// - "[2001:db8::1]:12345" (IIS/ARR + port)
//
// The returned string is the IP without brackets or port, so callers see a
// consistent form regardless of which proxy produced the header.
func parseForwardedForItem(item string) (string, net.IP) {
// Try host:port form first (handles "ip:port" and "[ipv6]:port").
if host, _, err := net.SplitHostPort(item); err == nil {
if ip := net.ParseIP(host); ip != nil {
return host, ip
}
}
// Strip optional surrounding brackets for bare "[ipv6]" with no port.
unbracketed := item
if strings.HasPrefix(unbracketed, "[") && strings.HasSuffix(unbracketed, "]") {
unbracketed = unbracketed[1 : len(unbracketed)-1]
}
if ip := net.ParseIP(unbracketed); ip != nil {
return unbracketed, ip
}
return "", nil
}
// updateRouteTree do update to the route tree recursively
func updateRouteTree(n *node) {
n.path = strings.ReplaceAll(n.path, escapedColon, colon)

View File

@ -1156,3 +1156,35 @@ func TestUpdateRouteTreesCalledOnce(t *testing.T) {
assert.Equal(t, "ok", w.Body.String())
}
}
func TestValidateHeaderForwardedForForms(t *testing.T) {
engine := New()
// Disable trusted proxies so the rightmost parseable entry is returned.
require.NoError(t, engine.SetTrustedProxies(nil))
tests := []struct {
name string
header string
wantIP string
wantOK bool
}{
{"plain IPv4", "192.168.8.39", "192.168.8.39", true},
{"plain IPv6", "240e:318:2f4a:de56::240", "240e:318:2f4a:de56::240", true},
{"bracketed IPv6 (IIS/ARR)", "[240e:318:2f4a:de56::240]", "240e:318:2f4a:de56::240", true},
{"IPv4 with port", "192.168.8.39:38792", "192.168.8.39", true},
{"bracketed IPv6 with port", "[240e:318:2f4a:de56::240]:38792", "240e:318:2f4a:de56::240", true},
{"IPv6 loopback bracketed", "[::1]", "::1", true},
{"chain with port on last entry", "1.2.3.4, 5.6.7.8:9000", "5.6.7.8", true},
{"empty", "", "", false},
{"garbage", "not-an-ip", "", false},
{"bracketed garbage", "[not-an-ip]", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotIP, gotOK := engine.validateHeader(tt.header)
assert.Equal(t, tt.wantOK, gotOK)
assert.Equal(t, tt.wantIP, gotIP)
})
}
}

8
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1
go.mongodb.org/mongo-driver/v2 v2.5.0
golang.org/x/net v0.55.0
golang.org/x/net v0.56.0
google.golang.org/protobuf v1.36.11
)
@ -39,7 +39,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.39.0 // indirect
)

16
go.sum
View File

@ -79,15 +79,15 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -55,7 +55,7 @@ func cleanPath(p string) string {
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).
for r < n {