* fix: strip leading whitespace from sanitizeUserFacingText output
LLM responses frequently begin with \n\n, which survives through
sanitizeUserFacingText and reaches the channel as visible blank lines.
Root cause: the function used trimmed text for empty-checks but returned
the untrimmed 'stripped' variable. Two one-line fixes:
1. Return empty string (not whitespace-only 'stripped') for blank input
2. Apply trimStart() to the final return value
Fixes the same issue as #8052 and #10612 but at the root cause
(sanitizeUserFacingText) rather than scattering trimStart across
multiple delivery paths.
* Changelog: note sanitizeUserFacingText whitespace normalization
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
---------
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(security): validate OAuth state parameter to prevent CSRF attacks (OC-25)
The parseOAuthCallbackInput() function in the Chutes OAuth flow had two
critical bugs that completely defeated CSRF state validation:
1. State extracted from callback URL was never compared against the
expected cryptographic nonce, allowing attacker-controlled state values
2. When URL parsing failed (bare authorization code input), the catch block
fabricated a matching state using expectedState, making the caller's
CSRF check always pass
## Attack Flow
1. Victim runs `openclaw login chutes --manual`
2. System generates cryptographic state: randomBytes(16).toString("hex")
3. Browser opens: https://api.chutes.ai/idp/authorize?state=abc123...
4. Attacker obtains their OWN OAuth authorization code (out of band)
5. Attacker tricks victim into pasting just "EVIL_CODE" (not full URL)
6. parseOAuthCallbackInput("EVIL_CODE", "abc123...") is called
7. new URL("EVIL_CODE") throws → catch block executes
8. catch returns { code: "EVIL_CODE", state: "abc123..." } ← FABRICATED
9. Caller checks: parsed.state !== state → "abc123..." !== "abc123..." → FALSE
10. CSRF check passes! System calls exchangeChutesCodeForTokens()
11. Attacker's code exchanged for access + refresh tokens
12. Victim's account linked to attacker's OAuth session
Fix:
- Add explicit state validation against expectedState before returning
- Remove state fabrication from catch block; always return error for
non-URL input
- Add comprehensive unit tests for state validation
Remediated by Aether AI Agent security analysis.
* fix(security): harden chutes manual oauth state check (#16058) (thanks @aether-ai-agent)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(sandbox): add separate browser.binds config for browser containers
Allow configuring bind mounts independently for browser containers via
sandbox.browser.binds. When set, browser containers use browser-specific
binds instead of inheriting docker.binds. Falls back to docker.binds
when browser.binds is not configured for backwards compatibility.
Closes#14614
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(sandbox): honor empty browser binds override (#16230) (thanks @seheepeak)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>