ComfyUI/app/prompt_metadata.py
dante01yoon db9c8cc2fd
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
fix(server): scope prompt metadata to active prompt_id and validate at submission
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).
2026-05-20 19:01:38 +09:00

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