mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-08-05 18:05:08 +08:00
Fix SVG previews broken by the stored-XSS forced-download (#15149)
* Fix SVG previews broken by the stored-XSS forced-download /view and the assets download route force every SVG to application/octet-stream + attachment. That blocks the stored XSS from GHSA-779p-m5rp-r4h4, but it also breaks the SVG node output and Media Assets previews, which request the file with a plain <img>. Exempt only that case. An SVG referenced by an <img> loads in secure static mode with scripting and external references disabled, so the payload cannot fire. The attack needs the SVG to become a document, which arrives with a different Sec-Fetch-Dest. Browsers set that header themselves and page script cannot override it. A missing header, from a non-browser client or a proxy that strips it, fails closed. The blocklist itself is unchanged; this is a call-site gate. * Don't let a cache replay the inline SVG into document context The Sec-Fetch-Dest exemption makes /view and the assets content route vary their Content-Type and Content-Disposition on a request header, but neither response said so. FileResponse emits Last-Modified/ETag and the cache_control middleware skips /view (the filename is in the query string, not the path), so the inline image/svg+xml variant is heuristically cacheable. A cache keyed on the URL alone could hand an entry primed by an <img> load to a later top-level navigation of the same URL, turning the SVG back into a document and re-enabling the stored XSS the forced download blocks. Set Vary: Sec-Fetch-Dest and Cache-Control: no-store on both branches, not just the exempt one: a cached attachment replayed to an <img> would re-break the preview this fix exists to restore. Also strip parameters from content_type before building the assets response. mime_type there is uploader-supplied and unvalidated, and aiohttp rejects a charset in the content_type argument with ValueError, so a stored "image/svg+xml; charset=utf-8" turned a valid inline SVG into a 500. Route-level guards now pin the headers on both branches and the parameterised mime type; all three fail against the previous commit.
This commit is contained in:
@@ -315,15 +315,29 @@ async def download_asset_content(request: web.Request) -> web.Response:
|
||||
404, "FILE_NOT_FOUND", "Underlying file not found on disk."
|
||||
)
|
||||
|
||||
# User-controlled asset content must never render inline in the app origin
|
||||
# User-controlled asset content must not render inline in the app origin
|
||||
# (stored XSS via SVG/HTML/XML). Force dangerous types to download and
|
||||
# override any requested inline disposition. Centralised through
|
||||
# folder_paths.is_dangerous_content_type so this can't drift from /view and
|
||||
# /userdata (the previous inline set here omitted image/svg+xml and missed
|
||||
# the charset/casing/+xml-dialect bypasses).
|
||||
# override any requested inline disposition; SVG loaded into an <img> is
|
||||
# exempt, see renders_safely_as_image. Centralised through folder_paths so
|
||||
# this can't drift from /view and /userdata (the previous inline set here
|
||||
# omitted image/svg+xml and missed the charset/casing/+xml-dialect bypasses).
|
||||
extra_headers = {}
|
||||
sec_fetch_dest = request.headers.get("Sec-Fetch-Dest")
|
||||
if folder_paths.is_dangerous_content_type(content_type):
|
||||
content_type = "application/octet-stream"
|
||||
disposition = "attachment"
|
||||
# This response now depends on a request header, so it must not be
|
||||
# reused across destinations by a browser or intermediary cache: an
|
||||
# inline SVG primed by an <img> fetch and replayed to a document
|
||||
# navigation of the same URL would re-enable the stored XSS.
|
||||
extra_headers["Vary"] = "Sec-Fetch-Dest"
|
||||
extra_headers["Cache-Control"] = "no-store"
|
||||
if not folder_paths.renders_safely_as_image(content_type, sec_fetch_dest):
|
||||
content_type = "application/octet-stream"
|
||||
disposition = "attachment"
|
||||
|
||||
# mime_type is uploader-supplied and unvalidated, so it can carry
|
||||
# parameters. aiohttp rejects a charset in the content_type argument with
|
||||
# ValueError, which would turn a valid inline SVG into a 500.
|
||||
content_type = content_type.split(";", 1)[0].strip() or "application/octet-stream"
|
||||
|
||||
safe_name = (filename or "").replace("\r", "").replace("\n", "")
|
||||
encoded = urllib.parse.quote(safe_name)
|
||||
@@ -356,6 +370,7 @@ async def download_asset_content(request: web.Request) -> web.Response:
|
||||
"Content-Disposition": cd,
|
||||
"Content-Length": str(file_size),
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
**extra_headers,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user