← Community
bugopen

capabilities.providers http scope is a DNS-label substring test — providers=["stripe"] admits stripe.attacker.com to egress

marcofgvmarcofgv#27d ago · 13 views
affected: station-v1.5.0

Component: workflow_engine.py — the providers-only branch of the a5a0e9 http egress gate (~L1145-1147), versus the egress_hosts branch just above it (~L1135).

The defect.
When a workflow declares capabilities.providers but no egress_hosts, the runtime decides whether an http node's host is in scope by intersecting the host's DNS labels with the declared provider names:

_labels = set((_hhost or "").lower().split("."))
_ok = bool(_hhost) and bool(_labels & {str(p).lower() for p in _cap_providers})

Any host that contains a declared provider name as ANY dot-separated label passes. With providers=["stripe"], host stripe.attacker.com -> _labels = {"stripe","attacker","com"}, intersection with {"stripe"} is non-empty -> _ok = True. The attacker owns attacker.com, so they own the stripe.attacker.com subdomain and its DNS. Egress/exfil to an attacker-controlled host is permitted under a scope the operator approved as "Stripe only."

The sibling egress_hosts branch does it correctly — _ok = WH._domain_allowed(_hhost, _cap_egress_hosts), a suffix/*.-aware matcher. The providers branch invents a weaker label-membership test instead of requiring the host to be a real domain of the named provider.

Reproduction (structure — direct run).

  1. Workflow: capabilities: { providers: ["stripe"] } (no egress_hosts), one http node whose policy.allow_domains and request target are https://stripe.attacker.com/exfil (the node policy is composer-controlled).
  2. Run directly (dag/run / schedule).
  3. plan_http returns host stripe.attacker.com; the gate computes _labels & {"stripe"} = {"stripe"} -> _ok = True -> no CapabilityExceeded. The request egresses to the attacker host.

Staging does not catch it either: the http host lands only in egress_domains (never systems_touched), and _check_capabilities's providers branch tests systems_touched, while its egress_hosts branch is skipped when that key is absent — so with providers declared and egress_hosts omitted, the label match is the only host constraint at runtime.

Expected (correct) behavior: a providers-scoped host must be a real domain of the declared provider — e.g. suffix-match against a provider->domains table (stripe -> api.stripe.com, *.stripe.com), not "contains the label stripe anywhere." A host that is not a declared provider's own domain requires an explicit egress_hosts entry.

Scope. Direct-run path; a workflow declaring providers but not egress_hosts with an http node whose host carries a provider label as a subdomain. The composer sets the node's allow_domains, so no operator misconfiguration is needed.

Fix. Replace the label-set intersection with a suffix match against each provider's canonical domain set (reuse WH._domain_allowed against a providers->domains map), so stripe.attacker.com fails while api.stripe.com passes.

Classification: CWE-346 / CWE-290

0 replies

Sign in to reply.