Skip to content

Commit e5f3ff6

Browse files
authored
Merge pull request #1400 from traPtitech/fix/skip_file_security_validate
ファイルアップロードのときにOpenAPIのsecurity validationをしない
2 parents 3acfa53 + bf8ccc7 commit e5f3ff6

File tree

4 files changed

+140
-0
lines changed

4 files changed

+140
-0
lines changed

src/handler/v2/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ func (api *API) setRoutes(e *echo.Echo) error {
104104
// 空のpathのgroupを作成し、oapiMiddleware.OapiRequestValidatorを設定する
105105
apiGroup := e.Group("")
106106
apiGroup.Use(echomiddleware.OapiRequestValidatorWithOptions(swagger, &echomiddleware.Options{
107+
Skipper: fileUploadSkipper,
107108
Options: openapi3filter.Options{
108109
AuthenticationFunc: api.Checker.check,
109110
// validate時にデータがメモリに乗るため、
@@ -113,6 +114,7 @@ func (api *API) setRoutes(e *echo.Echo) error {
113114
ExcludeResponseBody: true,
114115
},
115116
}))
117+
apiGroup.Use(api.fileUploadAuthMiddleware)
116118
openapi.RegisterHandlersWithBaseURL(apiGroup, api, "/api/v2")
117119

118120
return nil

src/handler/v2/checker.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ func (checker *Checker) GameOwnerAuthChecker(ctx context.Context, _ *openapi3fil
336336

337337
// GameMaintainerAuthChecker
338338
// そのゲームのmaintainer(collaborator)であるかどうかを調べるチェッカー
339+
//
340+
// IMPORTANT: [(*Checker).FileUploadAuthMiddleware] は、この関数の第2引数が使われていないことに依存した実装になっている。
339341
func (checker *Checker) GameMaintainerAuthChecker(ctx context.Context, _ *openapi3filter.AuthenticationInput) error {
340342
c := echomiddleware.GetEchoContext(ctx)
341343
// GetEchoContextの内部実装をみるとnilがかえりうるので、

src/handler/v2/file_upload.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package v2
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"path"
7+
"slices"
8+
9+
"github.com/labstack/echo/v4"
10+
echomiddleware "github.com/oapi-codegen/echo-middleware"
11+
)
12+
13+
// isFileUploadRequest はファイルをアップロードするエンドポイントならtrueを返す。
14+
// POST /api/v2/games/:gameID/{files,images,videos} へのリクエストが含まれる。
15+
func isFileUploadRequest(c echo.Context) bool {
16+
if c.Request().Method != http.MethodPost {
17+
return false
18+
}
19+
20+
gameID := c.Param("gameID")
21+
if gameID == "" {
22+
return false
23+
}
24+
25+
targetPathBase := path.Join("/api/v2/games", gameID)
26+
targetPaths := []string{
27+
path.Join(targetPathBase, "files"),
28+
path.Join(targetPathBase, "images"),
29+
path.Join(targetPathBase, "videos"),
30+
}
31+
32+
reqPath := path.Clean(c.Request().URL.Path)
33+
34+
return slices.Contains(targetPaths, reqPath)
35+
}
36+
37+
// fileUploadSkipper はファイルをアップロードするエンドポイントについてバリデーションをスキップする。
38+
// OapiRequestValidator は 内部の ValidateSecurityRequirements でリクエストボディを全部読んでいる。
39+
// そのため、画像・動画・ファイルのアップロード時にメモリ不足になる可能性がある。
40+
// POST /api/v2/games/:gameID/{files,images,videos} へのリクエストは、バリデーションをスキップする。
41+
func fileUploadSkipper(c echo.Context) bool {
42+
return isFileUploadRequest(c)
43+
}
44+
45+
// fileUploadAuthMiddleware はファイルをアップロードするエンドポイントに対してのみ認証を行うミドルウェアを返す。
46+
// POST /api/v2/games/:gameID/{files,images,videos} へのリクエストが含まれる。
47+
//
48+
// IMPORTANT: [(*Checker).GameMaintainerAuthChecker] の第2引数が使われていないことに依存した実装になっている。
49+
func (checker *Checker) fileUploadAuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
50+
return func(c echo.Context) error {
51+
if !isFileUploadRequest(c) {
52+
return next(c)
53+
}
54+
55+
// ref: [github.com/oapi-codegen/echo-middleware.GetEchoContext]
56+
// ここで echo.Context を context.Context に詰め込んでおかないと、GameMaintainerAuthChecker 内で取得できない。
57+
ctx := context.WithValue(c.Request().Context(), echomiddleware.EchoContextKey, c) //nolint:staticcheck
58+
59+
err := checker.GameMaintainerAuthChecker(ctx, nil)
60+
if err != nil {
61+
return err
62+
}
63+
64+
return next(c)
65+
}
66+
}

src/handler/v2/file_upload_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package v2
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func Test_isFileUploadRequest(t *testing.T) {
11+
t.Parallel()
12+
13+
testCases := map[string]struct {
14+
method string
15+
path string
16+
gameID string
17+
want bool
18+
}{
19+
"POST /api/v2/games/:gameID/files": {
20+
method: http.MethodPost,
21+
path: "/api/v2/games/123/files",
22+
gameID: "123",
23+
want: true,
24+
},
25+
"POST /api/v2/games/:gameID/images": {
26+
method: http.MethodPost,
27+
path: "/api/v2/games/123/images",
28+
gameID: "123",
29+
want: true,
30+
},
31+
"POST /api/v2/games/:gameID/videos": {
32+
method: http.MethodPost,
33+
path: "/api/v2/games/123/videos",
34+
gameID: "123",
35+
want: true,
36+
},
37+
"GET /api/v2/games/:gameID/files": {
38+
method: http.MethodGet,
39+
path: "/api/v2/games/123/files",
40+
gameID: "123",
41+
want: false,
42+
},
43+
"POST /api/v2/games/:gameID/others": {
44+
method: http.MethodPost,
45+
path: "/api/v2/games/123/others",
46+
gameID: "123",
47+
want: false,
48+
},
49+
"POST /api/v2/games/files": {
50+
method: http.MethodPost,
51+
path: "/api/v2/games/files",
52+
gameID: "",
53+
want: false,
54+
},
55+
}
56+
57+
for name, testCase := range testCases {
58+
t.Run(name, func(t *testing.T) {
59+
t.Parallel()
60+
61+
c, _, _ := setupTestRequest(t, testCase.method, testCase.path, nil)
62+
c.SetParamNames("gameID")
63+
c.SetParamValues(testCase.gameID)
64+
65+
got := isFileUploadRequest(c)
66+
67+
assert.Equal(t, testCase.want, got)
68+
})
69+
}
70+
}

0 commit comments

Comments
 (0)