Skip to content
5 changes: 3 additions & 2 deletions context.go
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.

I think a adding a nil check would be better

// - cKeys := c.Keys
    c.mu.RLock()
    if c.Keys != nil {
        cp.Keys = maps.Clone(c.Keys)
    }
    c.mu.RUnlock()

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions recovery.go
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.

Few things I noticed

  • Case sensitivity issue: HTTP headers are case-insensitive, but the code only matches exact case. A header like authorization: or AUTHORIZATION: would leak. Use strings.EqualFold or convert to lowercase before comparison.
  • Compared to the original implementation, this adds extra complexity (SplitN) without a clear benefit. Since we already know the header name from the prefix check, a simple hardcoded replacement (as before) seems easier to read and maintain.

Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
46 changes: 44 additions & 2 deletions recovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,25 +274,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 {
Expand Down
12 changes: 10 additions & 2 deletions render/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
11 changes: 11 additions & 0 deletions render/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading