1
0
mirror of https://github.com/labstack/echo.git synced 2024-12-22 20:06:21 +02:00

Security: c.Attachment and c.Inline should escape name in Content-Disposition header to avoid 'Reflect File Download' vulnerability. (#2541)

This is same as Go std does it 9d836d41d0/src/mime/multipart/writer.go (L132)
This commit is contained in:
Martti T 2023-11-07 14:10:06 +02:00 committed by GitHub
parent 50ebcd8d7c
commit 14daeb9680
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 63 additions and 19 deletions

View File

@ -584,8 +584,10 @@ func (c *context) Inline(file, name string) error {
return c.contentDisposition(file, name, "inline") return c.contentDisposition(file, name, "inline")
} }
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func (c *context) contentDisposition(file, name, dispositionType string) error { func (c *context) contentDisposition(file, name, dispositionType string) error {
c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", dispositionType, name)) c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`, dispositionType, quoteEscaper.Replace(name)))
return c.File(file) return c.File(file)
} }

View File

@ -414,31 +414,73 @@ func TestContextStream(t *testing.T) {
} }
func TestContextAttachment(t *testing.T) { func TestContextAttachment(t *testing.T) {
var testCases = []struct {
name string
whenName string
expectHeader string
}{
{
name: "ok",
whenName: "walle.png",
expectHeader: `attachment; filename="walle.png"`,
},
{
name: "ok, escape quotes in malicious filename",
whenName: `malicious.sh"; \"; dummy=.txt`,
expectHeader: `attachment; filename="malicious.sh\"; \\\"; dummy=.txt"`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New() e := New()
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/?pretty", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
c := e.NewContext(req, rec).(*context) c := e.NewContext(req, rec).(*context)
err := c.Attachment("_fixture/images/walle.png", "walle.png") err := c.Attachment("_fixture/images/walle.png", tc.whenName)
if assert.NoError(t, err) { if assert.NoError(t, err) {
assert.Equal(t, tc.expectHeader, rec.Header().Get(HeaderContentDisposition))
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "attachment; filename=\"walle.png\"", rec.Header().Get(HeaderContentDisposition))
assert.Equal(t, 219885, rec.Body.Len()) assert.Equal(t, 219885, rec.Body.Len())
} }
})
}
} }
func TestContextInline(t *testing.T) { func TestContextInline(t *testing.T) {
var testCases = []struct {
name string
whenName string
expectHeader string
}{
{
name: "ok",
whenName: "walle.png",
expectHeader: `inline; filename="walle.png"`,
},
{
name: "ok, escape quotes in malicious filename",
whenName: `malicious.sh"; \"; dummy=.txt`,
expectHeader: `inline; filename="malicious.sh\"; \\\"; dummy=.txt"`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
e := New() e := New()
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/?pretty", nil) req := httptest.NewRequest(http.MethodGet, "/", nil)
c := e.NewContext(req, rec).(*context) c := e.NewContext(req, rec).(*context)
err := c.Inline("_fixture/images/walle.png", "walle.png") err := c.Inline("_fixture/images/walle.png", tc.whenName)
if assert.NoError(t, err) { if assert.NoError(t, err) {
assert.Equal(t, tc.expectHeader, rec.Header().Get(HeaderContentDisposition))
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "inline; filename=\"walle.png\"", rec.Header().Get(HeaderContentDisposition))
assert.Equal(t, 219885, rec.Body.Len()) assert.Equal(t, 219885, rec.Body.Len())
} }
})
}
} }
func TestContextNoContent(t *testing.T) { func TestContextNoContent(t *testing.T) {