Skip to content

fix(raw): forward piped stdin to wp and refuse "-" without input - #8

Merged
itsjeremyjohnson merged 3 commits into
mainfrom
fix/raw-stdin
Oct 12, 2026
Merged

itsjeremyjohnson merged 3 commits into
mainfrom
fix/raw-stdin

Conversation

@itsjeremyjohnson

@itsjeremyjohnson itsjeremyjohnson commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #7. Merge #7 first, then this PR retargets to main.

Incident

On 2026-10-06, an operator ran wpgo raw -- post update <id> - on a production WordPress site, piping the new post body into wpgo. wpgo did not forward stdin: RawCmd.Run called RunContext.ExecWP, which reaches SSHClient.execInternal with a nil stdin. The remote wp read EOF for -, printed Success, and saved an empty post body. The operator caught the empty posts, unpublished them and restored the content.

Change

  • raw forwards local stdin to the remote wp only when an argument is -, the file argument wp reads from stdin. Other commands get no stdin, so unrelated script input never reaches the server. It uses a new Adapter.ExecWithStdin (standard and WP Engine; WP Engine keeps its 10-minute session limit) and RunContext.ExecWPWithStdin, both backed by the existing SSHClient.ExecWithStdin.
  • For a - read, raw refuses before any exec if stdin is a terminal, /dev/null or empty (0 bytes). The empty check peeks one byte and then streams the rest, so large inputs are not buffered.
  • - detection uses the same parse layers as the fix(backup): write database dumps outside the web root #7 guard (checkRawArgs). On WP Engine, whose gateway parses each word again, an argument such as "'-'" that becomes - only after the second parse counts as a stdin read.
  • raw never sends stdin to wp db (or sql). It refuses - on every db subcommand except export and dump, where - means stdout, and refuses db query with no SQL argument or --execute. SQL from stdin would skip the fix(backup): write database dumps outside the web root #7 SQL checks, so printf "SELECT 1 INTO OUTFILE '/tmp/x';" | wpgo raw -- db query is refused like the same SQL in argv.
  • --dry-run prints the command without reading or peeking at stdin, even for a - with no input.
  • x/crypto/ssh closes the forwarded stdin when the remote command exits, so a caller that passes an inherited pipe that never closes does not hang commands that ignore stdin.
  • The README and raw --help describe the new behaviour.

Testing

internal/cmd/raw_stdin_test.go runs RawCmd against an in-process SSH server (no live SSH). The WP Engine cases use the wpengine adapter, so the second parse is modelled by the same shellWords layer the guard uses.

  • TestRawForwardsPipedStdin: on standard and WP Engine, piped stdin reaches the remote wp post update 7 - and '-' byte for byte, and wp option get home receives empty stdin.
  • TestRawRefusesStdinReads: refused with no exec on the server: /dev/null, an empty pipe and nil stdin with -; "'-'" with an empty pipe on WP Engine; piped INTO OUTFILE SQL to db query, db query -, sql import -, and db 'import -' on WP Engine. Still run: db export - with /dev/null, and a command with no - and an empty pipe.
  • TestRawDryRunLeavesStdin: dry-run with - and piped input, or with no stdin, returns without error, sends nothing, and leaves the piped bytes unread.
  • Against the round-1 raw.go (43014b2), every new case fails: the WP Engine "'-'" and four SQL-pipe cases reach the server, the no-dash case forwards stdin, and dry-run refuses or consumes stdin.
  • mise exec go@1.25.1 -- make ci (fmt-check, golangci-lint, go test ./...) passes: 0 lint issues, all packages ok.

Not tested: a real TTY (the tests use /dev/null, which takes the same character-device path), the real WP Engine gateway, and a live site.

🤖 Generated with Claude Code


Note

Medium Risk
Changes raw remote execution semantics for stdin and - handling on production WordPress sites; mitigated by pre-connect validation and blocking unchecked SQL via stdin.

Overview
Fixes a production incident where wpgo raw -- post update <id> - did not forward piped stdin, so remote wp read EOF and saved empty post content while still reporting success.

raw stdin forwarding: When wp-cli has a - file argument, raw now streams local piped/redirected stdin to the remote wp via ExecWPStdin; commands without - still send no stdin. It refuses - before SSH when stdin is a TTY, empty, or /dev/null, and --dry-run does not peek at or consume stdin.

Safety around wp db: Stdin is never forwarded for database subcommands except db export/dump (where - means stdout). db query with no SQL/--execute and any other db use of - are blocked so SQL cannot bypass existing raw SQL checks.

WP Engine: - detection uses the same double-parse layers as the existing raw guards (e.g. "'-'").

README and raw --help document the behavior. New integration tests use an in-process SSH server to assert forwarding, refusals, and dry-run semantics on standard and WP Engine host types.

Reviewed by Cursor Bugbot for commit 1832e5b. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor

cursor Bot commented Oct 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: ff59e7cd-8ba3-41ce-bbfe-2846b66a74b4)

@cursor

cursor Bot commented Oct 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: a55dcd17-3e3d-4b54-90ee-a2cecd5b2da2)

Copy link
Copy Markdown
Owner Author

PR #7 is now merged into main at 4ec15753156dd8914e9cc1fcc4bfae66c2c7d364. This PR still targets fix/backup-outside-webroot. Its workflow only covers pull requests targeting main, so head 3bd9f66f5548e95d6552dddd3999a7d3800be28a has security/router results but no native build/test check.

For the existing owner: please confirm who is continuing this lane, retarget it to main, and record a passing current integration fmt-check/lint/test run plus independent review before merge. The merged main still calls ExecWP without forwarding stdin, so the piped post update ... - risk remains until this fix is integrated and the intended binary is installed and verified.

Installed-tool verification and verification of restored production content remain separate from the source merge.

itsjeremyjohnson and others added 3 commits October 11, 2026 23:31
`wpgo raw -- post update <id> -` ran the remote wp with no stdin. wp read
EOF for "-", reported Success and saved an empty post body.

raw now forwards local stdin to wp when it is piped or redirected, through
a new Adapter.ExecWithStdin backed by SSHClient.ExecWithStdin. When a "-"
argument makes wp read stdin and stdin is a terminal, /dev/null or empty,
raw refuses before connecting. db export and db dump keep "-" as stdout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Piped SQL reached `wpgo raw -- db query` unchecked, so INTO OUTFILE got past
the guard that refuses it in arguments. raw now refuses "-" on every db
subcommand except export and dump, and db query with no SQL or --execute.

On WP Engine an argument like "'-'" becomes "-" only after the gateway's
second parse, so raw let it through with empty stdin. Dash detection now
uses the same parse layers as checkRawArgs.

raw forwards stdin only when wp reads it for "-", and dry-run prints the
command without reading stdin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main (#9) now refuses `wp db import` in checkRawArgs before the stdin
check runs, so these cases are still refused with nothing sent, under
that message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@itsjeremyjohnson
itsjeremyjohnson changed the base branch from fix/backup-outside-webroot to main October 12, 2026 04:38
@cursor

cursor Bot commented Oct 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: 182d8dbf-de5d-45a0-a8db-c459a004b6a7)

@itsjeremyjohnson

Copy link
Copy Markdown
Owner Author

Sweep 2026-10-12: rebased onto main (9ac16e3) and retargeted. Main (#9) already had identical stdin plumbing named ExecStdin/ExecWPStdin, so raw now uses those names and the duplicate ExecWithStdin/ExecWPWithStdin methods are gone. In 1832e5b, two test cases now expect main's earlier db import refusal: they are still refused and nothing is sent, but the message differs.

Evidence:

  • mise exec go@1.25.1 -- make ci: 0 lint issues, all packages ok. CI build passed on 1832e5b.
  • The new stdin tests fail against round-1 raw.go (d85160a) in 8 subtests (no_dash forwarding, gateway '-', SQL pipes, dry-run) and pass on the head.
  • Independent Sol review (gpt-6.1-sol, read-only) of 1832e5b vs origin/main: 0 confirmed, 0 plausible findings. Its sandbox blocked the loopback SSH listener, so the test runs above, done outside that sandbox, supply the behavior evidence.

Not verified: the installed wpgo binary, the real WP Engine gateway, and a live site.

@itsjeremyjohnson
itsjeremyjohnson merged commit 43b334f into main Oct 12, 2026
4 checks passed
@itsjeremyjohnson
itsjeremyjohnson deleted the fix/raw-stdin branch October 12, 2026 04:50
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.

1 participant