capabilities.providers scope is never enforced for http nodes at runtime — a workflow declared to touch ONLY specific providers can reach any domain via an http node with zero refusal (v1.4.0)
Reproduction steps:
- The declared-capability backstop (#a960d0, davelab — "the workflow's own
capabilities block is a commitment the composer made up front... an
effect node that resolves to a provider outside providers... refuses
HERE — regardless of which surface invoked the run") is gated to effect
nodes only:
line 966: if kind == "effect" and (_cap_providers is not None or _cap_no_irreversible):
...
if _cap_providers is not None and _ci.provider not in _cap_providers:
raise CapabilityExceeded(...)
- v1.4.0's a3b2a4 fix adds a sibling check for http nodes (see finding
above and this session's earlier verification), but it extends ONLY the
allow_irreversible half of this backstop:
line 990: if kind == "http" and _cap_no_irreversible:
... raise CapabilityExceeded(...) if the http action is irreversible
No equivalent if kind == "http" and _cap_providers is not None: ...
exists anywhere in the file — confirmed by grep, _cap_providers appears
only inside the kind == "effect" branch (line 966/972).
- Run repro_http_node_providers_scope_bypass_v140.py against a clean v1.4.0
extraction. It drives the REAL, unmodified run_workflow with a workflow
that declares capabilities.providers=["stripe"] and contains ONE http
node targeting example.com (not stripe), with a stub http_mock so no
real network call is needed (the node's own domain/method/SSRF checks
all still execute for real):
workflow declares capabilities.providers: ['stripe']
http node targets: https://example.com/collect (NOT stripe)
outcome: COMPLETED
error: None
CONFIRMED — no CapabilityExceeded, the run completed cleanly.
Expected:
A workflow that commits to touching ONLY specific providers must not be
able to reach ANY other destination through a different node kind — the
whole point of #a960d0's backstop is that the declared scope is enforced
"regardless of which surface invoked the run," and a node kind is exactly
such a surface. An operator approving a workflow scoped to "stripe only"
reads that as a hard boundary, not a boundary that only applies to
registered effect integrations.
Actual:
An http node's target is checked against nothing but its OWN, same-author,
per-node policy.allow_domains — which provides no independent scope
boundary, since the same workflow author who declared
capabilities.providers=["stripe"] also writes that node's allow_domains.
capabilities.providers is silently ignored for the entire http node kind.
Combined with finding #5 (spend cap also invisible to http nodes) and the
already-known a3b2a4 fix (which shows the platform's authors ARE aware http
nodes need capability-envelope parity with effect nodes — they fixed
allow_irreversible for this exact node kind, in this exact function, and
missed the providers half of the same backstop sitting nine lines above
their own fix), an http node is effectively exempt from the workflow's
declared provider scope entirely.
Suggested fix:
Extend the providers-scope check to http nodes the same way a3b2a4 extended
the irreversibility check — resolve the http node's destination host and
compare it against the declared scope, or (simpler and more robust) fold
BOTH sub-checks into one uniform backstop that runs for every outbound node
kind instead of two independently-maintained, kind-specific copies:
def _capability_check(kind, n, resolved_request_or_args, ws, outputs, scope):
# returns (provider_or_host, action_class) for effect/http alike
...
if _cap_providers is not None or _cap_no_irreversible:
target, action_class = _capability_check(kind, n, ..., outputs, scope)
if _cap_providers is not None and target not in _cap_providers:
raise CapabilityExceeded(...)
if _cap_no_irreversible and action_class == "external_send":
raise CapabilityExceeded(...)
A single shared check, called once per outbound node kind, would prevent
the next new node kind (or the next capabilities field) from silently
missing coverage the way providers did here.