Skip to content

shell: reject multi-line LLM output so bash -c can't execute hidden commands#365

Merged
tbckr merged 1 commit into
tbckr:mainfrom
SAY-5:fix/sanitize-command-reject-newlines-360
Apr 26, 2026
Merged

shell: reject multi-line LLM output so bash -c can't execute hidden commands#365
tbckr merged 1 commit into
tbckr:mainfrom
SAY-5:fix/sanitize-command-reject-newlines-360

Conversation

@SAY-5

@SAY-5 SAY-5 commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #360.

SanitizeCommand explicitly preserved \n, and the sanitized string was passed straight to bash -c. Because bash -c treats newlines as command separators, a prompt-injected multi-line LLM response like ls\nrm -rf ~ rendered as two apparently-harmless lines in the confirmation prompt but executed both commands once the user typed Y.

The sanitizer's own comment scoped it to "display manipulation attacks". The fix needs to go a step further: for anything that is handed to bash -c, multi-line payloads have to be refused.

Fix

  • Change SanitizeCommand(string) to SanitizeCommand(string) (string, error).
  • Return the sentinel ErrMultilineCommand whenever the cleaned payload contains \n or \r.
  • ExecuteCommandWithConfirmation now aborts (and surfaces the error on output) before the confirmation prompt, so the user never sees just the harmless-looking first line.
  • Tabs stay preserved - they're a legitimate part of printf / awk invocations.

Pipes / redirects / && / || are intentionally still allowed - legitimate use cases like du -a | sort -rh | head -3 must continue to work - and are documented risks the user opts in to by choosing the shell-execution feature. Newlines are the specific class the confirmation prompt can't render honestly, which is why this layer is tightened.

Test plan

  • go build ./...
  • go test ./pkg/shell/... -count=1 (all existing tests updated for the new signature; new TestSanitizeCommand_RejectsMultilineInjection exercises \n, \r, \r\n, and the ls\nrm -rf ~ PoC)
  • TestSanitizeCommand_PreservesTab replaces the old PreservesNewlineAndTab to lock in the tab-preservation contract

…ommands

SanitizeCommand explicitly preserved '\n', and the sanitized
string was passed straight to `bash -c`. Because `bash -c`
treats newlines as command separators, a prompt-injected
multi-line LLM response like `ls\nrm -rf ~` renders as two
apparently-harmless lines in the confirmation prompt but
executes both commands once the user types Y (tbckr#360).

Change SanitizeCommand to return (string, error) and fail with
ErrMultilineCommand whenever the cleaned payload contains '\n'
or '\r'. Tabs are still preserved (they're legitimately used in
printf and friends). ExecuteCommandWithConfirmation now aborts
before showing the confirmation prompt when the LLM response is
multi-line, so the hidden trailing commands never reach bash.

Test coverage: new TestSanitizeCommand_RejectsMultilineInjection
exercises the \n, \r, \r\n and `ls\nrm -rf ~` paths and asserts
ErrMultilineCommand. Existing tests are updated for the new
signature. The replaced TestSanitizeCommand_PreservesNewlineAndTab
becomes TestSanitizeCommand_PreservesTab (tab-only).

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

Fixes tbckr#360
@SAY-5 SAY-5 requested a review from tbckr as a code owner April 21, 2026 07:19
@tbckr tbckr enabled auto-merge (squash) April 23, 2026 11:34
@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.92%. Comparing base (abd2da9) to head (62a138e).
⚠️ Report is 325 commits behind head on main.

Files with missing lines Patch % Lines
pkg/shell/shell.go 75.00% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #365      +/-   ##
==========================================
+ Coverage   75.80%   75.92%   +0.11%     
==========================================
  Files          20       21       +1     
  Lines        1298     1429     +131     
==========================================
+ Hits          984     1085     +101     
- Misses        226      256      +30     
  Partials       88       88              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tbckr tbckr disabled auto-merge April 26, 2026 09:37
@tbckr tbckr merged commit 21ee01f into tbckr:main Apr 26, 2026
7 checks passed
tbckr pushed a commit that referenced this pull request Apr 26, 2026
…dden commands (#365)

SanitizeCommand explicitly preserved '\n', and the sanitized
string was passed straight to `bash -c`. Because `bash -c`
treats newlines as command separators, a prompt-injected
multi-line LLM response like `ls\nrm -rf ~` renders as two
apparently-harmless lines in the confirmation prompt but
executes both commands once the user types Y (#360).

Change SanitizeCommand to return (string, error) and fail with
ErrMultilineCommand whenever the cleaned payload contains '\n'
or '\r'. Tabs are still preserved (they're legitimately used in
printf and friends). ExecuteCommandWithConfirmation now aborts
before showing the confirmation prompt when the LLM response is
multi-line, so the hidden trailing commands never reach bash.

Test coverage: new TestSanitizeCommand_RejectsMultilineInjection
exercises the \n, \r, \r\n and `ls\nrm -rf ~` paths and asserts
ErrMultilineCommand. Existing tests are updated for the new
signature. The replaced TestSanitizeCommand_PreservesNewlineAndTab
becomes TestSanitizeCommand_PreservesTab (tab-only).

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>

Fixes #360

Co-authored-by: SAY-5 <SAY-5@users.noreply.github.com>
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.

security: SanitizeCommand preserves newlines — multi-line LLM output executes as multiple shell commands

2 participants