From b4104b9769dcde1d19302f8deb24c7c60d5b6d39 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Mon, 20 Jul 2026 14:28:31 +0530 Subject: [PATCH] feat(media): allow HTML/Markdown/CSV uploads; serve scriptable types as downloads - media upload policy now allows text/html, text/markdown, text/x-markdown, text/csv (in addition to images/av, pdf, txt, zip, office docs) - blob endpoint adds X-Content-Type-Options: nosniff, and forces Content-Disposition: attachment for script-capable types (html, xhtml, svg, xml) so an uploaded file can't render/execute inline from the IIOS origin (stored-XSS). Images/video/audio/pdf still serve inline for preview. - dev-opa test covering the allowed types + unknown/oversize denials Co-Authored-By: Claude Opus 4.8 --- packages/iios-service/src/media/media.controller.ts | 9 +++++++++ .../iios-service/src/platform/dev-opa.port.spec.ts | 13 +++++++++++++ packages/iios-service/src/platform/dev-opa.port.ts | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/iios-service/src/media/media.controller.ts b/packages/iios-service/src/media/media.controller.ts index 666886f..87b1b32 100644 --- a/packages/iios-service/src/media/media.controller.ts +++ b/packages/iios-service/src/media/media.controller.ts @@ -5,6 +5,9 @@ import { SessionVerifier } from '../platform/session.verifier'; import { PresignDownloadDto, PresignUploadDto } from './media.dto'; import type { MessagePrincipal } from '../identity/actor.resolver'; +/** Types that can execute script if a browser renders them top-level — served as downloads only. */ +const SCRIPTABLE_MIMES = new Set(['text/html', 'application/xhtml+xml', 'image/svg+xml', 'text/xml', 'application/xml']); + @Controller('v1/media') export class MediaController { constructor( @@ -37,6 +40,12 @@ export class MediaController { async blob(@Param('token') token: string, @Res() res: Response) { const { data, mime } = await this.media.get(token); res.setHeader('Content-Type', mime); + // Never let the browser MIME-sniff an upload into something executable. + res.setHeader('X-Content-Type-Options', 'nosniff'); + // Script-capable types must not render inline from our origin (stored-XSS) — force a download. + // Images/video/audio/pdf stay inline so the app can preview them. Note /