@@ -74,11 +74,16 @@ func (f *s3Copier) Start(slug, version, shasum string) (bool, error) {
7474
7575func (f * s3Copier ) Copy (stat os.FileInfo , src io.Reader ) error {
7676 if ! f .started {
77- panic ( " copier should call Start() before Copy()" )
77+ return fmt . Errorf ( "appfs: copier must call Start() before Copy()" )
7878 }
7979
8080 // Write directly to the final location (appObj/filename).
81- objName := path .Join (f .appObj , stat .Name ())
81+ // Reject path traversal attempts in filenames.
82+ name := stat .Name ()
83+ if strings .Contains (name , ".." ) {
84+ return fmt .Errorf ("appfs: invalid filename %q" , name )
85+ }
86+ objName := path .Join (f .appObj , name )
8287
8388 contentType := filetype .ByExtension (path .Ext (stat .Name ()))
8489 if contentType == "" {
@@ -173,15 +178,21 @@ func (s *s3Server) ServeFileContent(w http.ResponseWriter, req *http.Request, sl
173178 }
174179
175180 if checkETag := req .Header .Get ("Cache-Control" ) == "" ; checkETag {
176- etag := fmt .Sprintf (`"%s"` , info .ETag [:10 ])
181+ etagVal := info .ETag
182+ if len (etagVal ) > 10 {
183+ etagVal = etagVal [:10 ]
184+ }
185+ etag := fmt .Sprintf (`"%s"` , etagVal )
177186 if web_utils .CheckPreconditions (w , req , etag ) {
178187 return nil
179188 }
180189 w .Header ().Set ("Etag" , etag )
181190 }
182191
183192 // Read the full object to handle brotli decompression.
184- content , err := io .ReadAll (obj )
193+ // Limit to 50 MiB to avoid unbounded memory allocation from corrupted objects.
194+ const maxAppFileSize = 50 << 20
195+ content , err := io .ReadAll (io .LimitReader (obj , maxAppFileSize ))
185196 if err != nil {
186197 return err
187198 }
@@ -269,6 +280,10 @@ func (s *s3Server) makeObjectName(slug, version, shasum, file string) string {
269280 if shasum != "" {
270281 basepath += "-" + shasum
271282 }
283+ // Prevent path traversal
284+ if strings .Contains (file , ".." ) {
285+ return basepath + "/invalid"
286+ }
272287 return path .Join (basepath , file )
273288}
274289
@@ -338,11 +353,16 @@ func isS3NotFound(err error) bool {
338353 return code == "NoSuchKey" || code == "NoSuchBucket"
339354}
340355
341- // wrapS3ErrNotExist converts S3 not-found errors to os.ErrNotExist.
356+ // wrapS3ErrNotExist converts S3 not-found errors to os.ErrNotExist and
357+ // sanitizes other S3 errors to avoid leaking internal bucket/key details.
342358func wrapS3ErrNotExist (err error ) error {
343359 if isS3NotFound (err ) {
344360 return os .ErrNotExist
345361 }
362+ code := minio .ToErrorResponse (err ).Code
363+ if code != "" {
364+ return fmt .Errorf ("s3 storage error: %s" , code )
365+ }
346366 return err
347367}
348368
0 commit comments