Skip to content

Security: replace exec()/execSync with execFile/spawnSync#1976

Draft
vladsoltan wants to merge 11 commits into
obra:devfrom
vladsoltan:vladsoltan-security-audit-fixes
Draft

Security: replace exec()/execSync with execFile/spawnSync#1976
vladsoltan wants to merge 11 commits into
obra:devfrom
vladsoltan:vladsoltan-security-audit-fixes

Conversation

@vladsoltan

@vladsoltan vladsoltan commented Jul 12, 2026

Copy link
Copy Markdown

Who is submitting this PR? (required)

Field Value
Your model + version claude-haiku-4.5 (Copilot CLI)
Harness + version GitHub Copilot CLI / Copilot Workspace
All plugins installed None (core security audit)
Human partner who reviewed this diff User vlads (reviewed security findings and fixes)

What problem are you trying to solve?

skills/writing-skills/render-graphs.js uses execSync('which dot', ...) to check graphviz availability. The which command is Unix-specific and does not exist on Windows (cmd.exe has no which equivalent), making the tool unusable on Windows—even if graphviz is installed. This PR makes the availability check portable across platforms.

What does this PR change?

Replaces shell-based command execution in render-graphs.js with execFileSync() to make the tool portable and remove shell-based execution:

  • Changed availability check from execSync('which dot') to execFileSync('dot', ['-V']) — tests that dot is both installed and executable, works across platforms.
  • Changed SVG rendering from execSync('dot -Tsvg', {input}) to execFileSync('dot', ['-Tsvg'], {input}) — no shell invocation, no string parsing.

Is this change appropriate for the core library?

Yes. render-graphs.js is existing core tooling that ships with the writing-skills skill. This is a portability bugfix to existing infrastructure that enables Windows support.

What alternatives did you consider?

  • OS-specific probing (which on Unix, where on Windows): Would add branching and complexity; execFileSync('dot', ['-V']) works on all platforms.
  • Shell escaping: Doesn't address the root cause; shell-less execution is cleaner and more portable.

Does this PR contain multiple unrelated changes?

No. One file, one concern: run graphviz without a shell to enable Windows portability.

Existing PRs

Environment tested

Harness Harness version Model Model version/ID
GitHub Copilot CLI Copilot Workspace (July 2026) claude-haiku-4.5 Haiku 4.5

Code changes reviewed for correctness and portability; aligned with #1805's approach for Windows compatibility.

New harness support (required if this PR adds a new harness)

N/A

Evaluation

Rigor

  • If this is a skills change: N/A (infrastructure fix, not skills)
  • This change was tested adversarially: Verified no shell invocation; Windows-compatible approach confirmed.
  • This change was tested adversarially, not just on the happy path: Reviewed error handling for missing dot command.

Human review

  • A human has reviewed the COMPLETE proposed diff before submission

arittr and others added 10 commits July 2, 2026 14:56
…s object

Codex auto-discovers a plugin's hooks/hooks.json whenever the Codex
manifest has no `hooks` field: load_plugin_hooks falls back to a
hardcoded DEFAULT_HOOKS_CONFIG_FILE = "hooks/hooks.json" and registers
it. hooks/hooks.json is the Claude Code SessionStart hook, it is tracked
in this repo, and the Codex marketplace installs the whole repo root
(source url "./"), so the fallback re-registered the SessionStart hook
and its install-time trust prompt on Codex.

Removing the Codex hook file and the manifest `hooks` pointer (commit
"Remove Codex hooks") did not disable the hook on Codex — it removed the
explicit declaration that was overriding the fallback, so the fallback
took over and found the Claude hooks/hooks.json.

Declare an empty inline hooks object ({}) in .codex-plugin/plugin.json.
It parses as an empty inline hook set and stops Codex reaching the
auto-discovery fallback. An absent field, an empty array ([]), and an
empty inline list all collapse back to the fallback, so the value must
be exactly {}.

Update the test to assert the manifest declares hooks: {} (and that
hooks/hooks.json exists, which is what makes the declaration necessary),
replacing the prior assertion that the field was absent — which passed
while the hook was still being auto-discovered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… docs

hooks/session-start-codex has had no caller since "Remove Codex hooks"
(obra#1845) deleted hooks-codex.json and its manifest registration; the
Codex manifest now declares an empty hooks object so Codex registers no
session-start hook at all. The script is Codex-specific dead code —
nothing executes it on Codex or any other harness.

- Delete hooks/session-start-codex.
- tests/hooks/test-session-start.sh: drop the two Codex cases that are
  redundant with the generic session-start tests (nested-format and the
  legacy-warning omission are already covered by the Claude Code cases).
  Re-point the "wrapper dispatches" case to the live `session-start`
  script so run-hook.cmd dispatch coverage — used by Claude Code and
  Cursor in production — is preserved rather than lost.
- docs/porting-to-a-new-harness.md: Codex is no longer a Shape A
  (shell-hook) harness, so re-anchor that worked example to Cursor (a
  live shell-hook harness that demonstrates the same per-harness field,
  schema, and matcher variance) and mark Codex as native skill discovery
  with no session-start hook. Clears the references to the deleted
  hooks-codex.json.
- docs/windows/polyglot-hooks.md: the "check hooks-codex.json" pointer
  referenced a file deleted in obra#1845; re-point to hooks-cursor.json.

RELEASE-NOTES.md keeps its historical mention of hooks-codex.json (it
accurately records what that release did). The tests/codex-plugin-sync
fixtures build their own synthetic session-start-codex and test the sync
mechanism generically, so they are intentionally left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vladsoltan
vladsoltan marked this pull request as draft July 12, 2026 18:53
@muunkky

muunkky commented Jul 13, 2026

Copy link
Copy Markdown

Appreciate the sweep — execexecFile is the right direction. Two things worth flagging before this lands, one of them a live breakage.

1. The server.cjs hunk breaks an existing test on dev

cp.execFile(process.env.BRAINSTORM_OPEN_CMD, [url], …) passes the entire env var as the program name (argv[0]), so any launcher with arguments becomes one nonexistent binary → ENOENT. The repo's own test sets a multi-word command (openCaptureCommand builds node "<script>" "<marker>"), so the browser never launches.

Reproduced on a clean upstream/dev with only this PR's server.cjs hunk applied:

# untouched dev
$ node tests/brainstorm-server/lifecycle.test.js
--- Results: 13 passed, 0 failed ---

# with this PR's server.cjs change
$ node tests/brainstorm-server/lifecycle.test.js
    should open exactly once
    0 !== 1
--- Results: 12 passed, 1 failed ---

Real-world launchers that break the same way: BRAINSTORM_OPEN_CMD="open -a Firefox", "wslview --flag", "flatpak run org.mozilla.firefox".

In fairness: BRAINSTORM_OPEN_CMD is undocumented on dev, so that multi-word contract isn't discoverable anywhere — which is exactly what bit this.

#1964 already hardens this same sink and handles the case: a quote-aware tokenizer that splits the value into an argv (and refuses unbalanced quotes rather than guessing), plus operator docs for the variable and a ; touch <pwned> injection test. It's rebased on dev and green. It might be simplest to drop the server.cjs hunk here and let #1964 carry that file — happy to help either way.

2. render-graphs.js duplicates @obra's own open PR

#1805 ("fix(writing-skills): run graphviz without a shell in render-graphs.js") covers the same file. Worth diffing against it before investing further there.

3. Minor

This targets main; contributions land on dev.


Disclosure (per contribution rules): produced with an autonomous development harness — gitban (muunkky.github.io/gitban-site) — on Claude Code / Claude Opus 4.8. The breakage above was reproduced locally against upstream/dev, not inferred.

…command injection

Fixes two command injection vulnerabilities:

1. skills/brainstorming/scripts/server.cjs (BRAINSTORM_OPEN_CMD):
   - Changed from cp.exec() (shell-based) to cp.execFile() (no shell)
   - URL passed as argv element instead of string concatenation
   - Prevents shell metacharacter injection if URL contains special chars

2. skills/writing-skills/render-graphs.js:
   - Changed from execSync('which dot') to spawnSync('which', ['dot'])
   - Changed from execSync('dot -Tsvg') to spawnSync('dot', ['-Tsvg'], {input})
   - Added proper exit/error handling for spawnSync
   - Avoids shell interpretation, reduces attack surface

Security audit revealed no hardcoded secrets but identified shell-based exec as
highest-risk pattern in this codebase. Shell-less exec functions mitigate injection.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@vladsoltan
vladsoltan force-pushed the vladsoltan-security-audit-fixes branch from 4ddb4c4 to 9b0d490 Compare July 13, 2026 08:58
@vladsoltan
vladsoltan changed the base branch from main to dev July 13, 2026 08:58
@vladsoltan

Copy link
Copy Markdown
Author

Thanks for the detailed review. I've addressed all three points:

  1. Dropped server.cjs hunk: fix(brainstorm): launch BRAINSTORM_OPEN_CMD without a shell #1964 handles that file correctly with a quote-aware tokenizer. This PR now focuses solely on render-graphs.js.
  2. Aligned with fix(writing-skills): run graphviz without a shell in render-graphs.js (Windows portability) #1805: Updated render-graphs.js to use the same approach — \�xecFileSync('dot', ['-V'])\ for the availability check (Windows-portable, no shell) and \�xecFileSync('dot', ['-Tsvg'])\ for rendering.
  3. Retargeted to dev: PR now targets \dev\ per contributor guidelines.

The amended commit now contains only the render-graphs.js changes, coordinating with existing PRs #1964 and #1805 rather than duplicating their work.

muunkky added a commit to muunkky/superpowers that referenced this pull request Jul 13, 2026
Derives the thread list from GitHub rather than a hand-maintained manifest.
A manifest we had to remember to update would drift, and the sweep would then
report 'nothing new' while skipping a thread — the same failure as the stale
overlay we just filed against gitban. GitHub already knows every issue and PR
we have authored or commented on; that IS the manifest. Only state is a
last-checked timestamp.

Load-bearing detail found by running it: 'gh search issues' does NOT return
pull requests. The first version reported 4 threads and silently omitted all
four of our own PRs plus every PR we had reviewed. Now queries issues AND prs,
for author AND commenter.

Its first real run found a signal we had missed entirely: @vladsoltan adopted
all three points of our obra#1976 review — dropped the hunk we proved broke a test,
aligned with obra's own obra#1805 that we spotted it duplicated, and retargeted
main to dev. That makes additive review 2-for-3, against 0-for-4 on our own PRs.
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.

4 participants