Skip to content

security: fix command injection in Telegram bridge#119

Open
kakuteki wants to merge 1 commit intoNVIDIA:mainfrom
kakuteki:fix/telegram-bridge-command-injection
Open

security: fix command injection in Telegram bridge#119
kakuteki wants to merge 1 commit intoNVIDIA:mainfrom
kakuteki:fix/telegram-bridge-command-injection

Conversation

@kakuteki
Copy link
Copy Markdown

@kakuteki kakuteki commented Mar 17, 2026

Summary

Fixes #118Command injection vulnerability in scripts/telegram-bridge.js

The Telegram bridge interpolated user messages directly into a shell command string passed to SSH. The existing escaping (single-quote only) did not prevent $() or backtick expansion, allowing attackers to execute arbitrary commands inside the sandbox and potentially exfiltrate the NVIDIA_API_KEY.

Changes

  • Pass user message and API key via stdin instead of shell string interpolation — the remote script reads the API key with read -r (line 1) and the message with cat (remaining stdin), then expands them inside double-quoted "$VAR" which is not subject to further shell parsing
  • Validate sessionId — strip non-alphanumeric characters (Telegram chat IDs are numeric)
  • Validate SANDBOX_NAME at startup — reject names with shell metacharacters to prevent injection in the execSync call
  • Change stdio from ["ignore", ...] to ["pipe", ...] so stdin is writable

Before (vulnerable)

const escaped = message.replace(/'/g, "'\''");
const cmd = `export NVIDIA_API_KEY='${API_KEY}' && nemoclaw-start ... -m '${escaped}' ...`;
spawn("ssh", [..., cmd], { stdio: ["ignore", "pipe", "pipe"] });

After (fixed)

const remoteScript = "read -r NVIDIA_API_KEY && export NVIDIA_API_KEY && MSG=$(cat) && exec nemoclaw-start ... -m \"$MSG\" ...";
const proc = spawn("ssh", [..., remoteScript], { stdio: ["pipe", "pipe", "pipe"] });
proc.stdin.write(API_KEY + "\n");
proc.stdin.write(message);
proc.stdin.end();

Test plan

  • Send a normal message via Telegram → agent responds correctly
  • Send a message containing $(whoami) → treated as literal text, no command execution
  • Send a message containing backticks and single quotes → no injection
  • Set an invalid SANDBOX_NAME (e.g. foo;rm -rf /) → startup exits with error
  • Verify NVIDIA_API_KEY no longer appears in process arguments (ps aux)

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced validation of configuration names to prevent invalid entries and improve error handling.
    • Improved sanitization of session identifiers for greater stability.
    • Strengthened security handling for sensitive credentials by preventing direct command-line exposure and reducing potential injection vulnerabilities.
    • Refined process communication to ensure more secure data transmission.

User messages from Telegram were interpolated directly into a shell
command string with only single-quote escaping, allowing attackers to
execute arbitrary commands via $() or backtick expansion.

Fix: pass both the API key and user message through stdin instead of
the command string, validate sessionId and SANDBOX_NAME, and change
stdio from "ignore" to "pipe" so stdin is writable.
@kakuteki kakuteki force-pushed the fix/telegram-bridge-command-injection branch from 7b1dfd9 to fa74697 Compare March 18, 2026 18:55
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

The pull request addresses a command injection vulnerability in the Telegram Bridge by sanitizing inputs (session ID and SANDBOX name), validating characters, and refactoring data passage from command-line embedding to stdin, preventing attackers from executing arbitrary commands via malicious messages.

Changes

Cohort / File(s) Summary
Telegram Bridge Security Hardening
scripts/telegram-bridge.js
Added SANDBOX name validation with invalid character checks. Introduced sessionId sanitization to safeSessionId. Replaced static SSH command construction with a remoteScript that reads NVIDIA_API_KEY and message from stdin instead of embedding them in the shell command string, eliminating command injection attack surface. Updated SSH stdio handling and temp config path to use sanitized session ID.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 With whiskers twitched and paws held high,
A rabbit cheers as injections die!
Through stdin flows the secret key,
Command injection? Never more—we're free!
Sanitized and safe, the bridge now stands,
Guarded by this coder's careful hands. 🔐

🚥 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 'security: fix command injection in Telegram bridge' clearly and concisely summarizes the primary change—fixing a command injection vulnerability in the Telegram bridge script.
Linked Issues check ✅ Passed All coding requirements from issue #118 are met: command injection is fixed via stdin-based message passing, sessionId and SANDBOX_NAME are validated, SSH stdio is configured for stdin writability, and double-quoted variable expansion prevents shell parsing.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing the command injection vulnerability in scripts/telegram-bridge.js as specified in issue #118; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
scripts/telegram-bridge.js (1)

25-27: Consider making ALLOWED_CHAT_IDS mandatory for production deployments.

The optional ALLOWED_CHAT_IDS whitelist is a good defense-in-depth mechanism. For production, enforcing this (or at least emitting a warning when unset) would reduce attack surface by ensuring only trusted Telegram chats can interact with the agent.

 const ALLOWED_CHATS = process.env.ALLOWED_CHAT_IDS
   ? process.env.ALLOWED_CHAT_IDS.split(",").map((s) => s.trim())
   : null;
+
+if (!ALLOWED_CHATS) {
+  console.warn("WARNING: ALLOWED_CHAT_IDS not set — all Telegram chats can interact with this bot");
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/telegram-bridge.js` around lines 25 - 27, Make ALLOWED_CHAT_IDS
mandatory (or emit a clear warning) by checking process.env.ALLOWED_CHAT_IDS at
startup and failing fast or logging a high‑severity warning if it's missing;
update the ALLOWED_CHATS initialization (the variable ALLOWED_CHATS and the
process.env.ALLOWED_CHAT_IDS usage) so that when the env var is absent the
process either exits with a descriptive error message or logs a prominent
warning advising this must be set in production, and ensure the message includes
guidance on the expected comma-separated format.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@scripts/telegram-bridge.js`:
- Around line 25-27: Make ALLOWED_CHAT_IDS mandatory (or emit a clear warning)
by checking process.env.ALLOWED_CHAT_IDS at startup and failing fast or logging
a high‑severity warning if it's missing; update the ALLOWED_CHATS initialization
(the variable ALLOWED_CHATS and the process.env.ALLOWED_CHAT_IDS usage) so that
when the env var is absent the process either exits with a descriptive error
message or logs a prominent warning advising this must be set in production, and
ensure the message includes guidance on the expected comma-separated format.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9730f601-7e91-4bf6-908f-ec6ee23bb938

📥 Commits

Reviewing files that changed from the base of the PR and between 241ffb2 and fa74697.

📒 Files selected for processing (1)
  • scripts/telegram-bridge.js

@kakuteki
Copy link
Copy Markdown
Author

Hi maintainers, could you please approve the CI workflow run for this PR? I've rebased onto the latest main. Thank you!

Copy link
Copy Markdown

@north-echo north-echo left a comment

Choose a reason for hiding this comment

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

Security Review, PR #119

The stdin-based approach for passing the API key and user message is the right call here. It eliminates shell interpretation of user input on the remote side entirely. This is the correct fix for the core command injection (issue #118). A few gaps worth addressing:

  1. execSync still used for ssh-config (line 95)

const sshConfig = execSync(openshell sandbox ssh-config ${SANDBOX}, { encoding: "utf-8" });

Even though SANDBOX is validated at startup, this is still string interpolation passed to a shell. PR #320 gets this right with execFileSync("openshell", ["sandbox", "ssh-config", SANDBOX]). Defense in depth: the validation is one layer, parameterized execution is another.

  1. SANDBOX regex allows leading hyphens

if (!/^[a-zA-Z0-9_-]+$/.test(SANDBOX)) {

This matches -v, --help, etc., which could be interpreted as options by openshell. PR #320's pattern is better:

/^[A-Za-z0-9][A-Za-z0-9_-]*$/

Requiring an alphanumeric first character prevents option injection.

  1. Predictable temp file path

const confPath = /tmp/nemoclaw-tg-ssh-${safeSessionId}.conf;

safeSessionId is derived from the Telegram chat ID, a public integer. On multi-user systems this is a symlink race target (CWE-377). PR #320 solves this with crypto.randomBytes(8).toString("hex") and exclusive creation (flag: "wx").

  1. No message length cap

Unbounded user input passed to the agent. A reasonable cap (e.g., 4096 chars matching Telegram's own limit) would prevent abuse.

  1. Bare openshell vs resolved path

The script imports resolveOpenshell() and validates it at startup, but the execSync call on line 95 uses bare openshell instead of the resolved OPENSHELL path.

Recommendation

This PR and #320 are complementary, not competing. This PR fixes the core injection; #320 fixes the surrounding hardening (execFileSync, temp file races, better SANDBOX regex). Combining both would give a comprehensive fix. Consider adopting #320's hardening into this branch, or coordinating with @pjt222.

The stdin approach itself is sound.

Christopher Lusk (christopherdlusk@gmail.com)

@wscurran wscurran added bug Something isn't working security Something isn't secure labels Mar 19, 2026
@wscurran
Copy link
Copy Markdown
Contributor

Thanks for identifying and fixing the command injection vulnerability, this is a critical security fix that will help protect NemoClaw users.

@wscurran wscurran added the priority: high Important issue that should be resolved in the next release label Mar 19, 2026
@kakuteki
Copy link
Copy Markdown
Author

@north-echo Thank you for the thorough security review. I'll coordinate with @pjt222 to address the points you raised.

@wscurran wscurran added the Integration: Telegram Use this label to identify Telegram bot integration issues with NemoClaw. label Mar 20, 2026
@wscurran wscurran requested a review from drobison00 March 23, 2026 16:43
@jyaunches jyaunches self-assigned this Mar 23, 2026
@cv
Copy link
Copy Markdown
Contributor

cv commented Mar 24, 2026

Not a duplicate of #617 (bridge framework). Your stdin-based approach (passing the API key and message via stdin instead of shell interpolation) is a different and arguably more robust mitigation than #617's shellQuote() approach. shellQuote can be bypassed if the quoting function has edge cases; stdin avoids shell parsing entirely.

However, #617 rewrites telegram-bridge.js into a backwards-compat wrapper that delegates to bridge.js. If #617 merges first, the file this PR patches will no longer contain the vulnerable code path. The stdin approach would need to be applied to bridge-core.js instead.

Worth coordinating with #617 on merge order.

jyaunches added a commit to jyaunches/NemoClaw that referenced this pull request Mar 25, 2026
…nitization

Adds two new Brev E2E test suites targeting the vulnerabilities fixed by
PR NVIDIA#119 (Telegram bridge command injection) and PR NVIDIA#156 (credential
exposure in migration snapshots + blueprint digest bypass).

Test suites:
- test-telegram-injection.sh: 8 tests covering command substitution,
  backtick injection, quote-breakout, parameter expansion, process
  table leaks, and SANDBOX_NAME validation
- test-credential-sanitization.sh: 13 tests covering auth-profiles.json
  deletion, credential field stripping, non-credential preservation,
  symlink safety, blueprint digest verification, and pattern-based
  field detection

These tests are expected to FAIL on main (unfixed code) and PASS
once PR NVIDIA#119 and NVIDIA#156 are merged.

Refs: NVIDIA#118, NVIDIA#119, NVIDIA#156, NVIDIA#813
jyaunches added a commit to jyaunches/NemoClaw that referenced this pull request Mar 25, 2026
Adds specification for a warm-pool model that eliminates the 40+ min
cold-start Docker build time from Brev E2E test runs.

Key design:
- Pre-warmed Brev instances using NemoClaw launchable
- Naming convention (e2e-warm-*) as state — no external tracking
- PR comment trigger (/test-brev <suites>) for maintainers
- Parallel test execution across pool instances
- Daily health check + 24h instance cycling
- Fallback to ephemeral mode if pool is empty

6 phases: pool script → warmer workflow → test runner →
PR trigger → run security tests → cleanup

Also includes the original NVIDIA#813 hardware E2E spec for reference.

Refs: NVIDIA#813, NVIDIA#118, NVIDIA#119, NVIDIA#156
mafueee pushed a commit to mafueee/NemoClaw that referenced this pull request Mar 28, 2026
jyaunches added a commit to jyaunches/NemoClaw that referenced this pull request Mar 30, 2026
…nitization

Adds two new Brev E2E test suites targeting the vulnerabilities fixed by
PR NVIDIA#119 (Telegram bridge command injection) and PR NVIDIA#156 (credential
exposure in migration snapshots + blueprint digest bypass).

Test suites:
- test-telegram-injection.sh: 8 tests covering command substitution,
  backtick injection, quote-breakout, parameter expansion, process
  table leaks, and SANDBOX_NAME validation
- test-credential-sanitization.sh: 13 tests covering auth-profiles.json
  deletion, credential field stripping, non-credential preservation,
  symlink safety, blueprint digest verification, and pattern-based
  field detection

These tests are expected to FAIL on main (unfixed code) and PASS
once PR NVIDIA#119 and NVIDIA#156 are merged.

Refs: NVIDIA#118, NVIDIA#119, NVIDIA#156, NVIDIA#813
cv added a commit that referenced this pull request Mar 30, 2026
…al sanitization (#1092)

* test(security): add E2E tests for command injection and credential sanitization

Adds two new Brev E2E test suites targeting the vulnerabilities fixed by
PR #119 (Telegram bridge command injection) and PR #156 (credential
exposure in migration snapshots + blueprint digest bypass).

Test suites:
- test-telegram-injection.sh: 8 tests covering command substitution,
  backtick injection, quote-breakout, parameter expansion, process
  table leaks, and SANDBOX_NAME validation
- test-credential-sanitization.sh: 13 tests covering auth-profiles.json
  deletion, credential field stripping, non-credential preservation,
  symlink safety, blueprint digest verification, and pattern-based
  field detection

These tests are expected to FAIL on main (unfixed code) and PASS
once PR #119 and #156 are merged.

Refs: #118, #119, #156, #813

* ci: temporarily disable repo guard for fork testing

* ci: bump bootstrap timeout, skip vLLM on CPU E2E runs

- Add SKIP_VLLM=1 support to brev-setup.sh
- Use SKIP_VLLM=1 in brev-e2e.test.js bootstrap
- Bump beforeAll timeout to 30 min for CPU instances
- Bump workflow timeout to 60 min for 3 test suites

* ci: bump bootstrap timeout to 40 min for sandbox image build

* ci: bump Brev instance to 8x32 for faster Docker builds

* ci: add real-time progress streaming for E2E bootstrap and tests

- Stream SSH output to CI log during bootstrap (no more silence)
- Add timestamps to brev-setup.sh and setup.sh info/warn/fail messages
- Add background progress reporter during sandbox Docker build (heartbeat every 30s showing elapsed time, current Docker step, and last log line)
- Stream test script output to CI log via tee + capture for assertions
- Filter potential secrets from progress heartbeat output

* ci: use NemoClaw launchable for E2E bootstrap

Replace bare 'brev create' + brev-setup.sh with 'brev start' using the
OpenShell-Community launch-nemoclaw.sh setup script. This installs Docker,
OpenShell CLI, and Node.js via the launchable's proven path, then runs
'nemoclaw onboard --non-interactive' to build the sandbox (testing whether
this path is faster than our manual setup.sh).

Changes:
- Default CPU back to 4x16 (8x32 didn't help — bottleneck was I/O)
- Launchable path: brev start + setup-script URL, poll for completion,
  rsync PR branch, npm ci, nemoclaw onboard
- Legacy path preserved (USE_LAUNCHABLE=0)
- Timestamped logging throughout for timing comparison
- New use_launchable workflow input (default: true)

* fix: prevent openshell sandbox create from hanging in non-interactive mode

openshell sandbox create without a command defaults to opening an interactive
shell inside the sandbox. In CI (non-interactive SSH), this hangs forever —
the sandbox goes Ready but the command never returns. The [?2004h] terminal
escape codes in CI logs were bash enabling bracketed paste mode, waiting for
input.

Add --no-tty -- true so the command exits immediately after the sandbox is
created and Ready.

* fix: source nvm in non-interactive SSH for launchable path

The launchable setup script installs Node.js via nvm, which sets up PATH
in ~/.nvm/nvm.sh. Non-interactive SSH doesn't source .bashrc, so npm/node
commands fail with 'command not found'. Source nvm.sh before running npm
in the launchable path and runRemoteTest.

* fix: setup.sh respects NEMOCLAW_SANDBOX_NAME env var

setup.sh defaulted to 'nemoclaw' ignoring the NEMOCLAW_SANDBOX_NAME env
var set by the CI test harness (e2e-test). Now uses $1 > $NEMOCLAW_SANDBOX_NAME > nemoclaw.

* ci: bump full E2E test timeout to 15 min for install + sandbox build

* ci: don't run full E2E alongside security tests (it destroys the sandbox)

The full E2E test runs install.sh --non-interactive which destroys and
rebuilds the sandbox. When TEST_SUITE=all, this kills the sandbox that
beforeAll created, causing credential-sanitization and telegram-injection
to fail with 'sandbox not running'. Only run full E2E when TEST_SUITE=full.

* ci: pre-build base image locally when GHCR image unavailable

On forks or before the first base-image workflow run, the GHCR base image
(ghcr.io/nvidia/nemoclaw/sandbox-base:latest) doesn't exist. This causes
the Dockerfile's FROM to fail. Now setup.sh checks for the base image
and builds Dockerfile.base locally if needed.

On subsequent builds, Docker layer cache makes this near-instant.
Once the GHCR base image is available, this becomes a no-op (docker pull
succeeds and the local build is skipped).

* ci: install nemoclaw CLI after bootstrap in non-launchable path

brev-setup.sh creates the sandbox but doesn't install the host-side
nemoclaw CLI that test scripts need for 'nemoclaw <name> status'.
Add npm install + build + link step after bootstrap.

* fix: use npm_config_prefix for nemoclaw CLI install so it lands on PATH

* fix: npm link from repo root where bin.nemoclaw is defined

* fix(ci): register sandbox in nemoclaw registry after setup.sh bootstrap

setup.sh creates the sandbox via openshell directly but never writes
~/.nemoclaw/sandboxes.json. The security test scripts check
`nemoclaw <name> status` which reads the registry, causing all E2E
runs to fail with 'Sandbox e2e-test not running'.

Write the registry entry after nemoclaw CLI install so the test
scripts can find the sandbox.

* style: shfmt formatting fix in setup.sh

* fix(test): exclude policy presets from C7 secret pattern scan

C7 greps for 'npm_' inside the sandbox and false-positives on
nemoclaw-blueprint/policies/presets/npm.yaml which contains rule
names like 'npm_yarn', not actual credentials. Filter out /policies/
paths from all three pattern checks.

* docs(ci): add test suite descriptions to e2e-brev workflow header

Document what each test_suite option runs so maintainers can make an
informed choice from the Actions UI without reading the test scripts.

* ci: re-enable repo guard for e2e-brev workflow

Re-enable the github.repository check so the workflow only runs on
NVIDIA/NemoClaw, not on forks.

* fix(test): update setup-sandbox-name test for NEMOCLAW_SANDBOX_NAME env var

setup.sh now uses ${1:-${NEMOCLAW_SANDBOX_NAME:-nemoclaw}} instead of
${1:-nemoclaw}. Update the test to match and add coverage for the env
var fallback path.

* fix(lint): add shellcheck directives for injection test payloads and fix stdio type

* fix(lint): suppress SC2034 for status_output in credential sanitization test

* fix: address CodeRabbit review — timeout, pipefail, fail-closed probes, shell injection in test

- Bump e2e-brev workflow timeout-minutes from 60 to 90
- Add fail-fast when launchable setup exceeds 40-min wait
- Add pipefail to remote pipeline commands in runRemoteTest and npm ci
- Fix backtick shell injection in validateName test loop (use process.argv)
- Make sandbox_exec fail closed with __PROBE_FAILED__ sentinel
- Add probe failure checks in C6/C7 sandbox assertions

---------

Co-authored-by: Carlos Villela <cvillela@nvidia.com>
@jyaunches
Copy link
Copy Markdown
Contributor

Hi! Thanks for this contribution. It looks like this branch has fallen a bit behind main and has some merge conflicts. Could you please rebase or merge with main to resolve the conflicts? Let me know if you need any help. Thanks!

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

Labels

bug Something isn't working Integration: Telegram Use this label to identify Telegram bot integration issues with NemoClaw. priority: high Important issue that should be resolved in the next release security Something isn't secure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Telegram Bridge command injection

6 participants