Skip to content

fix: escape shell metacharacters in expanded env files#1311

Open
ralphbean wants to merge 1 commit into
mainfrom
fix/shell-safe-env-expansion
Open

fix: escape shell metacharacters in expanded env files#1311
ralphbean wants to merge 1 commit into
mainfrom
fix/shell-safe-env-expansion

Conversation

@ralphbean
Copy link
Copy Markdown
Contributor

Summary

  • Replace os.ExpandEnv with a shell-safe expander that escapes \, ", $, and ` — the four characters special inside double-quoted shell strings
  • Env file templates (export FOO="${FOO}") stay unchanged; the expander ensures substituted values can't break out of the double-quote context
  • Adds 21 unit tests and 28 shell roundtrip tests (source expanded file in real sh, read value back, assert exact match)

Problem

HUMAN_INSTRUCTION and other user-authored env vars are expanded into shell env files via os.ExpandEnv, which does naive text substitution. When the value contains ", (, $, or backticks, the expanded file has invalid shell syntax:

# Template:
export HUMAN_INSTRUCTION="${HUMAN_INSTRUCTION}"

# After os.ExpandEnv with value containing double quotes and parens:
export HUMAN_INSTRUCTION="revise "Grant all roles" (for example, with IAM policy)"
#                               ^ closes the quote   ^ syntax error

This caused the pre-agent security scan to flag it as a critical finding, blocking the fix agent entirely.

Failing run: https://github.com/fullsend-ai/.fullsend/actions/runs/26232380969/job/77196067764

Fix

shellSafeExpandEnv uses os.Expand with a custom mapper that calls escapeForDoubleQuotes on each substituted value. Only the four characters that have special meaning inside double quotes need escaping:

Character Escaped Why
\ \\ Escape character (must be first to avoid double-escaping)
" \" Would close the surrounding double quotes
$ \$ Would trigger variable/command expansion
` \` Would trigger command substitution

Characters like (, ), ||, &&, ;, > are not special inside double quotes and don't need escaping.

Test plan

Fixes #408
Fixes #615

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

Site preview

Preview: https://290e0d47-site.fullsend-ai.workers.dev

Commit: f2c64ba2e0ef2c850bc84f23c66406a3cc94efc3

@fullsend-ai-review
Copy link
Copy Markdown

fullsend-ai-review Bot commented May 21, 2026

Review

Findings

No findings.

Previous run

Review

Findings

No findings.

The change correctly replaces os.ExpandEnv with a shell-safe expander that escapes the four characters special inside double-quoted shell strings (\, ", $, `). The escape order is correct (backslash first to prevent double-escaping). The implementation uses os.Expand with a custom mapper, which preserves the existing ${VAR} expansion behavior while adding safety.

The test suite is thorough: 21 unit tests cover individual escape cases, edge cases, and injection attempts; 28 shell roundtrip tests source the expanded file in a real sh process and verify exact value preservation. The roundtrip tests are the definitive correctness proof — if values survive a real shell source-and-read cycle, the escaping is sound.

The remaining os.ExpandEnv calls in the codebase are appropriate: line 938 in run.go expands file paths (not shell-sourced content), and sandbox.go uses it for credential/config values passed as CLI args (not sourced as shell).

@fullsend-ai-review fullsend-ai-review Bot added the ready-for-merge All reviewers approved — ready to merge label May 21, 2026
Replace os.ExpandEnv with a shell-safe expander that escapes the four
characters special inside double quotes (\, ", $, `) before substituting
values into env file templates. This prevents user-authored text like
HUMAN_INSTRUCTION from breaking shell syntax when the expanded env file
is sourced inside the sandbox.

The env file templates use export FOO="${FOO}" — after Go-level expansion,
unescaped values containing double quotes or parentheses would break out
of the quoting context, causing "Syntax error: ( unexpected" or similar
failures in the sandbox shell.

Includes 21 unit tests and 28 shell roundtrip tests that verify every
expanded value survives a real sh source-and-read cycle, covering
injection attempts, all special characters, and the exact values from
the failing runs referenced in the issues.

Fixes #408
Fixes #615

Signed-off-by: Ralph Bean <[email protected]>
Assisted-by: Claude claude-opus-4-6 <[email protected]>
Signed-off-by: Ralph Bean <[email protected]>
@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge and removed ready-for-merge All reviewers approved — ready to merge labels May 21, 2026
Copy link
Copy Markdown
Contributor

@waynesun09 waynesun09 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Squad (10 agents) — 1 HIGH, 2 MEDIUM findings

Well-executed security fix. The escaping logic is POSIX-correct (all four double-quote special characters handled in the right order) and the shell roundtrip tests are exemplary. The HIGH finding — 9 env template lines across 5 files use unquoted ${VAR} patterns where the escaping assumptions don't hold — is not exploitable today (all values are machine-generated), but the templates should be standardized to "${VAR}" in a follow-up to eliminate the risk class.

Comment thread internal/cli/run.go
Comment on lines +1000 to +1005
// shellSafeExpandEnv expands ${VAR} references in text using the host
// environment, escaping characters that are special inside double quotes
// (", $, `, \) so the result is safe to source as a shell script.
// Templates use the standard export FOO="${FOO}" pattern; this function
// ensures substituted values cannot break out of the double-quote context.
// Fixes #408, #615.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — Unquoted env templates are not protected by this escaping

The doc comment says "Templates use the standard export FOO="${FOO}" pattern" but 9 template lines across 5 files use unquoted ${VAR}:

  • code-agent.env: export ISSUE_NUMBER=${ISSUE_NUMBER}, GITHUB_ISSUE_URL, GH_TOKEN
  • review.env, triage.env, prioritize.env: export GH_TOKEN=${GH_TOKEN}
  • gcp-vertex.env: ANTHROPIC_VERTEX_PROJECT_ID, CLOUD_ML_REGION, GOOGLE_CLOUD_PROJECT

In an unquoted context, characters like spaces, ;, |, &&, > are shell operators — the escaper does NOT protect against them. Currently safe because these are machine-generated values (tokens, integers, project IDs), but the function name shellSafeExpandEnv implies broader safety than it provides.

Suggestion (follow-up PR): Standardize all env templates to export VAR="${VAR}" — a one-line change per template, zero regression risk (double-quoting simple values is a no-op in shell), eliminates an entire class of future bugs.

7/10 review agents flagged this (strong consensus)

Comment thread internal/cli/run_test.go
env: map[string]string{"A": "1", "B": "two (2)"},
want: "export A=\"1\"\nexport B=\"two (2)\"",
},
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM — Misleading test name

This test uses FOO=bar (no special characters) but the name "unquoted template still safe" implies the escaper handles unquoted contexts safely for arbitrary values. It does not — e.g., FOO="hello world" would expand to export FOO=hello world, and sh would interpret world as a separate command.

Suggest renaming to "unquoted template with simple value" to remove the safety implication, and optionally adding a comment documenting that unquoted templates are only safe for values without whitespace or shell operators.

5/10 review agents flagged this

Comment thread internal/cli/run_test.go
@@ -430,6 +431,209 @@ func TestEnvToList_Sorted(t *testing.T) {
assert.Equal(t, "Z_VAR=z", list[2])
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM — Missing test for $VAR (no braces) expansion

All test templates use ${VAR} (with braces), but os.Expand also handles $VAR (without braces). Adding one test case with template export FOO="$FOO" would confirm the brace-less path also gets escaped values and document this behavior.

3/10 review agents flagged this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-merge All reviewers approved — ready to merge

Projects

None yet

2 participants