docs: capture todo - uploads keep the client extension (stored XSS, g15office bemfi2zs)

This commit is contained in:
2026-10-11 01:35:37 +02:00
parent c53ae6a39b
commit 4d107f4ccb

View File

@@ -0,0 +1,32 @@
---
created: 2026-10-11T00:00:00Z
title: Uploads keep the client file extension, so a valid image can be served as HTML (stored XSS)
priority: high
area: lagoon/attach, fonoteka.go uploads
severity: major
g15office: bemfi2zs
files:
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/collection_media_controller.go (storeUpload)
- ../fonoteka.go/plugins/golem15/fonoteka/classes/album_files.go (StorePublicFile)
- modules/lagoon/attach
---
## Problem
Found in the Phase 12 code review (WR-01 in `.planning/phases/12-p-ytarium-api-collections-and-albums/12-REVIEW.md`).
Public uploads keep the extension the client sent. The web server serves `storage/app/uploads/public` and picks the Content-Type from the extension; the blob's `ContentType` metadata does not reach it. A PNG with `<script>` in a `tEXt` chunk passes both upload checks (`image|mimes:...` sniffs the content, `IsAllowedImage` decodes it). Uploaded as `x.html`, it is stored as `<hex>.html`, its URL comes back in the 201 response and every listing, and a browser renders it as HTML.
WinterCMS's `File::getDiskName` behaves the same way (inherited, not introduced), but any household member or write-scoped token can exploit it. Still present as of 2026-10-11: `storeUpload` appends `filepath.Ext(upload.Filename)` to the disk name.
## Solution
- Derive the stored extension from the sniffed MIME type (`image/png` → `png`, and so on) in one shared helper, preferably in `lagoon/attach` so every plugin gets it. Keep the client name only in `file_name`.
- The URL changes only for uploads whose client extension disagrees with their content; the parity corpus does not record that case.
- Defence in depth: `X-Content-Type-Options: nosniff` and a file-type allow-list on the web server's uploads location, documented in the deploy docs.
- Shares lines with the CR-01 fix in the application project (odd extensions breaking every listing).
## Acceptance
- A PNG uploaded as `x.html` is stored with a `.png` disk name and its URL ends in `.png`.
- A regression test covers that upload.