Merge 3f1b179fdc1edcc4b8f2232010322266e87bb98d into d3ffc9985281dcf4d3bef604cce4e662b1a327a6

This commit is contained in:
Barry 2026-04-16 18:23:13 +00:00 committed by GitHub
commit 4d00287d51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 48 additions and 8 deletions

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

View File

@ -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)
}

View File

@ -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 {

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)