mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-30 19:07:25 +08:00
Address adversarial-review findings on FE-745 metadata propagation: - send_sync previously spread active_prompt_metadata onto every dict payload, contaminating unrelated status/queue broadcasts with the running prompt's workflow_id. Change the slot to (prompt_id, metadata) and only inject when payload.prompt_id matches the active prompt_id. Same condition applied to the WS reconnect catch-up frame. - post_prompt now validates extra_data.metadata at the submission boundary: flat dict[str,str], max 16 keys, 64-char keys, 256-char values, and reserved server-side keys (prompt_id, node, output, etc.) are rejected with 400. Removes the broadcast-amplification vector where a client could submit arbitrarily large metadata and force it onto every WS frame. - Extract validate_client_metadata + caps into app/prompt_metadata.py so tests can import without pulling server.py's import-time side effects. - Expand tests-unit/server_test/test_prompt_metadata.py from 12 to 47: add TestStatusBroadcastsAreNotContaminated for prompt_id-scoping and TestValidateClientMetadata for the new submission-boundary checks (including parametrized reserved-key rejection).
45 lines
1.9 KiB
Python
45 lines
1.9 KiB
Python
"""Validation for client-supplied per-prompt metadata (extra_data.metadata)."""
|
|
|
|
from typing import Optional
|
|
|
|
|
|
MAX_METADATA_KEYS = 16
|
|
MAX_METADATA_KEY_LEN = 64
|
|
MAX_METADATA_VALUE_LEN = 256
|
|
|
|
# Server-emitted top-level fields on prompt-scoped WebSocket events.
|
|
# Client-supplied metadata may not shadow these — payload-wins-on-conflict
|
|
# only protects keys present in each individual frame, so reserve them
|
|
# at the submission boundary as defense in depth.
|
|
RESERVED_METADATA_KEYS = frozenset({
|
|
"prompt_id", "node", "display_node", "output", "nodes", "node_id",
|
|
"node_type", "executed", "exception_message", "exception_type",
|
|
"traceback", "current_inputs", "current_outputs", "timestamp",
|
|
"sid", "status", "prompt", "value", "max",
|
|
})
|
|
|
|
|
|
def validate_client_metadata(raw) -> tuple[Optional[dict], Optional[str]]:
|
|
"""Return ``(cleaned_metadata, error_message)``.
|
|
|
|
A missing field (``None``) is treated as empty metadata. Anything else
|
|
must be a flat ``dict[str, str]`` within the size caps and free of
|
|
reserved keys.
|
|
"""
|
|
if raw is None:
|
|
return {}, None
|
|
if not isinstance(raw, dict):
|
|
return None, "extra_data.metadata must be an object"
|
|
if len(raw) > MAX_METADATA_KEYS:
|
|
return None, f"extra_data.metadata exceeds {MAX_METADATA_KEYS} keys"
|
|
cleaned: dict = {}
|
|
for key, value in raw.items():
|
|
if not isinstance(key, str) or not key or len(key) > MAX_METADATA_KEY_LEN:
|
|
return None, f"metadata key must be a non-empty string up to {MAX_METADATA_KEY_LEN} chars"
|
|
if key in RESERVED_METADATA_KEYS:
|
|
return None, f"metadata key '{key}' is reserved"
|
|
if not isinstance(value, str) or len(value) > MAX_METADATA_VALUE_LEN:
|
|
return None, f"metadata value for '{key}' must be a string up to {MAX_METADATA_VALUE_LEN} chars"
|
|
cleaned[key] = value
|
|
return cleaned, None
|