--- name: remote-shell-quoting-safety description: Use when a command or file content needs to pass through multiple nested shell layers (e.g. local shell -> ssh -> a jail/container exec wrapper -> su -> the target interpreter). Avoids silent corruption of $, quotes, and special characters. --- # Remote Shell Quoting Safety ## The problem Content containing `$`, quotes, or other shell-special characters gets progressively mangled the more shell layers it passes through (local shell → `ssh` → a container/jail exec wrapper like `bastille cmd` → `su -c` → the actual target interpreter). Each layer's own quoting rules interact, and by 3+ layers deep it becomes extremely hard to reason about correctly — and failures are often **silent**: no error, just corrupted content (e.g. `$_SERVER['KEY']` silently becoming `['KEY']`, `$` and the variable name simply eaten). **Confirmed real incident**: a `wp-config.php` snippet containing `$_SERVER['HTTP_X_FORWARDED_PROTO']` passed through `ssh → bastille cmd → su -m www -c '...'` came out the other side as bare `["HTTP_X_FORWARDED_PROTO"]` — no error at the point of corruption, the resulting fatal PHP error only surfaced two steps later at a completely different command, making the actual cause much harder to trace back. ## The fix: base64, not nested quoting Don't try to escape correctly through N layers. Instead: 1. Write the target file's **complete final content** locally (a real file, not a shell variable). 2. Base64-encode it locally: `base64 -w0 localfile > /tmp/payload.b64` (or pipe directly). 3. `cat`/pipe the base64 text through the SSH/exec layers (base64 output is alphanumeric-only — nothing for any shell layer to misinterpret, regardless of how many layers it passes through). 4. Decode back to the real file **on the far side, in ONE final step**: `base64 -d payload.b64 > /real/target/path`. ```bash B64=$(base64 -w0 local_file.php) echo "$B64" | ssh user@host "cat > /tmp/upload.b64" ssh user@host "base64 -d /tmp/upload.b64 > /real/target/path && rm /tmp/upload.b64" ``` For a jail/container layer on top of SSH (e.g. `bastille cmd `), land the base64 payload on the **host** filesystem first (which the jail can usually see via a shared mount or a copy step), then decode from **inside** the jail in one command — don't try to thread the base64 string itself through the jail-exec wrapper's own quoting too; one decode step per real filesystem boundary crossed is enough. ## When to reach for this - Any file content containing `$`, backticks, mixed quotes, or PHP/shell code being pushed through 2+ nested shell contexts. - Whenever a "the file exists but does something subtly wrong" bug appears after a multi-hop deployment step, and the content involves special characters — check for quoting corruption before looking elsewhere. ## zsh footgun: never name a variable `path` (or a few other reserved names) If the remote host's shell is zsh (several boxes in this fleet default to it), assigning a plain string to a variable literally named `path` does not create a normal local variable — zsh ties the lowercase `path` array to `$PATH` (colon-joined ⇄ array elements) automatically. Overwriting it with a single string **replaces `$PATH` itself**, and every subsequent command in that shell (including ones on later lines of the same script) fails with `command not found: cp` / `command not found: awk` / etc. — confirmed live 2026-08-07 editing nginx configs over SSH into a zsh remote shell, where a loop using `path="/usr/local/etc/nginx/..."` broke every command after the assignment with no explanation beyond `command not found`. **Rename to anything else** (`cfgpath`, `target`, `dest`, ...) — this isn't specific to nginx configs, it'll happen with any script that happens to pick `path` as a variable name on a zsh remote. A few other zsh-reserved lowercase names exist for the same reason (`status`, `pipestatus`) — when in doubt, avoid single common English words as variable names in scripts meant to run under an unknown/remote shell, or check `typeset -p ` first if unsure. ## What NOT to do Don't respond to a quoting failure by adding more layers of escaping (`\\\$`, `'"'"'`, etc.) — this usually makes the problem harder to reason about, not easier, and is exactly the kind of fragile fix that looks like it works until the next slightly-different payload breaks it again. Switch to the base64 pattern instead of debugging escape depth.