Skip to content

fix: sanitize untyped-error messages before the buyer-facing wire/webhook #1587

Description

@ChrisHuie

Follow-up from #1547 (re-review). Untyped exceptions surface raw str(e) to the buyer, unchanged by normalization. Pre-existing — the synchronous failed-Task artifact already carried it; #1547 only extended it to the top-level webhook body — so this is not a #1547 gate, but the durable fix belongs at the shared sink.

Detail

normalize_to_adcp_error (src/core/exceptions.py:988) maps an untyped Exception to AdCPError(str(exc)) — it rewrites the wire code (INTERNAL_ERRORSERVICE_UNAVAILABLE) but preserves the message verbatim. build_two_layer_error_envelope passes exc.message into both envelope layers (:951-954), and the A2A path also forwards it to the webhook body (_fail_task_with_webhook(task, str(e)), src/a2a_server/adcp_a2a_server.py:984result_data["error"]). For a SQLAlchemy/OS error, str(e) can carry SQL fragments, table names, or filesystem paths.

AdCP transport-errors.mdx §Security Considerations directs that buyer-facing error messages not include DB text / stack traces / internal paths.

Contract

Sanitize at the sink: for an untyped Exception normalized to base AdCPError(INTERNAL_ERROR), emit a generic buyer-facing message and log the raw str(e) server-side only. This closes the failed-Task artifact, the webhook body, and the per-skill result paths in one place, across all transports (MCP/REST/A2A share normalize_to_adcp_error). Verify against the wire-envelope tests + BDD error scenarios.

Interacts with #1578 (the protocol-webhook channel posts to a client-supplied URL without SSRF validation), so an unsanitized message can reach an attacker-chosen endpoint.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions