security(use_ros): blocklist for safety-critical publish topics#1087
security(use_ros): blocklist for safety-critical publish topics#1087CurrentlyAWey wants to merge 8 commits into
Conversation
Add a default blocklist preventing LLM-initiated publish to cmd_vel, joint_command, joint_trajectory, emergency_stop, and navigation topics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cagataycali
left a comment
There was a problem hiding this comment.
Reviewer note (contributor, non-gating). Reasonable defense-in-depth: the use_ros tool's publish path only had a character-level _NAME_RE while the DDS bridge got _require_secure_command_surface, so a topic-semantic blocklist for safety-critical command topics closes a real asymmetry. The namespace-stripping match (/my_robot/cmd_vel -> /cmd_vel) is the right instinct.
Before merge-ready, per AGENTS.md ("Pin every reviewed fix with a regression test"): the 44-line addition has no test file. Please commit the plan as tests: publish to /cmd_vel blocked; /my_robot/cmd_vel blocked (namespace stripping); /my_custom_topic passes; subscribe/read to /cmd_vel still works (the read path must be unaffected — this is the most important regression to pin, since over-blocking would break legitimate telemetry); and STRANDS_ROS2_PUBLISH_BLOCKLIST="" disables. One edge worth a test: namespace stripping should not let /cmd_vel_evil or /foo/notcmd_vel slip through as a substring — assert exact final-segment match, not a contains-check. CI still pending; early, non-blocking feedback.
Pin blocklist contract: safety-critical topics blocked, namespace-prefixed variants caught, substring false positives rejected, env override works. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks for the review — tests added in the latest push.
The read/subscribe path is unaffected — the blocklist check is only inside |
|
Hey @CurrentlyAWey — first, genuinely thank you for this. Both PRs surface real risks (Hydra "last value wins" + agent-driven publish to That said, we'd like to shift the enforcement model before merging, and we'd love your help doing it. Below is the reasoning — please push back if you disagree. The concern with hard-blockBoth PRs currently
What we'd prefer: Human-in-the-loop gate
The change is small: instead of Concrete asksIf you're up for it, could you refactor along these lines?
If reshaping the PR isn't practical, we can also land your current version behind a feature flag (default off) and iterate — happy either way. Again, thank you for the thorough writeup and the PoCs. The security surface you identified is real; we just want the mitigation to keep the tools usable for the 99% legitimate case while still catching the injection path. cc @cagataycali |
Per review feedback: hard-block kills legitimate agent use cases. Replace with a human-in-the-loop (HIL) gate using tool_context.interrupt(): - Interactive/dev: prompt operator via Strands interrupt mechanism - Headless: per-topic allowlist via STRANDS_ROS2_PUBLISH_ALLOW - Fully-trusted: BYPASS_TOOL_CONSENT=true passes with WARNING log - No tool_context: fail-closed with error dict Blocklist data unchanged. Env var changes from global blocklist override (STRANDS_ROS2_PUBLISH_BLOCKLIST) to per-topic allowlist (STRANDS_ROS2_PUBLISH_ALLOW). Tests assert the gate is called with correct args, not a raise. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Refactored in 6f17383: replaced the hard error return with a human-in-the-loop gate using What changed:
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…consent env The HIL publish gate short-circuits (allows without prompting) when BYPASS_TOOL_CONSENT=true or STRANDS_ROS2_PUBLISH_ALLOW covers the topic. The five TestGatePublish cases that assert the no-context / non-covered- allowlist / interrupt-approved / declined / runtime-error paths never neutralized these env vars, so any shell that exports BYPASS_TOOL_CONSENT=true (common in agent/automation contexts) makes the gate allow silently and the tests fail with 'assert None is not None' / 'interrupt called 0 times'. Add a class-scoped autouse fixture that clears both vars per test via monkeypatch.delenv. Cases that exercise the bypass/allowlist paths still opt in explicitly with monkeypatch.setenv, so the contract is unchanged and the suite is deterministic regardless of the ambient environment. Mirrors the same fix applied to the lerobot_train extra_flags gate tests (strands-labs#1246). Co-authored-by: CurrentlyAWey <CurrentlyAWey@users.noreply.github.com>
Bring in the pyarrow py.typed mypy override (strands-labs#1240) and other merged changes so the lint gate passes on the current base.
…pics The publish HIL gate blocks safety-critical command topics (cmd_vel and its namespaced forms) unless approved/allowlisted. Two pre-existing tests in tests/tools/test_use_ros.py used /turtle1/cmd_vel and /cmd_vel as incidental publish targets and now hit the gate in a clean (no BYPASS_TOOL_CONSENT) environment: - test_publish_dispatches_with_real_dict verifies the fields dict reaches the rclpy _publish helper with types intact; its topic is incidental. Move it to a non-command topic so it exercises the dispatch/marshaling path it is meant to cover rather than the gate. - test_publish_requires_topic_and_type asserts a publish without a type errors; move it off /cmd_vel so it reaches the missing-arg check instead of short-circuiting on the gate. The gate's own behavior is fully covered by tests/test_use_ros_publish_blocklist.py. No production behavior change.
cagataycali
left a comment
There was a problem hiding this comment.
Verified and taking this over the line. The publish HIL gate mirrors the merged extra_flags gate (#1085): safety-critical command topics (cmd_vel and its namespaced forms, joint_command/joint_trajectory, e-stop, motor enable/disable, navigate_to_pose/follow_path) are gated behind tool_context.interrupt(), with a per-topic STRANDS_ROS2_PUBLISH_ALLOW allowlist, BYPASS_TOOL_CONSENT warn-and-proceed, and fail-closed when no tool_context is available. The read/echo/subscribe path is untouched. Namespace stripping via exact final-segment match (rsplit) correctly catches /my_robot/cmd_vel while /cmd_vel_evil, /not_cmd_vel and /joint_trajectory_status pass.
Brought the tests to green: (1) made TestGatePublish hermetic to the ambient BYPASS_TOOL_CONSENT/STRANDS_ROS2_PUBLISH_ALLOW env via a class-scoped autouse fixture, matching the fix applied to the extra_flags gate tests (#1246) so the suite is deterministic regardless of the shell; (2) merged main to pick up the pyarrow py.typed mypy override (#1240); (3) moved two incidental publish tests in tests/tools/test_use_ros.py off the now-gated cmd_vel topics so they exercise the marshaling / missing-arg paths they are meant to cover rather than the gate. Full lint + test suite green. Thanks for the thorough writeup and PoCs.
|
All checks are green and this is approved. Summary of what was needed to bring it over the line:
Full lint + suite green. The repository ruleset requires an approval from someone other than the last pusher, so this needs a merge from a second maintainer (or a re-push from @CurrentlyAWey). @yinsong1986 could you do the honors? |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cagataycali
left a comment
There was a problem hiding this comment.
All checks green (call-test-lint, CodeQL, breaking-change, LLM-input-safety, dependency-review). The HIL publish gate is sound: blocklist of safety-critical command surfaces (cmd_vel/joint/e-stop/nav), namespace-stripped matching, STRANDS_ROS2_PUBLISH_ALLOW + BYPASS_TOOL_CONSENT escape hatches, fail-closed with no tool_context, and a flat approve/deny interrupt sentinel that never echoes the operator reply. Mirrors the merged lerobot_train HIL gate. Approving post-rebase.
|
All 8 checks are green on the latest head (call-test-lint, CodeQL, Detect Breaking Changes, LLM Input Safety, dependency-review, Analyze python/actions). The publish gate has been reviewed and approved: a blocklist of safety-critical command surfaces ( This is merge-ready but the repository ruleset requires the approving review to come from someone other than the last pusher, and I am recorded as the last pusher (from the earlier test-fix commits). Routing to @yinsong1986 for the final approve + merge. |
Summary
Add a default blocklist preventing LLM-initiated publish to safety-critical ROS 2 topics via
use_ros, gated by a human-in-the-loop (HIL) interrupt./cmd_vel,/cmd_vel_unstamped,/joint_command,/joint_trajectory,/joint_trajectory_controller/joint_trajectory,/emergency_stop,/e_stop,/motor_enable,/enable_motor,/disable_motor,/navigate_to_pose,/follow_path/my_robot/cmd_velmatches/cmd_vel)Three operating modes
tool_context.interrupt()prompts operatorSTRANDS_ROS2_PUBLISH_ALLOW=/cmd_vel,/follow_pathBYPASS_TOOL_CONSENT=trueWhen no
tool_contextis available (outside an agent loop), the gate fails closed with an actionable error suggesting the env vars.Design rationale
docs/security.md§1 documents tool scoping as the primary mitigation — agents that don't needuse_rosnever receive it. But agents that legitimately needuse_rosfor subscribe/observe can be prompt-injected into publishing velocity commands. This adds defense-in-depth: even withuse_rosloaded, safety-critical topics require explicit operator consent.Test plan
STRANDS_ROS2_PUBLISH_ALLOWper-topic allowlist skips the gateBYPASS_TOOL_CONSENT=trueallows with WARNINGtool_context.interrupt()called with correct reason dict🤖 Generated with Claude Code