diff --git a/routes/emoji_routes.py b/routes/emoji_routes.py
index 4b92079..76f6aba 100644
--- a/routes/emoji_routes.py
+++ b/routes/emoji_routes.py
@@ -16,7 +16,7 @@ from pathlib import Path
import httpx
from fastapi import APIRouter
-from fastapi.responses import FileResponse, Response
+from fastapi.responses import Response
logger = logging.getLogger(__name__)
@@ -26,12 +26,42 @@ _CACHE_DIR = Path(__file__).resolve().parent.parent / "data" / "emoji_cache"
_OPENMOJI_BASE = "https://cdn.jsdelivr.net/npm/openmoji@15.0.0/black/svg"
# codepoints like "1f600" or "1f468-200d-1f469-200d-1f467" (lowercase hex, '-' joined)
_CODE_RE = re.compile(r"^[0-9a-f]{2,6}(?:-[0-9a-f]{2,6})*$")
-_SVG_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"}
+_MAX_SVG_BYTES = 256 * 1024
+_BLOCKED_SVG_RE = re.compile(
+ br"<\s*(?:script|foreignObject|iframe|object|embed|image)\b|"
+ br"\bon[a-z0-9_-]+\s*=",
+ re.IGNORECASE,
+)
+_EXTERNAL_REF_RE = re.compile(
+ br"\b(?:href|xlink:href)\s*=\s*['\"](?:https?:|//|data:|javascript:)",
+ re.IGNORECASE,
+)
+_SVG_SECURITY_HEADERS = {
+ "X-Content-Type-Options": "nosniff",
+ "Content-Security-Policy": "sandbox",
+ "Cross-Origin-Resource-Policy": "same-origin",
+}
+_SVG_HEADERS = {
+ "Cache-Control": "public, max-age=31536000, immutable",
+ **_SVG_SECURITY_HEADERS,
+}
# Returned when a codepoint is unknown/unreachable: an empty (transparent) SVG,
# so the CSS mask renders nothing instead of a solid box. Not cached, so a later
# request can still pick up the real glyph once the CDN is reachable.
_BLANK_SVG = b''
-_BLANK_HEADERS = {"Cache-Control": "no-store"}
+_BLANK_HEADERS = {"Cache-Control": "no-store", **_SVG_SECURITY_HEADERS}
+
+
+def _is_safe_svg(content: bytes) -> bool:
+ if not isinstance(content, bytes) or not content:
+ return False
+ if len(content) > _MAX_SVG_BYTES:
+ return False
+ if b"