diff --git a/context.go b/context.go index 5174033e..8757a32a 100644 --- a/context.go +++ b/context.go @@ -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 diff --git a/recovery.go b/recovery.go index bbf1d565..23492464 100644 --- a/recovery.go +++ b/recovery.go @@ -91,21 +91,31 @@ 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") for i, line := range lines { - if strings.HasPrefix(line, "Authorization:") { + switch { + case hasHeaderPrefixFold(line, "Authorization:"): lines[i] = "Authorization: *" + case hasHeaderPrefixFold(line, "Proxy-Authorization:"): + lines[i] = "Proxy-Authorization: *" } } return strings.Join(lines, "\r\n") } +// hasHeaderPrefixFold reports whether line begins with prefix, ignoring ASCII case. +func hasHeaderPrefixFold(line, prefix string) bool { + return len(line) >= len(prefix) && strings.EqualFold(line[:len(prefix)], prefix) +} + func defaultHandleRecovery(c *Context, _ any) { c.AbortWithStatus(http.StatusInternalServerError) } diff --git a/recovery_test.go b/recovery_test.go index 028c4ad6..8749daba 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -293,6 +293,16 @@ func TestSecureRequestDump(t *testing.T) { 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: "No Authorization header", req: func() *http.Request { diff --git a/render/json.go b/render/json.go index 2f98676c..f2ccaf8c 100644 --- a/render/json.go +++ b/render/json.go @@ -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)) diff --git a/render/render_test.go b/render/render_test.go index f63878b9..c234d189 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -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)