httplib: cover filename metacharacter escaping in ServeSetHeaders - #330
Conversation
|
/review AI review started. |
Advanced AI Review
Click to expand reviewVerified the test passes and confirmed the expected encodings against the real implementation ( 1. 🟡 The exact case issue #303 asks for is missing; the added case covers #302's territory instead
The PR description says "Closes #303", and issue #303's concluded fix is a test using a non-ASCII filename with a space, asserting the full header:
The added case instead exercises
The metacharacter case does partially satisfy #303's underlying concern (the suite can now distinguish the implementation from one that omits escaping entirely), so this is a coverage gap rather than a bug, but merging with "Closes #303" would close the issue without its requested case. Add it alongside the new case: // non-ASCII characters and spaces must be percent-encoded in the RFC 5987
// parameter; the legacy quoted parameter intentionally keeps the raw UTF-8 bytes
w = httptest.NewRecorder()
ServeSetHeaders(w, &ServeHeaderOptions{Filename: "café résumé.pdf"})
assert.Equal(t, `attachment; filename="café résumé.pdf"; filename*=UTF-8''caf%C3%A9%20r%C3%A9sum%C3%A9.pdf`, w.Header().Get("Content-Disposition"))(I verified this exact assertion passes against the current 2. ⚪️ Comment mischaracterizes the threat as "header injection"
// quotes and backslashes must be escaped in the quoted-string form and
// percent-encoded in the RFC 5987 parameter, to prevent header injectionEscaping // quotes and backslashes must be escaped in the quoted-string form and
// percent-encoded in the RFC 5987 parameter, so the filename cannot break
// out of the quoted-string and inject disposition parameters3. ⚪️ Growing copy-paste blocks; make the test table-driven like its sibling
The diff extends a repeated func TestServeSetHeadersContentDisposition(t *testing.T) {
cases := []struct {
name string
opts *ServeHeaderOptions
expected string
}{
{
// a filename without an explicit disposition must default to "attachment", so that
// user-controlled content is never rendered inline by the browser
name: "DefaultsToAttachment",
opts: &ServeHeaderOptions{Filename: "foo.zip"},
expected: `attachment; filename="foo.zip"; filename*=UTF-8''foo.zip`,
},
{
name: "InlineDisposition",
opts: &ServeHeaderOptions{Filename: "foo.zip", Disposition: "inline"},
expected: `inline; filename="foo.zip"; filename*=UTF-8''foo.zip`,
},
{
name: "EscapesQuoteAndBackslash",
opts: &ServeHeaderOptions{Filename: `a"b\c.zip`},
expected: `attachment; filename="a\"b\\c.zip"; filename*=UTF-8''a%22b%5Cc.zip`,
},
{
name: "PercentEncodesNonASCIIAndSpaces",
opts: &ServeHeaderOptions{Filename: "café résumé.pdf"},
expected: `attachment; filename="café résumé.pdf"; filename*=UTF-8''caf%C3%A9%20r%C3%A9sum%C3%A9.pdf`,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
w := httptest.NewRecorder()
ServeSetHeaders(w, c.opts)
assert.Equal(t, c.expected, w.Header().Get("Content-Disposition"))
assert.Equal(t, "Content-Disposition", w.Header().Get("Access-Control-Expose-Headers"))
})
}
w := httptest.NewRecorder()
ServeSetHeaders(w, &ServeHeaderOptions{})
assert.Empty(t, w.Header().Get("Content-Disposition"))
}The assertion values themselves in the new case are correct, the test passes, and no production code changes were needed or made. Review generated using |
|
@taoeffect approved! ✅ |
Closes #302
AI Disclosure
Co-authored with: Opus 5