mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-07-26 10:07:38 +08:00
fix(security): harden CSRF with Content-Type gate and expand E2E coverage (#2818)
Defense-in-depth over GET→POST alone: reject the three CORS-safelisted simple-form Content-Types (x-www-form-urlencoded, multipart/form-data, text/plain) on 16 no-body POST handlers (glob + legacy) to block <form method=POST> CSRF that bypasses method-only gating. Move comfyui_switch_version to a JSON body so the preflight requirement applies. Split db_mode/policy/update/channel_url_list into GET(read) + POST(write). Tighten do_fix (high → high+) and gate three previously-ungated config setters at middle. Resynchronize openapi.yaml (27 paths, 30 operations, ComfyUISwitchVersionParams as a shared $ref component). Add E2E harness variants, Playwright config, CSRF/secgate suites, 39-endpoint coverage, and a CHANGELOG. Breaking: legacy per-op POST routes (install/uninstall/fix/disable/update/ reinstall/abort_current) are removed; callers already use queue/batch. Legacy /manager/notice (v1) is removed; /v2/manager/notice is retained. Reported-by: XlabAI Team of Tencent Xuanwu Lab CVSS: 8.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H)
This commit is contained in:
@@ -1,10 +1,74 @@
|
||||
"""Security helpers for CSRF protection and Content-Type gating.
|
||||
|
||||
reject_simple_form_post() is applied ONLY to POST handlers that do not consume
|
||||
a request body (e.g., snapshot/save, queue/reset, queue/start, reboot). These
|
||||
are vulnerable to cross-origin <form method=POST> attacks because the server
|
||||
accepts the request without parsing any body — the attacker needs no ability
|
||||
to forge a valid payload, only to point a hidden form at the URL.
|
||||
|
||||
Handlers that DO read a body via ``await request.json()`` (install/git_url,
|
||||
install/pip, queue/install_model, db_mode POST, policy/update POST,
|
||||
channel_url_list POST, queue/batch, queue/task, import_fail_info, etc.) are
|
||||
NOT gated here — a cross-origin <form method=POST> cannot forge a valid JSON
|
||||
body because the browser refuses to send ``application/json`` without a CORS
|
||||
preflight, which this server rejects by not responding with an appropriate
|
||||
Access-Control-Allow-Origin.
|
||||
|
||||
DO NOT add the gate to body-reading handlers (redundant + UX-breaking).
|
||||
DO NOT remove the gate from no-body handlers (this is the bypass vector).
|
||||
"""
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
is_personal_cloud_mode = False
|
||||
handler_policy = {}
|
||||
|
||||
|
||||
# CORS "simple request" Content-Type set per Fetch spec §3.2.3. Browsers send
|
||||
# <form method=POST> submissions with one of these three MIME types and do NOT
|
||||
# trigger a CORS preflight, so a malicious cross-origin page can silently POST
|
||||
# into state-changing endpoints if we only gate on HTTP method. Blocking these
|
||||
# three Content-Types on our mutation endpoints forces any non-same-origin POST
|
||||
# to use a non-simple Content-Type (e.g. application/json), which triggers a
|
||||
# preflight that this server rejects (no Access-Control-Allow-Origin response).
|
||||
_SIMPLE_FORM_CONTENT_TYPES = frozenset({
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data',
|
||||
'text/plain',
|
||||
})
|
||||
|
||||
|
||||
def reject_simple_form_post(request) -> Optional[web.Response]:
|
||||
"""Reject Content-Types that enable preflight-less <form method=POST> CSRF.
|
||||
|
||||
These 3 MIME types are the complete CORS "simple request" Content-Type set
|
||||
(Fetch spec §3.2.3 "CORS-safelisted request-header"). Blocking them
|
||||
eliminates the <form method=POST> cross-origin CSRF vector, because any
|
||||
other Content-Type triggers a browser-enforced CORS preflight — and this
|
||||
server does not answer preflights with ``Access-Control-Allow-Origin``,
|
||||
effectively blocking cross-origin requests that use non-simple types.
|
||||
|
||||
Returns:
|
||||
web.Response(status=400) when the request has a simple-form
|
||||
Content-Type that must be rejected. None when the request is allowed
|
||||
to proceed (no body, application/json, or any non-simple Content-Type).
|
||||
|
||||
Note:
|
||||
aiohttp's ``request.content_type`` normalizes the header (lower-cases,
|
||||
strips parameters), so a ``multipart/form-data; boundary=----X`` header
|
||||
is compared as ``multipart/form-data``.
|
||||
"""
|
||||
if request.content_type in _SIMPLE_FORM_CONTENT_TYPES:
|
||||
return web.Response(
|
||||
status=400,
|
||||
text='Invalid Content-Type for this endpoint. Use application/json or omit body.',
|
||||
)
|
||||
return None
|
||||
|
||||
class HANDLER_POLICY(Enum):
|
||||
MULTIPLE_REMOTE_BAN_NON_LOCAL = 1
|
||||
MULTIPLE_REMOTE_BAN_NOT_PERSONAL_CLOUD = 2
|
||||
|
||||
Reference in New Issue
Block a user