Compare commits

...

11 Commits

Author SHA1 Message Date
Barry
159edc5b7f
Merge 528a95d48d992f168084a648a414740d3caec116 into dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9 2026-08-15 20:13:53 +08: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
barry3406
528a95d48d refactor(recovery): inline header prefix check and strengthen case tests
Drop the small hasHeaderPrefixFold helper and inline the
len+EqualFold check in secureRequestDump for a simpler, single-site
hardcoded replacement.

Also switch the existing lowercase/mixed-case Authorization tests
to write directly to r.Header (bypassing http.Header.Set canonicalization)
so they actually exercise case-insensitive matching on the wire, and
add uppercase Authorization plus lowercase/uppercase Proxy-Authorization
cases.
2026-04-19 09:11:11 -07:00
barry3406
3f1b179fdc address review feedback from Nurysso
- context.go: guard maps.Clone with explicit nil check for c.Keys
- recovery.go: match Authorization/Proxy-Authorization case-insensitively
  with strings.EqualFold and use simple hardcoded masked replacements
  instead of SplitN for readability
2026-04-16 11:22:28 -07:00
barry3406
473f9ddf55 test: fix testifylint encoded-compare warning in AsciiJSON test 2026-04-09 06:47:54 -07:00
barry3406
4ce483f0f4 test: add tests for supplementary Unicode and Proxy-Authorization
- Add TestRenderAsciiJSONSupplementaryUnicode to verify emoji (U+1F600)
  is encoded as UTF-16 surrogate pair (\uD83D\uDE00)
- Add Proxy-Authorization test case to TestSecureRequestDump to verify
  credentials are sanitized in recovery panic logs
2026-04-09 06:35:52 -07:00
barry3406
08e51b48be fix: sanitize Proxy-Authorization header in recovery panic logs
secureRequestDump only masks the Authorization header but not
Proxy-Authorization, which also carries credentials (used by gin's
own BasicAuthForProxy middleware). When a panic occurs behind proxy
auth, credentials are logged in plaintext.
2026-04-09 06:10:47 -07:00
barry3406
cf3be80b0e fix: encode supplementary Unicode as surrogate pairs in AsciiJSON
Characters above U+FFFF (emoji, math symbols, etc.) were escaped as
\u1f600 (5+ hex digits) which is invalid JSON per RFC 8259. Strict
JSON parsers reject this output.

Per the spec, supplementary plane characters must be encoded as
UTF-16 surrogate pairs (e.g. U+1F600 becomes \uD83D\uDE00).
2026-04-09 06:10:36 -07:00
barry3406
e7c8e50895 fix: move Keys read inside RLock in Context.Copy() to prevent data race
Copy() reads c.Keys before acquiring the read lock, then clones the
stale reference under the lock. If another goroutine calls Set()
between the read and the lock acquisition, the cKeys variable may
reference a map being concurrently modified.

This is particularly dangerous on the first Set() call where c.Keys
transitions from nil to a new map allocation.
2026-04-09 06:10:21 -07:00
12 changed files with 104 additions and 33 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

View File

@ -132,9 +132,10 @@ func (c *Context) Copy() *Context {
cp.handlers = nil
cp.fullPath = c.fullPath
cKeys := c.Keys
c.mu.RLock()
cp.Keys = maps.Clone(cKeys)
if c.Keys != nil {
cp.Keys = maps.Clone(c.Keys)
}
c.mu.RUnlock()
cParams := c.Params

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 {

View File

@ -91,16 +91,25 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
}
}
// secureRequestDump returns a sanitized HTTP request dump where the Authorization header,
// if present, is replaced with a masked value ("Authorization: *") to avoid leaking sensitive credentials.
// secureRequestDump returns a sanitized HTTP request dump where the Authorization
// and Proxy-Authorization headers, if present, are replaced with a masked value
// (e.g. "Authorization: *") to avoid leaking sensitive credentials.
//
// Currently, only the Authorization header is sanitized. All other headers and request data remain unchanged.
// Header name matching is case-insensitive since HTTP headers are case-insensitive
// per RFC 9110. All other headers and request data remain unchanged.
func secureRequestDump(r *http.Request) string {
httpRequest, _ := httputil.DumpRequest(r, false)
lines := strings.Split(bytesconv.BytesToString(httpRequest), "\r\n")
const (
authPrefix = "Authorization:"
proxyPrefix = "Proxy-Authorization:"
)
for i, line := range lines {
if strings.HasPrefix(line, "Authorization:") {
switch {
case len(line) >= len(authPrefix) && strings.EqualFold(line[:len(authPrefix)], authPrefix):
lines[i] = "Authorization: *"
case len(line) >= len(proxyPrefix) && strings.EqualFold(line[:len(proxyPrefix)], proxyPrefix):
lines[i] = "Proxy-Authorization: *"
}
}
return strings.Join(lines, "\r\n")

View File

@ -318,25 +318,67 @@ func TestSecureRequestDump(t *testing.T) {
wantNotContain: "Bearer secret-token",
},
{
// Bypass http.Header.Set canonicalization to put a lowercase
// header name on the wire and verify case-insensitive matching.
name: "authorization header lowercase",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("authorization", "some-secret")
r.Header["authorization"] = []string{"some-secret"}
return r
}(),
wantContains: "Authorization: *",
wantNotContain: "some-secret",
},
{
name: "AUTHORIZATION header uppercase",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header["AUTHORIZATION"] = []string{"UPPER-SECRET"}
return r
}(),
wantContains: "Authorization: *",
wantNotContain: "UPPER-SECRET",
},
{
name: "Authorization header mixed case",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("AuThOrIzAtIoN", "token123")
r.Header["AuThOrIzAtIoN"] = []string{"token123"}
return r
}(),
wantContains: "Authorization: *",
wantNotContain: "token123",
},
{
name: "Proxy-Authorization header",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header.Set("Proxy-Authorization", "Basic cHJveHk6c2VjcmV0")
return r
}(),
wantContains: "Proxy-Authorization: *",
wantNotContain: "Basic cHJveHk6c2VjcmV0",
},
{
name: "proxy-authorization header lowercase",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header["proxy-authorization"] = []string{"Basic bG93ZXI="}
return r
}(),
wantContains: "Proxy-Authorization: *",
wantNotContain: "Basic bG93ZXI=",
},
{
name: "PROXY-AUTHORIZATION header uppercase",
req: func() *http.Request {
r, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
r.Header["PROXY-AUTHORIZATION"] = []string{"Basic VVBQRVI="}
return r
}(),
wantContains: "Proxy-Authorization: *",
wantNotContain: "Basic VVBQRVI=",
},
{
name: "No Authorization header",
req: func() *http.Request {

View File

@ -160,11 +160,19 @@ func (r AsciiJSON) Render(w http.ResponseWriter) error {
}
var buffer bytes.Buffer
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
escapeBuf := make([]byte, 0, 12) // Preallocate for surrogate pair escape sequences
for _, r := range bytesconv.BytesToString(ret) {
if r > unicode.MaxASCII {
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
if r > 0xFFFF {
// Supplementary plane: encode as UTF-16 surrogate pair per RFC 8259
r -= 0x10000
high := 0xD800 + (r>>10)&0x3FF
low := 0xDC00 + r&0x3FF
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x\\u%04x", high, low)
} else {
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r)
}
buffer.Write(escapeBuf)
} else {
buffer.WriteByte(byte(r))

View File

@ -261,6 +261,17 @@ func TestRenderAsciiJSON(t *testing.T) {
assert.Equal(t, "3.1415926", w2.Body.String())
}
func TestRenderAsciiJSONSupplementaryUnicode(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]string{"emoji": "😀"}
err := (AsciiJSON{data}).Render(w)
require.NoError(t, err)
// U+1F600 must be encoded as UTF-16 surrogate pair per RFC 8259.
// Use Contains to verify the surrogate pair encoding in the raw output.
assert.Contains(t, w.Body.String(), `\ud83d\ude00`)
}
func TestRenderAsciiJSONFail(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)