Skip to content

fix: stop scenario metadata from reaching shells and host commands - #155

Open
adamsrnmsu wants to merge 1 commit into
sandialabs:mainfrom
adamsrnmsu:fix-host-command-execution
Open

adamsrnmsu wants to merge 1 commit into
sandialabs:mainfrom
adamsrnmsu:fix-host-command-execution

Conversation

@adamsrnmsu

@adamsrnmsu adamsrnmsu commented Oct 1, 2026 •

Copy link
Copy Markdown

Description

Keep scenario metadata out of shells and host-side commands. Several components splice metadata into shell strings or into ovs-vsctl and minimega commands, and some run scenario-supplied scripts directly on the phenix host.

  • No shell. utils.run_command() runs an argument vector; a string is shlex-split. collector, pcap, mm, tcpdump, trim_pcap() and pcap_capinfos() pass lists, and tshark output is redirected from Python instead of through bash -c '... > file'. The remaining string callers (tshark -Y "...", mergecap, editcap, phenix version) use no shell features and split the same way.
  • Single-token validation for values that reach ovs-vsctl or minimega: the mirror app's bridge, VLAN alias and HIL interfaces (Go), the mgmt_tap bridge, erspan bridges, interfaces, IPs, session key and excluded VLANs, tcpdump interfaces, mm capture filenames and filters, and the experiment and compute names in mm_compute_cmd().
  • Host-side commands need an explicit opt-in. ⚠️ This changes a default. art and cc validators and the pipe via program run on the phenix host, not in a VM, so they now require PHENIX_SCORCH_HOST_VALIDATORS=1, read once in common/settings.py. Without it, validators are skipped with a warning and a via fails the component. Documented in the art, cc and pipe READMEs and in the AGENTS.md environment-variable table.
  • kafka. The PID file moves out of world-writable /tmp, and cleanup only kills the PID if /proc/<pid>/cmdline still shows a kafka listener.
  • ssh. A new optional known_hosts metadata field. When set, unknown host keys are rejected; when absent, the component keeps trusting the key but logs a warning.

Related Issues/PRs

One of four independent PRs: #152, #153, #154. Each is a single commit on main and they can merge in any order. #154 also adds a ### Security section to CHANGELOG.md, so whichever of the two merges second needs a small rebase. No other files conflict.

Type of Change

  • Bugfix (fix)

Checklist

  • This PR conforms to the process detailed in the Contributing Guide.
  • I have included no proprietary/sensitive information in my code or the PR.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation.
  • I have tested my code (describe below).

Testing

  • Python: make check and make test in src/python on Python 3.12: 708 passed on this branch alone, and 743 with all four PRs merged together. New tests in common/tests/test_command_helpers.py cover mm_compute_cmd token validation and confirm that run_command("echo 'a; touch /tmp/x' *") reaches echo without shell expansion.
  • Go: golangci-lint run (v2.11.3, the pinned version) reports 0 issues, and go test -race ./... passes. The new util_test.go covers validateOVSToken and the metadata extraction paths.

Additional Notes

This series started as a single hardening patch. Where it departs from that patch:

  • kafka PID location. The patch moved the PID file under base_dir, which is per loop and count. configure skips when the PID file already exists, so a per-loop path would let every loop start another listener. It now lives in the experiment's files_dir/scorch/: still loop-independent, but no longer in /tmp.
  • Go formatting. The patch's util.go failed the repo's golangci-lint (golines, noinlineerr). It is reformatted, with the error checks moved out of the if statements.

An open question for reviewers: with the env var unset, art and cc skip a configured validator with only a warning, so a result that would have failed validation now reads as passed. Failing the component instead, as pipe does for via, would be stricter and closer to the "don't silently recover" rule in AGENTS.md. This PR keeps the warning; happy to switch.

🤖 Generated with Claude Code

https://claude.ai/code/session_016KAfcDUSerQCCWwBM9BxAo

@GhostofGoes

Copy link
Copy Markdown
Contributor

@adamsrnmsu Please separate your changes into 3 PRs. You opened 3 PRs, with the same changes in all 3, lol. One commit per PR my guy.

@adamsrnmsu
adamsrnmsu force-pushed the fix-host-command-execution branch from 98ef201 to 78726f9 Compare October 2, 2026 01:51
@adamsrnmsu

Copy link
Copy Markdown
Author

@GhostofGoes Split as requested. #153, #154 and #155 are now each one commit on top of main with no dependencies between them, so they can be reviewed and merged in any order (#152 was already standalone). The only overlap is CHANGELOG.md: #154 and #155 both add a ### Security section, so I'll rebase whichever merges second.

@GhostofGoes

Copy link
Copy Markdown
Contributor

Glad someone is doing a security pass to clean up the pile of years of exec sins. Please ensure you test this PR.

@adamsrnmsu
adamsrnmsu force-pushed the fix-host-command-execution branch from 78726f9 to 7398d5b Compare October 4, 2026 23:53
Several apps and SCORCH components splice scenario metadata into shell
strings, minimega and ovs-vsctl commands, or run scenario-supplied scripts
directly on the phenix host.

- utils.run_command() executes an argument vector instead of a shell string;
  a str is shlex-split. collector, pcap, mm, tcpdump, trim_pcap() and
  pcap_capinfos() pass argument lists, and tshark output is redirected from
  Python rather than through 'bash -c ... >'.
- Values that end up in ovs-vsctl or minimega commands are validated as
  single tokens: the mirror app's bridge, VLAN alias and HIL interfaces (Go),
  the mgmt_tap bridge, erspan bridges, interfaces, IPs, session key and
  excluded VLANs, tcpdump interfaces, mm capture filenames and filters, and
  the experiment and compute names in mm_compute_cmd().
- Validators for the art and cc components and the pipe 'via' program run on
  the phenix host, not in a VM, so they now need an explicit opt-in:
  PHENIX_SCORCH_HOST_VALIDATORS=1, read once in common/settings.py. Without it validators are skipped with a
  warning and a via fails the component. This is a breaking default.
- The kafka PID file leaves world-writable /tmp for the experiment's SCORCH
  files directory (still independent of loop and count, so configure keeps
  detecting a running listener), and cleanup only kills the PID if it is
  still a kafka listener.
- The ssh component takes an optional known_hosts file and rejects unknown
  host keys when one is given; without it, it warns before trusting the key.

Documented in the affected READMEs, AGENTS.md and the changelog; Go and
Python tests cover the new validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016KAfcDUSerQCCWwBM9BxAo
@adamsrnmsu
adamsrnmsu force-pushed the fix-host-command-execution branch from 7398d5b to 4618094 Compare October 5, 2026 00:00
Comment thread CHANGELOG.md
### Security
- **Common**: `run_command()` no longer uses a shell; `mm_compute_cmd()` rejects multi-token names.
- **Mirror, mgmt_tap, SCORCH**: Metadata is validated before reaching `ovs-vsctl`, minimega or `tshark`; commands run as argument vectors, not `bash -c`.
- **SCORCH**: kafka's PID file leaves `/tmp`; ssh takes optional `known_hosts`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Leaves /tmp"... where did it go? And did it take the kids with it?

If no validator is provided then it is assumed the test succeeded if the atomic
executor exits cleanly.

> [!IMPORTANT]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be enabled by default to avoid breaking existing environments, with the ability to lock things down more in environments where the security is needed (and there's less trust)


tempfile = f"/tmp/{uuid.uuid4()!s}.sh"
with open(tempfile, "w") as tf:
tempfile = Path(f"/tmp/{uuid.uuid4()!s}.sh")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there a module in python standard library for secure temporary file creation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants