fix(security): harden CSRF with Content-Type gate and expand E2E coverage (#2818)
Publish to PyPI / build-and-publish (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run

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:
Dr.Lt.Data
2026-04-22 05:04:30 +09:00
committed by GitHub
parent 49e205acd4
commit 4410ebc6a6
70 changed files with 13638 additions and 434 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ async function tryInstallCustomNode(event) {
}
}
let response = await api.fetchApi("/v2/manager/reboot");
let response = await api.fetchApi("/v2/manager/reboot", { method: 'POST' });
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
return false;
+16 -7
View File
@@ -628,14 +628,23 @@ async function switchComfyUI() {
showVersionSelectorDialog(versions, obj.current, async (selected_version) => {
if(selected_version == 'nightly') {
update_policy_combo.value = 'nightly-comfyui';
api.fetchApi('/v2/manager/policy/update?value=nightly-comfyui');
api.fetchApi('/v2/manager/policy/update', { method: 'POST', body: JSON.stringify({value: 'nightly-comfyui'}) });
}
else {
update_policy_combo.value = 'stable-comfyui';
api.fetchApi('/v2/manager/policy/update?value=stable-comfyui');
api.fetchApi('/v2/manager/policy/update', { method: 'POST', body: JSON.stringify({value: 'stable-comfyui'}) });
}
let response = await api.fetchApi(`/v2/comfyui_manager/comfyui_switch_version?ver=${selected_version}`, { cache: "no-store" });
let response = await api.fetchApi('/v2/comfyui_manager/comfyui_switch_version', {
method: 'POST',
cache: "no-store",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ver: selected_version,
client_id: api.clientId,
ui_id: api.clientId,
}),
});
if (response.status == 200) {
infoToast(`ComfyUI version is switched to ${selected_version}`);
}
@@ -797,7 +806,7 @@ function restartOrStop() {
rebootAPI();
}
else {
api.fetchApi('/v2/manager/queue/reset');
api.fetchApi('/v2/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
}
@@ -951,7 +960,7 @@ class ManagerMenuDialog extends ComfyDialog {
.then(data => { this.datasrc_combo.value = data; });
this.datasrc_combo.addEventListener('change', function (event) {
api.fetchApi(`/v2/manager/db_mode?value=${event.target.value}`);
api.fetchApi('/v2/manager/db_mode', { method: 'POST', body: JSON.stringify({value: event.target.value}) });
});
const dbRetrievalSetttingItem = createSettingsCombo("DB", this.datasrc_combo);
@@ -973,7 +982,7 @@ class ManagerMenuDialog extends ComfyDialog {
}
channel_combo.addEventListener('change', function (event) {
api.fetchApi(`/v2/manager/channel_url_list?value=${event.target.value}`);
api.fetchApi('/v2/manager/channel_url_list', { method: 'POST', body: JSON.stringify({value: event.target.value}) });
});
channel_combo.value = data.selected;
@@ -1037,7 +1046,7 @@ class ManagerMenuDialog extends ComfyDialog {
});
update_policy_combo.addEventListener('change', function (event) {
api.fetchApi(`/v2/manager/policy/update?value=${event.target.value}`);
api.fetchApi('/v2/manager/policy/update', { method: 'POST', body: JSON.stringify({value: event.target.value}) });
});
const updateSetttingItem = createSettingsCombo("Update", update_policy_combo);
+1 -1
View File
@@ -172,7 +172,7 @@ export function rebootAPI() {
customConfirm("Are you sure you'd like to reboot the server?").then((isConfirmed) => {
if (isConfirmed) {
try {
api.fetchApi("/v2/manager/reboot");
api.fetchApi("/v2/manager/reboot", { method: 'POST' });
}
catch(exception) {}
}
+1 -1
View File
@@ -462,7 +462,7 @@ export class CustomNodesManager {
".cn-manager-stop": {
click: () => {
api.fetchApi('/v2/manager/queue/reset');
api.fetchApi('/v2/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
+1 -1
View File
@@ -170,7 +170,7 @@ export class ModelManager {
".cmm-manager-stop": {
click: () => {
api.fetchApi('/v2/manager/queue/reset');
api.fetchApi('/v2/manager/queue/reset', { method: 'POST' });
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
}
},
+3 -3
View File
@@ -9,7 +9,7 @@ loadCss("./snapshot.css");
async function restore_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/v2/snapshot/restore?target=${target}`, { cache: "no-store" });
const response = await api.fetchApi(`/v2/snapshot/restore?target=${target}`, { method: 'POST', cache: "no-store" });
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
@@ -37,7 +37,7 @@ async function restore_snapshot(target) {
async function remove_snapshot(target) {
if(SnapshotManager.instance) {
try {
const response = await api.fetchApi(`/v2/snapshot/remove?target=${target}`, { cache: "no-store" });
const response = await api.fetchApi(`/v2/snapshot/remove?target=${target}`, { method: 'POST', cache: "no-store" });
if(response.status == 403) {
show_message('This action is not allowed with this security level configuration.');
@@ -63,7 +63,7 @@ async function remove_snapshot(target) {
async function save_current_snapshot() {
try {
const response = await api.fetchApi('/v2/snapshot/save', { cache: "no-store" });
const response = await api.fetchApi('/v2/snapshot/save', { method: 'POST', cache: "no-store" });
app.ui.dialog.close();
return true;
}