mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-14 05:39:08 +02:00
fix(security): normalize MIME type in isInlineContentType (#3050)
isInlineContentType is the security boundary that decides whether an uploaded file is served with Content-Disposition: inline (renderable in the document origin) or attachment. The SVG carve-out added in #3023 to block stored-XSS via uploaded .svg only matched the exact literal "image/svg+xml", so callers that supply "IMAGE/SVG+XML", "image/svg+xml; charset=utf-8", or whitespace-padded variants would still see disposition=inline. MIME type matching is case-insensitive per RFC 2045 §5.1 and may carry parameters, so the safe thing is to normalize at the boundary instead of trusting every caller. Today both call sites (S3.Upload and LocalStorage.Serve) happen to feed in the exact literal because the upload handler overrides .svg to "image/svg+xml" before storage sees it, so this is defense-in-depth rather than a live regression. Hardens the helper so any future caller (including one that ever trusts a client-supplied Content-Type) stays behind the same guard. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -27,12 +27,22 @@ func sanitizeFilename(name string) string {
|
||||
// and can carry <script>, <foreignObject>, or onload= attributes that
|
||||
// execute in the document's origin when rendered inline. Forcing
|
||||
// attachment disposition prevents stored-XSS via uploaded .svg files.
|
||||
//
|
||||
// Input is normalized (trim, lowercase, strip parameters) before matching
|
||||
// so that values like "image/svg+xml; charset=utf-8" or "IMAGE/SVG+XML"
|
||||
// can't slip past the SVG carve-out. RFC 2045 §5.1 defines MIME type
|
||||
// matching as case-insensitive with optional parameters; this is the
|
||||
// security boundary, so normalize here instead of trusting callers.
|
||||
func isInlineContentType(ct string) bool {
|
||||
if ct == "image/svg+xml" {
|
||||
mediaType := strings.ToLower(strings.TrimSpace(ct))
|
||||
if i := strings.IndexByte(mediaType, ';'); i >= 0 {
|
||||
mediaType = strings.TrimSpace(mediaType[:i])
|
||||
}
|
||||
if mediaType == "image/svg+xml" {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(ct, "image/") ||
|
||||
strings.HasPrefix(ct, "video/") ||
|
||||
strings.HasPrefix(ct, "audio/") ||
|
||||
ct == "application/pdf"
|
||||
return strings.HasPrefix(mediaType, "image/") ||
|
||||
strings.HasPrefix(mediaType, "video/") ||
|
||||
strings.HasPrefix(mediaType, "audio/") ||
|
||||
mediaType == "application/pdf"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,18 @@ func TestIsInlineContentType(t *testing.T) {
|
||||
|
||||
// SVG must NOT render inline — it can carry executable script.
|
||||
{"image/svg+xml", false},
|
||||
// MIME types are case-insensitive (RFC 2045 §5.1) and may carry
|
||||
// parameters. The SVG carve-out is a security boundary, so any
|
||||
// variant that resolves to image/svg+xml must also be blocked.
|
||||
{"IMAGE/SVG+XML", false},
|
||||
{"Image/Svg+Xml", false},
|
||||
{"image/svg+xml; charset=utf-8", false},
|
||||
{"image/svg+xml;charset=utf-8", false},
|
||||
{" image/svg+xml ", false},
|
||||
// Normalization must not break the positive cases either.
|
||||
{"IMAGE/PNG", true},
|
||||
{"image/png; foo=bar", true},
|
||||
{" application/pdf", true},
|
||||
|
||||
{"text/html", false},
|
||||
{"application/octet-stream", false},
|
||||
|
||||
Reference in New Issue
Block a user