Skip to content

Commit

Permalink
Security: c.Attachment and c.Inline should escape name in `Content-Di…
Browse files Browse the repository at this point in the history
…sposition` header to avoid 'Reflect File Download' vulnerability. (#2541)

This is same as Go std does it https://github.com/golang/go/blob/9d836d41d0d9df3acabf7f9607d3b09188a9bfc6/src/mime/multipart/writer.go#L132
  • Loading branch information
aldas committed Nov 7, 2023
1 parent 50ebcd8 commit 14daeb9
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 21 deletions.
4 changes: 3 additions & 1 deletion context.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,8 +584,10 @@ func (c *context) Inline(file, name string) error {
return c.contentDisposition(file, name, "inline")
}

var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")

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

Expand Down
82 changes: 62 additions & 20 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,30 +414,72 @@ func TestContextStream(t *testing.T) {
}

func TestContextAttachment(t *testing.T) {
e := New()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
c := e.NewContext(req, rec).(*context)

err := c.Attachment("_fixture/images/walle.png", "walle.png")
if assert.NoError(t, err) {
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())
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()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
c := e.NewContext(req, rec).(*context)

err := c.Attachment("_fixture/images/walle.png", tc.whenName)
if assert.NoError(t, err) {
assert.Equal(t, tc.expectHeader, rec.Header().Get(HeaderContentDisposition))

assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, 219885, rec.Body.Len())
}
})
}
}

func TestContextInline(t *testing.T) {
e := New()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/?pretty", nil)
c := e.NewContext(req, rec).(*context)

err := c.Inline("_fixture/images/walle.png", "walle.png")
if assert.NoError(t, err) {
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())
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()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
c := e.NewContext(req, rec).(*context)

err := c.Inline("_fixture/images/walle.png", tc.whenName)
if assert.NoError(t, err) {
assert.Equal(t, tc.expectHeader, rec.Header().Get(HeaderContentDisposition))

assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, 219885, rec.Body.Len())
}
})
}
}

Expand Down

0 comments on commit 14daeb9

Please sign in to comment.