Summary
Issue/epic attachments can never be downloaded. The file-serving endpoint only accepts object paths under uploads/, but the attachment flow stores objects under attachments/<issueId>/<assetId> and returns download URLs of the form /api/files/attachments/.... Every attachment download is therefore rejected with HTTP 400 before any lookup happens.
Severity
High — a core feature (issue attachments) is completely broken. Uploads appear to succeed, but the file can never be retrieved.
Affected code
api/internal/handler/upload.go:101 — ServeFile rejects any path not prefixed with uploads/:
if path == "" || strings.Contains(path, "..") || !strings.HasPrefix(path, "uploads/") {
c.Status(http.StatusBadRequest); return
}
api/internal/service/attachment.go:101,136,145 — attachments are stored as attachments/<issueId>/<assetId> and the API hands the UI "/api/files/" + objectName (i.e. /api/files/attachments/...).
- Route:
GET /api/files/*path -> ServeFile (api/internal/router/router.go).
Live reproduction (verified)
Authenticated fetch against a running instance:
| Path |
Result |
GET /api/files/attachments/<uuid>/<uuid> |
400 Bad Request |
GET /api/files/uploads/nonexistent-key |
404 Not Found |
The 400-vs-404 contrast isolates the failure to the uploads/-only prefix gate (it rejects attachments/ before checking whether the object exists), so it fails for every attachment regardless of existence.
Suggested fix
Allow the attachments/ prefix in ServeFile (accept both uploads/ and attachments/) while keeping the .. traversal guard. NOTE: address the related hardening item (set X-Content-Type-Options: nosniff + Content-Disposition, constrain attachment content-type) at the same time, otherwise enabling attachments/ serving activates a latent stored-XSS vector.
Summary
Issue/epic attachments can never be downloaded. The file-serving endpoint only accepts object paths under
uploads/, but the attachment flow stores objects underattachments/<issueId>/<assetId>and returns download URLs of the form/api/files/attachments/.... Every attachment download is therefore rejected with HTTP 400 before any lookup happens.Severity
High — a core feature (issue attachments) is completely broken. Uploads appear to succeed, but the file can never be retrieved.
Affected code
api/internal/handler/upload.go:101—ServeFilerejects any path not prefixed withuploads/:api/internal/service/attachment.go:101,136,145— attachments are stored asattachments/<issueId>/<assetId>and the API hands the UI"/api/files/" + objectName(i.e./api/files/attachments/...).GET /api/files/*path->ServeFile(api/internal/router/router.go).Live reproduction (verified)
Authenticated fetch against a running instance:
GET /api/files/attachments/<uuid>/<uuid>GET /api/files/uploads/nonexistent-keyThe 400-vs-404 contrast isolates the failure to the
uploads/-only prefix gate (it rejectsattachments/before checking whether the object exists), so it fails for every attachment regardless of existence.Suggested fix
Allow the
attachments/prefix inServeFile(accept bothuploads/andattachments/) while keeping the..traversal guard. NOTE: address the related hardening item (setX-Content-Type-Options: nosniff+Content-Disposition, constrain attachment content-type) at the same time, otherwise enablingattachments/serving activates a latent stored-XSS vector.