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:
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Self-verify reports/e2e_verification_audit.md tally consistency.
|
||||
|
||||
For each test-file section, parse the verdict column of every row in the
|
||||
section's table and count PASS / WEAK / INADEQUATE / N/A symbols. Cross-check
|
||||
against (a) the section's own "File verdict: ..." line and (b) the Summary
|
||||
Matrix row for that filename.
|
||||
|
||||
Exit 0 on full consistency, 1 on any mismatch.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
AUDIT = Path(__file__).resolve().parent.parent / "reports" / "e2e_verification_audit.md"
|
||||
|
||||
PASS_SYM = "\u2705"
|
||||
WEAK_SYM = "\u26a0"
|
||||
INADEQ_SYM = "\u274c"
|
||||
|
||||
SECTION_RE = re.compile(
|
||||
r"^(?:# Section \d+|## \d+\.)\s+\u2014?\s*(?P<file>\S+\.(?:py|spec\.ts))"
|
||||
)
|
||||
VERDICT_LINE_RE = re.compile(r"^\*\*File verdict\*\*:\s*(?P<body>.+)$")
|
||||
SUMMARY_ROW_RE = re.compile(
|
||||
r"^\|\s*(?P<file>[^|]+?)\s*\|"
|
||||
r"\s*(?P<p>\d+)\s*\|"
|
||||
r"\s*(?P<w>\d+)\s*\|"
|
||||
r"\s*(?P<i>\d+)\s*\|"
|
||||
r"\s*(?P<n>\d+)\s*\|"
|
||||
r"\s*(?P<t>\d+)\s*\|\s*$"
|
||||
)
|
||||
TOTAL_ROW_RE = re.compile(
|
||||
r"^\|\s*\*\*TOTAL\*\*\s*\|"
|
||||
r"\s*\*\*(?P<p>\d+)\*\*\s*\|"
|
||||
r"\s*\*\*(?P<w>\d+)\*\*\s*\|"
|
||||
r"\s*\*\*(?P<i>\d+)\*\*\s*\|"
|
||||
r"\s*\*\*(?P<n>\d+)\*\*\s*\|"
|
||||
r"\s*\*\*(?P<t>\d+)\*\*\s*\|\s*$"
|
||||
)
|
||||
ALL_N_TESTS_RE = re.compile(r"All\s+(\d+)\s+tests", re.IGNORECASE)
|
||||
|
||||
|
||||
def count_symbols(line: str) -> tuple[int, int, int, int]:
|
||||
p = line.count(PASS_SYM)
|
||||
w = line.count(WEAK_SYM)
|
||||
i = line.count(INADEQ_SYM)
|
||||
n = 0
|
||||
if re.search(r"\|\s*N/A\s*\|", line) or re.search(r"\|\s*N/A\s*\u2014", line):
|
||||
bulk = ALL_N_TESTS_RE.search(line)
|
||||
n = int(bulk.group(1)) if bulk else 1
|
||||
return p, w, i, n
|
||||
|
||||
|
||||
def parse_file_verdict(body: str) -> tuple[int, int, int, int]:
|
||||
p = w = i = n = 0
|
||||
for match in re.finditer(r"(\d+)/\d+\s*(\S)", body):
|
||||
num, sym = int(match.group(1)), match.group(2)
|
||||
if sym == PASS_SYM:
|
||||
p = num
|
||||
elif sym == WEAK_SYM:
|
||||
w = num
|
||||
elif sym == INADEQ_SYM:
|
||||
i = num
|
||||
n_match = re.search(r"(\d+)/\d+\s*N/A", body)
|
||||
if n_match:
|
||||
n = int(n_match.group(1))
|
||||
return p, w, i, n
|
||||
|
||||
|
||||
def main() -> int:
|
||||
content = AUDIT.read_text(encoding="utf-8").splitlines()
|
||||
|
||||
sections: dict[str, dict[str, tuple[int, int, int, int]]] = {}
|
||||
current_file: str | None = None
|
||||
row_tally = (0, 0, 0, 0)
|
||||
in_table = False
|
||||
|
||||
for line in content:
|
||||
m = SECTION_RE.match(line)
|
||||
if m:
|
||||
if current_file is not None:
|
||||
sections.setdefault(current_file, {})["rows"] = row_tally
|
||||
current_file = os.path.basename(m.group("file").strip())
|
||||
row_tally = (0, 0, 0, 0)
|
||||
in_table = False
|
||||
continue
|
||||
if current_file is None:
|
||||
continue
|
||||
if line.startswith("| Test ") or re.match(r"^\|\s*-+", line) or line.startswith("|---"):
|
||||
in_table = True
|
||||
continue
|
||||
if in_table and line.startswith("|"):
|
||||
p, w, i, n = count_symbols(line)
|
||||
row_tally = (row_tally[0] + p, row_tally[1] + w, row_tally[2] + i, row_tally[3] + n)
|
||||
continue
|
||||
if in_table and not line.startswith("|"):
|
||||
in_table = False
|
||||
fv = VERDICT_LINE_RE.match(line)
|
||||
if fv and current_file:
|
||||
sections.setdefault(current_file, {})["file_verdict"] = parse_file_verdict(fv.group("body"))
|
||||
|
||||
if current_file is not None:
|
||||
sections.setdefault(current_file, {})["rows"] = row_tally
|
||||
|
||||
summary_rows: dict[str, tuple[int, int, int, int, int]] = {}
|
||||
total_row: tuple[int, int, int, int, int] | None = None
|
||||
for line in content:
|
||||
tm = TOTAL_ROW_RE.match(line)
|
||||
if tm:
|
||||
total_row = (
|
||||
int(tm.group("p")), int(tm.group("w")), int(tm.group("i")),
|
||||
int(tm.group("n")), int(tm.group("t")),
|
||||
)
|
||||
continue
|
||||
sm = SUMMARY_ROW_RE.match(line)
|
||||
if sm:
|
||||
name = sm.group("file").strip()
|
||||
if name.lower() == "file" or name.startswith("**") or name.startswith("---"):
|
||||
continue
|
||||
summary_rows[name] = (
|
||||
int(sm.group("p")), int(sm.group("w")), int(sm.group("i")),
|
||||
int(sm.group("n")), int(sm.group("t")),
|
||||
)
|
||||
|
||||
ok = True
|
||||
print(f"Audit: {AUDIT}")
|
||||
print(f"Sections parsed: {len(sections)} Summary rows parsed: {len(summary_rows)}")
|
||||
print("-" * 90)
|
||||
hdr = f"{'File':44} {'Rows(P W I N)':16} {'FileVerdict':16} {'Summary(P W I N T)':20} OK"
|
||||
print(hdr)
|
||||
print("-" * 90)
|
||||
|
||||
section_totals = [0, 0, 0, 0, 0]
|
||||
for name, data in sections.items():
|
||||
rows = data.get("rows", (0, 0, 0, 0))
|
||||
fv = data.get("file_verdict", (0, 0, 0, 0))
|
||||
sm_row = summary_rows.get(name)
|
||||
|
||||
row_str = "{} {} {} {}".format(*rows)
|
||||
fv_str = "{} {} {} {}".format(*fv) if fv != (0, 0, 0, 0) else "(none)"
|
||||
sm_str = "{} {} {} {} {}".format(*sm_row) if sm_row else "(missing)"
|
||||
|
||||
row_matches_sm = sm_row is not None and rows == sm_row[:4]
|
||||
fv_matches_rows = fv == rows
|
||||
this_ok = row_matches_sm and fv_matches_rows
|
||||
mark = "\u2713" if this_ok else "\u2717"
|
||||
if not this_ok:
|
||||
ok = False
|
||||
print(f"{name:44} {row_str:16} {fv_str:16} {sm_str:20} {mark}")
|
||||
if sm_row:
|
||||
for idx in range(5):
|
||||
section_totals[idx] += sm_row[idx]
|
||||
|
||||
unseen_summary = set(summary_rows.keys()) - set(sections.keys())
|
||||
for name in unseen_summary:
|
||||
sm_row = summary_rows[name]
|
||||
for idx in range(5):
|
||||
section_totals[idx] += sm_row[idx]
|
||||
print(f"{name:44} {'(no section)':16} {'(n/a)':16} {'{} {} {} {} {}'.format(*sm_row):20} -")
|
||||
|
||||
print("-" * 90)
|
||||
if total_row:
|
||||
reported_total = total_row
|
||||
computed = tuple(section_totals)
|
||||
total_ok = reported_total == computed
|
||||
mark = "\u2713" if total_ok else "\u2717"
|
||||
print(f"{'TOTAL (from Summary)':44} reported={reported_total} computed={computed} {mark}")
|
||||
if not total_ok:
|
||||
ok = False
|
||||
else:
|
||||
print("TOTAL row NOT FOUND")
|
||||
ok = False
|
||||
|
||||
print()
|
||||
print("PASS" if ok else "FAIL")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user