Skip to content

fix(sandboxes): prevent shell injection in setupOpenClaw#147

Open
Juanpaulo1128 wants to merge 1 commit intorecoupable:mainfrom
Juanpaulo1128:fix/setup-openclaw-shell-injection
Open

fix(sandboxes): prevent shell injection in setupOpenClaw#147
Juanpaulo1128 wants to merge 1 commit intorecoupable:mainfrom
Juanpaulo1128:fix/setup-openclaw-shell-injection

Conversation

@Juanpaulo1128
Copy link
Copy Markdown

@Juanpaulo1128 Juanpaulo1128 commented May 4, 2026

Summary

setupOpenClaw interpolated accountId, RECOUP_API_KEY, and GITHUB_TOKEN directly into a node -e "..." shell command. accountId is validated only as z.string().min(1), so a value containing an apostrophe (e.g. o'malley) breaks out of the JS string literal and executes attacker-controlled code inside the Vercel Sandbox — with the API key and GitHub token already in scope.

Severity: Critical (shell + JS injection on user-controlled input, secrets in scope)
Affected: src/sandboxes/setupOpenClaw.ts only
Reachable from: any task that calls setupOpenClaw(sandbox, accountId)setupSandboxTask, runSandboxCommandTask, etc.

Root cause

// before
`node -e "
  c.env.RECOUP_API_KEY = '${process.env.RECOUP_API_KEY}';
  c.env.RECOUP_ACCOUNT_ID = '${accountId}';
  ${githubToken ? `c.env.GITHUB_TOKEN = '${githubToken}';` : ""}
"`

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes a critical shell/JS injection in `setupOpenClaw` by removing string interpolation and running a constant Node script with env vars. Prevents arbitrary code execution when `accountId` contains quotes or special characters.

- **Bug Fixes**
  - Replaced shell-interpolated `node -e "..."` with argv-form `node` and a constant `INJECT_ENV_SCRIPT`.
  - Passed `RECOUP_API_KEY`, `RECOUP_ACCOUNT_ID`, `OPENCLAW_DEFAULT_MODEL`, and optional `GITHUB_TOKEN` via `runCommand` `env`; script reads from `process.env`.
  - Added guard for missing `RECOUP_API_KEY` and kept masked logging and non-zero exit handling.

<sup>Written for commit 16b740f3ce280ed5ee060adfb4ebaec7e66fa357. Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Chores**
  * Enhanced security practices for credential handling in sandbox environment setup, including redacted logging of sensitive configuration values and explicit validation of required API credentials before initialization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

accountId, RECOUP_API_KEY, and GITHUB_TOKEN were interpolated into a
`node -e "..."` shell command. accountId is validated only as
z.string().min(1), so an apostrophe in the value would break out of
the JS string literal and execute attacker-controlled code in the
sandbox.

Pass the secrets as process env vars to a constant script body
instead. No more interpolation; runCommand uses argv form, no shell
layer.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

The setupOpenClaw function is refactored to inject OpenClaw environment variables by executing a fixed script inside the sandbox via node -e, reading required and optional credentials from process.env at runtime instead of interpolating secrets directly into the JS snippet. Error handling and gateway startup logic remain unchanged.

Changes

OpenClaw Environment Injection Security Improvement

Layer / File(s) Summary
Script Definition
src/sandboxes/setupOpenClaw.ts (lines 6–29)
New module-level INJECT_ENV_SCRIPT constant contains the sandbox-side JavaScript that reads ~/.openclaw/openclaw.json, sets RECOUP_API_KEY, RECOUP_ACCOUNT_ID, conditionally sets GITHUB_TOKEN, configures default model, and writes the updated JSON back to disk.
Environment Validation & Preparation
src/sandboxes/setupOpenClaw.ts (lines 44–68)
setupOpenClaw reads RECOUP_API_KEY from process.env, throws an explicit error if missing, reads optional GITHUB_TOKEN, and logs injection status with RECOUP_API_KEY redacted and GITHUB_TOKEN presence indicated; builds execution options to run the script via node -e with env vars passed through the sandbox env field.
Execution & Error Handling
src/sandboxes/setupOpenClaw.ts (lines 70–97)
After script execution, checks exit code and logs stderr on failure; then starts the gateway with nohup, logs detailed output on gateway failure, and throws on non-zero gateway exit code; otherwise logs "OpenClaw gateway started".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Secrets now safe in the shadows they dwell,
No more secrets whispered in shell command's bell,
A script reads what's needed, just-in-time and right,
Redacted logs sparkle with updated delight!
OpenClaw onboards with caution and care,
No interpolation lingering in the air!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(sandboxes): prevent shell injection in setupOpenClaw' directly and specifically describes the main security vulnerability being addressed in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/sandboxes/setupOpenClaw.ts (1)

57-68: Remove as any cast and properly type the runCommand payload.

The as any cast bypasses TypeScript's type checking for a security-critical call. All environment variable values here are strings (recoupApiKey and accountId are strings, OPENCLAW_DEFAULT_MODEL is a string constant, and githubToken is conditionally spread), so they match RunCommandParams.env's expected Record<string, string> type. Use Parameters<Sandbox["runCommand"]>[0] to extract the correct parameter type from the overloaded runCommand signature instead of the overly permissive Record<string, unknown>.

Suggested fix
-  const injectEnvOpts: Record<string, unknown> = {
+  const injectEnvOpts: Parameters<Sandbox["runCommand"]>[0] = {
     cmd: "node",
     args: ["-e", INJECT_ENV_SCRIPT],
     env: {
       RECOUP_API_KEY: recoupApiKey,
       RECOUP_ACCOUNT_ID: accountId,
       OPENCLAW_DEFAULT_MODEL,
       ...(githubToken ? { GITHUB_TOKEN: githubToken } : {}),
     },
   };
 
-  const injectEnv = await sandbox.runCommand(injectEnvOpts as any);
+  const injectEnv = await sandbox.runCommand(injectEnvOpts);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sandboxes/setupOpenClaw.ts` around lines 57 - 68, The runCommand call
currently uses an unsafe any cast; change injectEnvOpts to use the exact
parameter type for sandbox.runCommand (e.g. let injectEnvOpts:
Parameters<Sandbox["runCommand"]>[0]) and ensure the env property is typed as
Record<string,string> with all values as strings (recoupApiKey, accountId,
OPENCLAW_DEFAULT_MODEL and conditional GITHUB_TOKEN when githubToken is present)
so you can remove the as any cast and call sandbox.runCommand(injectEnvOpts).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/sandboxes/setupOpenClaw.ts`:
- Around line 51-55: The current logger.log call in setupOpenClaw.ts leaks
sensitive fragments and raw account identifiers (logger.log(... {
RECOUP_API_KEY: `${recoupApiKey.slice(0, 4)}...`, RECOUP_ACCOUNT_ID: accountId,
... })); change it to emit only presence booleans or redacted flags: replace
RECOUP_API_KEY with something like hasRecoupApiKey: Boolean(recoupApiKey) (no
fragments), replace RECOUP_ACCOUNT_ID with hasRecoupAccountId:
Boolean(accountId) (do not log the raw ID), and keep GITHUB_TOKEN as the
existing presence check; update the logger.log invocation and any related
callers to use these presence-only fields.
- Around line 18-20: When populating c.env in setupOpenClaw (the c variable in
src/sandboxes/setupOpenClaw.ts), ensure you explicitly clear c.env.GITHUB_TOKEN
when process.env.GITHUB_TOKEN is not set; currently only the presence branch
sets the token so old values persist in the persisted openclaw.json—add logic to
set c.env.GITHUB_TOKEN = undefined (or delete c.env.GITHUB_TOKEN) in the else
path to remove any previously saved credential.

---

Nitpick comments:
In `@src/sandboxes/setupOpenClaw.ts`:
- Around line 57-68: The runCommand call currently uses an unsafe any cast;
change injectEnvOpts to use the exact parameter type for sandbox.runCommand
(e.g. let injectEnvOpts: Parameters<Sandbox["runCommand"]>[0]) and ensure the
env property is typed as Record<string,string> with all values as strings
(recoupApiKey, accountId, OPENCLAW_DEFAULT_MODEL and conditional GITHUB_TOKEN
when githubToken is present) so you can remove the as any cast and call
sandbox.runCommand(injectEnvOpts).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c525641-c002-4a58-bfce-369b0035b142

📥 Commits

Reviewing files that changed from the base of the PR and between f9a0d2a and 16b740f.

📒 Files selected for processing (1)
  • src/sandboxes/setupOpenClaw.ts

Comment on lines +18 to +20
if (process.env.GITHUB_TOKEN) {
c.env.GITHUB_TOKEN = process.env.GITHUB_TOKEN;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear GITHUB_TOKEN when it is absent.

openclaw.json is persisted, so skipping this branch leaves any previous c.env.GITHUB_TOKEN in place. A later run without GITHUB_TOKEN will keep using the old credential.

Suggested fix
   c.env.RECOUP_API_KEY = process.env.RECOUP_API_KEY;
   c.env.RECOUP_ACCOUNT_ID = process.env.RECOUP_ACCOUNT_ID;
   if (process.env.GITHUB_TOKEN) {
     c.env.GITHUB_TOKEN = process.env.GITHUB_TOKEN;
+  } else {
+    delete c.env.GITHUB_TOKEN;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (process.env.GITHUB_TOKEN) {
c.env.GITHUB_TOKEN = process.env.GITHUB_TOKEN;
}
if (process.env.GITHUB_TOKEN) {
c.env.GITHUB_TOKEN = process.env.GITHUB_TOKEN;
} else {
delete c.env.GITHUB_TOKEN;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sandboxes/setupOpenClaw.ts` around lines 18 - 20, When populating c.env
in setupOpenClaw (the c variable in src/sandboxes/setupOpenClaw.ts), ensure you
explicitly clear c.env.GITHUB_TOKEN when process.env.GITHUB_TOKEN is not set;
currently only the presence branch sets the token so old values persist in the
persisted openclaw.json—add logic to set c.env.GITHUB_TOKEN = undefined (or
delete c.env.GITHUB_TOKEN) in the else path to remove any previously saved
credential.

Comment on lines 51 to 55
logger.log("Injecting env vars into openclaw.json", {
RECOUP_API_KEY: `${process.env.RECOUP_API_KEY.slice(0, 4)}...`,
RECOUP_API_KEY: `${recoupApiKey.slice(0, 4)}...`,
RECOUP_ACCOUNT_ID: accountId,
GITHUB_TOKEN: githubToken ? "present" : "missing",
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop logging secret fragments and raw account IDs.

This security fix still emits a stable prefix of RECOUP_API_KEY and the raw tenant identifier into logs. Prefer presence-only fields here.

Suggested fix
   logger.log("Injecting env vars into openclaw.json", {
-    RECOUP_API_KEY: `${recoupApiKey.slice(0, 4)}...`,
-    RECOUP_ACCOUNT_ID: accountId,
+    RECOUP_API_KEY: "present",
+    RECOUP_ACCOUNT_ID: "present",
     GITHUB_TOKEN: githubToken ? "present" : "missing",
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.log("Injecting env vars into openclaw.json", {
RECOUP_API_KEY: `${process.env.RECOUP_API_KEY.slice(0, 4)}...`,
RECOUP_API_KEY: `${recoupApiKey.slice(0, 4)}...`,
RECOUP_ACCOUNT_ID: accountId,
GITHUB_TOKEN: githubToken ? "present" : "missing",
});
logger.log("Injecting env vars into openclaw.json", {
RECOUP_API_KEY: "present",
RECOUP_ACCOUNT_ID: "present",
GITHUB_TOKEN: githubToken ? "present" : "missing",
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sandboxes/setupOpenClaw.ts` around lines 51 - 55, The current logger.log
call in setupOpenClaw.ts leaks sensitive fragments and raw account identifiers
(logger.log(... { RECOUP_API_KEY: `${recoupApiKey.slice(0, 4)}...`,
RECOUP_ACCOUNT_ID: accountId, ... })); change it to emit only presence booleans
or redacted flags: replace RECOUP_API_KEY with something like hasRecoupApiKey:
Boolean(recoupApiKey) (no fragments), replace RECOUP_ACCOUNT_ID with
hasRecoupAccountId: Boolean(accountId) (do not log the raw ID), and keep
GITHUB_TOKEN as the existing presence check; update the logger.log invocation
and any related callers to use these presence-only fields.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 1 file

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