Two real gotchas from today's goddy->gringo Laravel migration: every jail on an NPM-fronted host needs a dedicated listen port (8000+last-octet), not just the shared port 80, or NPM cutover silently can't reach it even though every direct/Host-header test passes. Separately, zsh ties a variable literally named `path` to $PATH itself - overwriting it broke every subsequent command in a config-editing script with no clear error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
90 lines
4.4 KiB
Markdown
90 lines
4.4 KiB
Markdown
---
|
|
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 <jail>`),
|
|
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 <name>` 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.
|