mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-22 11:24:18 +00:00
fix(security): add defense-in-depth username validation in GCP startup script (#2689)
Add explicit username format validation (`/^[a-zA-Z0-9_-]+$/`) as defense-in-depth in `getStartupScript()` and `createInstance()`. While `resolveUsername()` currently returns a constant, this belt-and-suspenders check prevents shell injection if the function is ever changed to accept dynamic input. Fixes #2688 Agent: ux-engineer 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
085759aeaf
commit
1696ecdaa9
1 changed files with 19 additions and 0 deletions
|
|
@ -647,10 +647,23 @@ async function ensureSshKey(): Promise<string> {
|
|||
|
||||
const GCP_SSH_USER = "root";
|
||||
|
||||
/** Defense-in-depth: allowed username pattern (alphanumeric, underscore, hyphen). */
|
||||
const SAFE_USERNAME_RE = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
function resolveUsername(): string {
|
||||
return GCP_SSH_USER;
|
||||
}
|
||||
|
||||
/** Assert username is safe for shell interpolation (defense-in-depth). */
|
||||
function assertSafeUsername(username: string): void {
|
||||
if (!SAFE_USERNAME_RE.test(username)) {
|
||||
throw new Error(
|
||||
`Invalid GCP username '${username}': must match /^[a-zA-Z0-9_-]+$/. ` +
|
||||
"This is a defense-in-depth check — the username should already be validated upstream.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Server Name ────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getServerName(): Promise<string> {
|
||||
|
|
@ -664,6 +677,11 @@ export async function promptSpawnName(): Promise<void> {
|
|||
// ─── Cloud Init Startup Script ──────────────────────────────────────────────
|
||||
|
||||
function getStartupScript(tier: CloudInitTier = "full"): string {
|
||||
// Defense-in-depth: validate username before any shell interpolation.
|
||||
// resolveUsername() currently returns a constant, but if it ever changes
|
||||
// to accept dynamic input, this prevents shell injection in the startup script.
|
||||
assertSafeUsername(resolveUsername());
|
||||
|
||||
const packages = getPackagesForTier(tier);
|
||||
const lines = [
|
||||
"#!/bin/bash",
|
||||
|
|
@ -705,6 +723,7 @@ export async function createInstance(
|
|||
tier?: CloudInitTier,
|
||||
): Promise<VMConnection> {
|
||||
const username = resolveUsername();
|
||||
assertSafeUsername(username);
|
||||
const pubKeys = await ensureSshKey();
|
||||
// Build ssh-keys metadata: one "user:key" entry per line
|
||||
const sshKeysMetadata = pubKeys
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue