Skip to content

Commit 686b61d

Browse files
committed
feat: implement file size limit of 10MB and add tests
1 parent bff390c commit 686b61d

File tree

2 files changed

+126
-5
lines changed

2 files changed

+126
-5
lines changed

lib/httpapi/server.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -444,10 +444,15 @@ func (s *Server) uploadFiles(ctx context.Context, input *struct {
444444

445445
file := formData.File.File
446446

447-
buf, err := io.ReadAll(file)
447+
// Limit file size to 10MB
448+
const maxFileSize = 10 << 20 // 10MB
449+
buf, err := io.ReadAll(io.LimitReader(file, maxFileSize+1))
448450
if err != nil {
449451
return nil, xerrors.Errorf("failed to upload file: %w", err)
450452
}
453+
if len(buf) > maxFileSize {
454+
return nil, huma.Error400BadRequest("file size exceeds 10MB limit")
455+
}
451456

452457
// Calculate checksum of the uploaded file to create unique subdirectory
453458
hash := sha256.Sum256(buf)
@@ -460,8 +465,8 @@ func (s *Server) uploadFiles(ctx context.Context, input *struct {
460465
return nil, xerrors.Errorf("failed to create upload directory: %w", err)
461466
}
462467

463-
// Save individual file with original filename
464-
filename := formData.File.Filename
468+
// Save individual file with original filename (extract just the base filename for security)
469+
filename := filepath.Base(formData.File.Filename)
465470

466471
outPath := filepath.Join(uploadDir, filename)
467472
err = os.WriteFile(outPath, buf, 0644)

lib/httpapi/server_test.go

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http"
1111
"net/http/httptest"
1212
"os"
13+
"path/filepath"
1314
"strings"
1415
"testing"
1516

@@ -706,6 +707,34 @@ func TestServer_UploadFiles(t *testing.T) {
706707
expectedStatusCode: http.StatusOK,
707708
expectFilePath: true,
708709
},
710+
{
711+
name: "upload file with absolute path filename",
712+
filename: "/tmp/absolute-path-file.txt",
713+
fileContent: "absolute path content",
714+
expectedStatusCode: http.StatusOK,
715+
expectFilePath: true,
716+
},
717+
{
718+
name: "upload file with relative path filename",
719+
filename: "../relative-path-file.txt",
720+
fileContent: "relative path content",
721+
expectedStatusCode: http.StatusOK,
722+
expectFilePath: true,
723+
},
724+
{
725+
name: "upload file with nested relative path",
726+
filename: "nested/path/file.txt",
727+
fileContent: "nested content",
728+
expectedStatusCode: http.StatusOK,
729+
expectFilePath: true,
730+
},
731+
{
732+
name: "upload file with backslash path separators",
733+
filename: "windows\\style\\path.txt",
734+
fileContent: "windows path content",
735+
expectedStatusCode: http.StatusOK,
736+
expectFilePath: true,
737+
},
709738
}
710739

711740
for _, tc := range cases {
@@ -766,8 +795,8 @@ func TestServer_UploadFiles(t *testing.T) {
766795
require.NoError(t, err)
767796
require.Equal(t, tc.fileContent, string(savedContent), "file content should match")
768797

769-
// Verify filename is preserved in the path
770-
expectedFilename := tc.filename
798+
// Verify filename is preserved in the path (only the base filename for security)
799+
expectedFilename := filepath.Base(tc.filename)
771800
if expectedFilename == "" {
772801
expectedFilename = "uploaded_file"
773802
}
@@ -839,4 +868,91 @@ func TestServer_UploadFiles_Errors(t *testing.T) {
839868

840869
require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
841870
})
871+
872+
t.Run("file size exactly 10MB", func(t *testing.T) {
873+
t.Parallel()
874+
875+
// Create exactly 10MB of data
876+
const tenMB = 10 << 20
877+
fileContent := make([]byte, tenMB)
878+
for i := range fileContent {
879+
fileContent[i] = byte(i % 256)
880+
}
881+
882+
var buf bytes.Buffer
883+
writer := multipart.NewWriter(&buf)
884+
part, err := writer.CreateFormFile("file", "10mb-file.bin")
885+
require.NoError(t, err)
886+
_, err = part.Write(fileContent)
887+
require.NoError(t, err)
888+
err = writer.Close()
889+
require.NoError(t, err)
890+
891+
req, err := http.NewRequest("POST", tsServer.URL+"/upload", &buf)
892+
require.NoError(t, err)
893+
req.Header.Set("Content-Type", writer.FormDataContentType())
894+
895+
client := &http.Client{}
896+
resp, err := client.Do(req)
897+
require.NoError(t, err)
898+
t.Cleanup(func() {
899+
_ = resp.Body.Close()
900+
})
901+
902+
require.Equal(t, http.StatusOK, resp.StatusCode)
903+
904+
// Parse response to get file path for cleanup
905+
body, err := io.ReadAll(resp.Body)
906+
require.NoError(t, err)
907+
var uploadResp struct {
908+
Ok bool `json:"ok"`
909+
FilePath string `json:"filePath"`
910+
}
911+
err = json.Unmarshal(body, &uploadResp)
912+
require.NoError(t, err)
913+
require.True(t, uploadResp.Ok)
914+
915+
// Clean up the uploaded file
916+
t.Cleanup(func() {
917+
_ = os.Remove(uploadResp.FilePath)
918+
})
919+
})
920+
921+
t.Run("file size exceeds 10MB limit", func(t *testing.T) {
922+
t.Parallel()
923+
924+
// Create slightly more than 10MB of data
925+
const tenMBPlusOne = (10 << 20) + 1
926+
fileContent := make([]byte, tenMBPlusOne)
927+
for i := range fileContent {
928+
fileContent[i] = byte(i % 256)
929+
}
930+
931+
var buf bytes.Buffer
932+
writer := multipart.NewWriter(&buf)
933+
part, err := writer.CreateFormFile("file", "large-file.bin")
934+
require.NoError(t, err)
935+
_, err = part.Write(fileContent)
936+
require.NoError(t, err)
937+
err = writer.Close()
938+
require.NoError(t, err)
939+
940+
req, err := http.NewRequest("POST", tsServer.URL+"/upload", &buf)
941+
require.NoError(t, err)
942+
req.Header.Set("Content-Type", writer.FormDataContentType())
943+
944+
client := &http.Client{}
945+
resp, err := client.Do(req)
946+
require.NoError(t, err)
947+
t.Cleanup(func() {
948+
_ = resp.Body.Close()
949+
})
950+
951+
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
952+
953+
// Verify error message
954+
body, err := io.ReadAll(resp.Body)
955+
require.NoError(t, err)
956+
require.Contains(t, string(body), "file size exceeds 10MB limit")
957+
})
842958
}

0 commit comments

Comments
 (0)