diff --git a/backend/src/api/routers/files.py b/backend/src/api/routers/files.py index 0035364..2ff8ebf 100644 --- a/backend/src/api/routers/files.py +++ b/backend/src/api/routers/files.py @@ -9,6 +9,7 @@ from fastapi.responses import FileResponse, PlainTextResponse from utils.files import ( content_disposition, counts_as_download, + is_active_mime, is_inline_mime, resolve_mime, ) @@ -20,7 +21,12 @@ router = APIRouter(tags=["files"], route_class=DishkaRoute) _GONE = "This link is no longer available.\n" _MISSING = "Not found.\n" -_NO_STORE = {"Cache-Control": "private, no-store", "X-Robots-Tag": "noindex, nofollow"} +_NO_STORE = { + "Cache-Control": "private, no-store", + "X-Robots-Tag": "noindex, nofollow", + "X-Content-Type-Options": "nosniff", +} +_SANDBOX = {"Content-Security-Policy": "sandbox"} def _client_ip(request: Request) -> str | None: @@ -76,6 +82,7 @@ async def _serve( media_type=mime, headers={ **_NO_STORE, + **(_SANDBOX if is_active_mime(mime) else {}), "Content-Disposition": content_disposition( row["file_name"], attachment=attachment ), diff --git a/backend/src/utils/files.py b/backend/src/utils/files.py index 0859f4a..48664be 100644 --- a/backend/src/utils/files.py +++ b/backend/src/utils/files.py @@ -67,6 +67,13 @@ _GENERIC_MIMES = { _INLINE_MIME_PREFIXES = ("image/", "video/", "audio/", "text/") _INLINE_MIMES = {"application/pdf", "application/json"} +_ACTIVE_MIMES = { + "text/html", + "application/xhtml+xml", + "image/svg+xml", + "text/xml", + "application/xml", +} _PREVIEW_AGENTS = ( "telegrambot", @@ -202,6 +209,10 @@ def is_inline_mime(mime: str | None) -> bool: return mime in _INLINE_MIMES or mime.startswith(_INLINE_MIME_PREFIXES) +def is_active_mime(mime: str | None) -> bool: + return bool(mime) and mime.split(";", 1)[0].strip().lower() in _ACTIVE_MIMES + + def content_disposition(file_name: str, *, attachment: bool) -> str: kind = "attachment" if attachment else "inline" ascii_name = file_name.encode("ascii", "replace").decode("ascii").replace('"', "_") diff --git a/frontend/src/lib/api/media.ts b/frontend/src/lib/api/media.ts index 4cd6796..4d2d34e 100644 --- a/frontend/src/lib/api/media.ts +++ b/frontend/src/lib/api/media.ts @@ -76,6 +76,18 @@ export function isPreviewable(kind: string, mime: string | null): boolean { return PREVIEW_MIME_PREFIXES.some((prefix) => mime?.startsWith(prefix)); } +const ACTIVE_MIMES = new Set([ + "text/html", + "application/xhtml+xml", + "image/svg+xml", + "text/xml", + "application/xml", +]); + +export function isActiveContent(mime: string | null): boolean { + return ACTIVE_MIMES.has(mime?.split(";", 1)[0].trim().toLowerCase() ?? ""); +} + export type VisualKind = "image" | "video" | "other"; const VIDEO_KINDS = new Set(["video", "video_note", "animation", "gif"]); diff --git a/frontend/src/lib/components/InlineButtons.svelte b/frontend/src/lib/components/InlineButtons.svelte index 9584ce6..773afe2 100644 --- a/frontend/src/lib/components/InlineButtons.svelte +++ b/frontend/src/lib/components/InlineButtons.svelte @@ -1,5 +1,6 @@