fix(security): harden CSRF with Content-Type gate and OpenAPI sync (#2819)
Python Linting / Run Ruff (push) Has been cancelled

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 5 no-body POST handlers (snapshot/save,
manager/queue/{reset,start,update_comfyui}, manager/reboot) to block
<form method=POST> CSRF that bypasses method-only gating. Convert 10 pure
state-changing endpoints (fetch_updates, queue/{update_all,reset,start,
update_comfyui}, snapshot/{remove,restore,save}, comfyui_switch_version,
reboot) from GET to POST and split 5 config endpoints
(db_mode/preview_method/channel_url_list/policy/{component,update}) into
GET(read) + POST(write, JSON body). Emit the in_progress + done event pair
from the /manager/queue/install sync-enable fast-path so client UI
finalizes (previously only queue/start's empty worker done fired, leaving
item.restart unset and the Enable button visible after a successful enable).
Harden js/custom-nodes-manager.js completion path: await onQueueCompleted
with try/catch (surfaces silent turbogrid stale-item throws), replace the
{}.length == 0 no-op empty guard, set install_context before queue/install
to avoid a sync-completion race, wrap classList/updateCell in try/catch.
Resynchronize openapi.yaml with the converted routes (method → post, query
params → requestBody JSON schema, sibling post on 5 split endpoints).
Update 31 JS fetchApi call sites across 7 files; add
tests/test_csrf_content_type_helper.py covering 5 Content-Type cases via
aiohttp TestClient.

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:
Dr.Lt.Data
2026-04-22 05:04:07 +09:00
committed by GitHub
parent d57c142019
commit 491f847bbc
11 changed files with 567 additions and 144 deletions
+14 -14
View File
@@ -470,12 +470,12 @@ async function updateComfyUI() {
set_inprogress_mode();
const response = await api.fetchApi('/manager/queue/update_comfyui');
const response = await api.fetchApi('/manager/queue/update_comfyui', { method: 'POST' });
showTerminal();
is_updating = true;
await api.fetchApi('/manager/queue/start');
await api.fetchApi('/manager/queue/start', { method: 'POST' });
}
function showVersionSelectorDialog(versions, current, onSelect) {
@@ -625,14 +625,14 @@ async function switchComfyUI() {
showVersionSelectorDialog(versions, obj.current, async (selected_version) => {
if(selected_version == 'nightly') {
update_policy_combo.value = 'nightly-comfyui';
api.fetchApi('/manager/policy/update?value=nightly-comfyui');
api.fetchApi('/manager/policy/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: 'nightly-comfyui' }) });
}
else {
update_policy_combo.value = 'stable-comfyui';
api.fetchApi('/manager/policy/update?value=stable-comfyui');
api.fetchApi('/manager/policy/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: 'stable-comfyui' }) });
}
let response = await api.fetchApi(`/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
let response = await api.fetchApi('/comfyui_manager/comfyui_switch_version', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ver: selected_version }), cache: "no-store" });
if (response.status == 200) {
infoToast(`ComfyUI version is switched to ${selected_version}`);
}
@@ -769,10 +769,10 @@ async function updateAll(update_comfyui) {
if(update_comfyui) {
update_all_button.innerText = "Updating ComfyUI...";
await api.fetchApi('/manager/queue/update_comfyui');
await api.fetchApi('/manager/queue/update_comfyui', { method: 'POST' });
}
const response = await api.fetchApi(`/manager/queue/update_all?mode=${mode}`);
const response = await api.fetchApi('/manager/queue/update_all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: mode }) });
if (response.status == 403) {
await handle403Response(response);
@@ -784,7 +784,7 @@ async function updateAll(update_comfyui) {
}
else if(response.status == 200) {
is_updating = true;
await api.fetchApi('/manager/queue/start');
await api.fetchApi('/manager/queue/start', { method: 'POST' });
}
}
@@ -813,7 +813,7 @@ function restartOrStop() {
rebootAPI();
}
else {
api.fetchApi('/manager/queue/reset');
api.fetchApi('/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
}
@@ -967,7 +967,7 @@ class ManagerMenuDialog extends ComfyDialog {
.then(data => { this.datasrc_combo.value = data; });
this.datasrc_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/db_mode?value=${event.target.value}`);
api.fetchApi('/manager/db_mode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
});
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_combo);
@@ -1043,7 +1043,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
// Normal operation
api.fetchApi(`/manager/preview_method?value=${event.target.value}`)
api.fetchApi('/manager/preview_method', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) })
.then(response => {
if (response.status === 403) {
// Feature transitioned to native
@@ -1087,7 +1087,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
channel_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/channel_url_list?value=${event.target.value}`);
api.fetchApi('/manager/channel_url_list', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
});
channel_combo.value = data.selected;
@@ -1152,7 +1152,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
component_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/policy/component?value=${event.target.value}`);
api.fetchApi('/manager/policy/component', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
set_component_policy(event.target.value);
});
@@ -1171,7 +1171,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
update_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/manager/policy/update?value=${event.target.value}`);
api.fetchApi('/manager/policy/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: event.target.value }) });
});
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);