@@ -445,26 +445,39 @@ export class FilesRouter {
445445
446446 // When the filename extension is not recognized by `mime`,
447447 // `FilesController.createFile` cannot derive a Content-Type from the
448- // filename and preserves the client-supplied Content-Type verbatim. The
449- // type the file is actually served as must therefore be validated against
450- // the blocklist. A Content-Type that does not parse as `type/subtype` with
451- // a non-empty type AND subtype (e.g. `image`, `image/`) is unparseable:
452- // browsers ignore it and fall back to MIME-sniffing the file body, which
453- // can render HTML/script markers as active content on storage adapters
454- // that serve the stored Content-Type. Reject such malformed values rather
455- // than store them verbatim, unless extension filtering is disabled (`*`).
448+ // filename and preserves the client-supplied Content-Type verbatim, so the
449+ // type the file is actually served as must be validated. Skip this when
450+ // extension filtering is disabled (`*`).
456451 const allowsAllExtensions = fileExtensions . includes ( '*' ) ;
457452 if ( ! isExtensionRecognized && contentType && ! allowsAllExtensions ) {
458453 const slashIndex = contentType . indexOf ( '/' ) ;
459454 const type = slashIndex > 0 ? contentType . slice ( 0 , slashIndex ) . trim ( ) : '' ;
460455 const subtype =
461456 slashIndex > 0 ? contentType . slice ( slashIndex + 1 ) . split ( ';' ) [ 0 ] . trim ( ) : '' ;
462457 if ( ! type || ! subtype ) {
458+ // A Content-Type that does not parse as `type/subtype` with a non-empty
459+ // type AND subtype is malformed: there is no valid MIME type without a
460+ // subtype (RFC 9110 §8.3.1). Browsers cannot parse it and fall back to
461+ // MIME-sniffing the file body, which can render HTML/script markers as
462+ // active content on storage adapters that serve the stored Content-Type
463+ // (e.g. `image`, `image/`). Surface the precise blocklist message when
464+ // the bare token names a blocked extension (e.g. a no-slash `svg`),
465+ // otherwise reject the unparseable Content-Type.
466+ const bareToken = ( slashIndex < 0 ? contentType . split ( ';' ) [ 0 ] : type ) . replace (
467+ / \s + / g,
468+ ''
469+ ) ;
470+ if ( bareToken && ! isValidExtension ( bareToken ) ) {
471+ rejectExtension ( bareToken ) ;
472+ return ;
473+ }
463474 next ( new Parse . Error ( Parse . Error . FILE_SAVE_ERROR , 'Invalid Content-Type.' ) ) ;
464475 return ;
465476 }
466- // Validate the Content-Type subtype against the blocklist, e.g.
477+ // Validate the well-formed Content-Type subtype against the blocklist, e.g.
467478 // "image/svg+xml" -> "svg+xml", "image/svg+xml;charset=utf-8" -> "svg+xml".
479+ // Valid custom/vendor types (e.g. "application/vnd.api+json") parse and are
480+ // allowed; only blocked subtypes are rejected.
468481 const contentTypeExtension = subtype . replace ( / \s + / g, '' ) ;
469482 if ( ! isValidExtension ( contentTypeExtension ) ) {
470483 rejectExtension ( contentTypeExtension ) ;
0 commit comments