mirror of
https://github.com/whyour/qinglong.git
synced 2025-12-13 07:25:05 +08:00
342 lines
10 KiB
JavaScript
342 lines
10 KiB
JavaScript
const Module = require('module');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
// Get the QL_DIR and data directory paths
|
|
const qlDir = process.env.QL_DIR || path.join(__dirname, '../../');
|
|
let dataDir = process.env.QL_DATA_DIR || path.join(qlDir, 'data');
|
|
|
|
// Remove trailing slash if present
|
|
dataDir = dataDir.replace(/\/$/, '');
|
|
|
|
// Normalize paths to avoid bypassing with relative paths or symlinks
|
|
const normalizedQlDir = fs.existsSync(qlDir) ? fs.realpathSync(qlDir) : path.resolve(qlDir);
|
|
const normalizedDataDir = fs.existsSync(dataDir) ? fs.realpathSync(dataDir) : path.resolve(dataDir);
|
|
|
|
// Protected directories - no write access allowed
|
|
const protectedPaths = [
|
|
path.join(normalizedQlDir, 'back'),
|
|
path.join(normalizedQlDir, 'src'),
|
|
path.join(normalizedQlDir, 'shell'),
|
|
path.join(normalizedQlDir, 'sample'),
|
|
path.join(normalizedQlDir, 'node_modules'),
|
|
path.join(normalizedDataDir, 'config'),
|
|
path.join(normalizedDataDir, 'db'),
|
|
];
|
|
|
|
// Allowed write directories - scripts can write here
|
|
const allowedWritePaths = [
|
|
path.join(normalizedDataDir, 'scripts'),
|
|
path.join(normalizedDataDir, 'log'),
|
|
path.join(normalizedDataDir, 'repo'),
|
|
path.join(normalizedDataDir, 'raw'),
|
|
path.join(normalizedQlDir, '.tmp'),
|
|
'/tmp',
|
|
];
|
|
|
|
// Check if sandboxing is enabled (default: true)
|
|
const sandboxEnabled = process.env.QL_DISABLE_SANDBOX !== 'true';
|
|
|
|
function isPathProtected(targetPath) {
|
|
if (!sandboxEnabled) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Resolve to absolute path and follow symlinks
|
|
const resolvedPath = fs.realpathSync.native ?
|
|
fs.realpathSync.native(targetPath) :
|
|
path.resolve(targetPath);
|
|
|
|
// Check if path is in a protected directory
|
|
for (const protectedPath of protectedPaths) {
|
|
if (resolvedPath.startsWith(protectedPath + path.sep) || resolvedPath === protectedPath) {
|
|
// Check if it's in an allowed subdirectory
|
|
const isInAllowedPath = allowedWritePaths.some(allowedPath =>
|
|
resolvedPath.startsWith(allowedPath + path.sep) || resolvedPath === allowedPath
|
|
);
|
|
|
|
if (!isInAllowedPath) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check if trying to write outside data/scripts without being in allowed paths
|
|
const isInQlDir = resolvedPath.startsWith(normalizedQlDir + path.sep) || resolvedPath === normalizedQlDir;
|
|
const isInDataDir = resolvedPath.startsWith(normalizedDataDir + path.sep) || resolvedPath === normalizedDataDir;
|
|
|
|
if (isInQlDir || isInDataDir) {
|
|
const isInAllowedPath = allowedWritePaths.some(allowedPath =>
|
|
resolvedPath.startsWith(allowedPath + path.sep) || resolvedPath === allowedPath
|
|
);
|
|
|
|
if (!isInAllowedPath) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch (err) {
|
|
// If path doesn't exist yet, check parent directory
|
|
const parentPath = path.dirname(targetPath);
|
|
if (parentPath !== targetPath) {
|
|
return isPathProtected(parentPath);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function createSecurityError(operation, targetPath) {
|
|
const err = new Error(
|
|
`Security Error: Script attempted to ${operation} protected path: ${targetPath}\n` +
|
|
`Scripts are only allowed to write to: ${allowedWritePaths.join(', ')}`
|
|
);
|
|
err.code = 'EACCES';
|
|
return err;
|
|
}
|
|
|
|
// Store original fs methods
|
|
const originalFS = {};
|
|
const writeOperations = [
|
|
'writeFile', 'writeFileSync',
|
|
'appendFile', 'appendFileSync',
|
|
'mkdir', 'mkdirSync',
|
|
'rmdir', 'rmdirSync',
|
|
'unlink', 'unlinkSync',
|
|
'rm', 'rmSync',
|
|
'rename', 'renameSync',
|
|
'copyFile', 'copyFileSync',
|
|
'chmod', 'chmodSync',
|
|
'chown', 'chownSync',
|
|
'link', 'linkSync',
|
|
'symlink', 'symlinkSync',
|
|
'truncate', 'truncateSync',
|
|
'utimes', 'utimesSync',
|
|
];
|
|
|
|
// Wrap fs methods
|
|
for (const method of writeOperations) {
|
|
if (fs[method]) {
|
|
originalFS[method] = fs[method];
|
|
}
|
|
}
|
|
|
|
function wrapFsMethod(method, isSync) {
|
|
return function(...args) {
|
|
const targetPath = args[0];
|
|
|
|
if (isPathProtected(targetPath)) {
|
|
const err = createSecurityError(method, targetPath);
|
|
if (isSync) {
|
|
throw err;
|
|
} else {
|
|
const callback = args[args.length - 1];
|
|
if (typeof callback === 'function') {
|
|
process.nextTick(() => callback(err));
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// For rename/copy operations, check destination too
|
|
if ((method.startsWith('rename') || method.startsWith('copy')) && args[1]) {
|
|
if (isPathProtected(args[1])) {
|
|
const err = createSecurityError(method, args[1]);
|
|
if (isSync) {
|
|
throw err;
|
|
} else {
|
|
const callback = args[args.length - 1];
|
|
if (typeof callback === 'function') {
|
|
process.nextTick(() => callback(err));
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
return originalFS[method].apply(fs, args);
|
|
};
|
|
}
|
|
|
|
// Apply wrappers
|
|
if (sandboxEnabled) {
|
|
for (const method of writeOperations) {
|
|
if (fs[method]) {
|
|
const isSync = method.endsWith('Sync');
|
|
fs[method] = wrapFsMethod(method, isSync);
|
|
}
|
|
}
|
|
|
|
// Wrap createWriteStream
|
|
originalFS.createWriteStream = fs.createWriteStream;
|
|
fs.createWriteStream = function(targetPath, options) {
|
|
if (isPathProtected(targetPath)) {
|
|
throw createSecurityError('createWriteStream', targetPath);
|
|
}
|
|
return originalFS.createWriteStream.call(fs, targetPath, options);
|
|
};
|
|
|
|
// Wrap promises API if it exists
|
|
if (fs.promises) {
|
|
const promisesOriginal = {};
|
|
const promisesMethods = [
|
|
'writeFile', 'appendFile', 'mkdir', 'rmdir', 'unlink', 'rm',
|
|
'rename', 'copyFile', 'chmod', 'chown', 'link', 'symlink',
|
|
'truncate', 'utimes',
|
|
];
|
|
|
|
for (const method of promisesMethods) {
|
|
if (fs.promises[method]) {
|
|
promisesOriginal[method] = fs.promises[method];
|
|
fs.promises[method] = async function(...args) {
|
|
const targetPath = args[0];
|
|
|
|
if (isPathProtected(targetPath)) {
|
|
throw createSecurityError(method, targetPath);
|
|
}
|
|
|
|
// For rename/copy operations, check destination too
|
|
if ((method === 'rename' || method === 'copyFile') && args[1]) {
|
|
if (isPathProtected(args[1])) {
|
|
throw createSecurityError(method, args[1]);
|
|
}
|
|
}
|
|
|
|
return promisesOriginal[method].apply(fs.promises, args);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wrap child_process to prevent sandbox bypass via subprocesses
|
|
let childProcessWrapped = false;
|
|
if (sandboxEnabled) {
|
|
// We need to get child_process before wrapping Module.prototype.require
|
|
const childProcess = require('child_process');
|
|
const originalSpawn = childProcess.spawn;
|
|
const originalExec = childProcess.exec;
|
|
const originalExecSync = childProcess.execSync;
|
|
const originalExecFile = childProcess.execFile;
|
|
const originalExecFileSync = childProcess.execFileSync;
|
|
const originalFork = childProcess.fork;
|
|
|
|
// Helper to ensure NODE_OPTIONS and PYTHONPATH are set for subprocesses
|
|
function ensureSandboxEnv(options = {}) {
|
|
const env = { ...process.env, ...options.env };
|
|
|
|
// Ensure NODE_OPTIONS includes the sandbox
|
|
const sandboxPreload = path.join(__dirname, 'sandbox.js');
|
|
if (!env.NODE_OPTIONS) {
|
|
env.NODE_OPTIONS = '';
|
|
}
|
|
if (!env.NODE_OPTIONS.includes(sandboxPreload)) {
|
|
env.NODE_OPTIONS = `-r ${sandboxPreload} ${env.NODE_OPTIONS}`.trim();
|
|
}
|
|
|
|
// Ensure PYTHONPATH includes the sandbox directory
|
|
if (!env.PYTHONPATH) {
|
|
env.PYTHONPATH = '';
|
|
}
|
|
if (!env.PYTHONPATH.includes(__dirname)) {
|
|
env.PYTHONPATH = `${__dirname}:${env.PYTHONPATH}`;
|
|
}
|
|
|
|
return { ...options, env };
|
|
}
|
|
|
|
// Wrap spawn
|
|
childProcess.spawn = function(...args) {
|
|
if (args[2]) {
|
|
args[2] = ensureSandboxEnv(args[2]);
|
|
} else if (args.length >= 3) {
|
|
args[2] = ensureSandboxEnv({});
|
|
}
|
|
return originalSpawn.apply(childProcess, args);
|
|
};
|
|
|
|
// Wrap exec
|
|
childProcess.exec = function(...args) {
|
|
const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] : undefined;
|
|
const optionsIndex = callback ? args.length - 2 : args.length - 1;
|
|
|
|
if (args[optionsIndex] && typeof args[optionsIndex] === 'object') {
|
|
args[optionsIndex] = ensureSandboxEnv(args[optionsIndex]);
|
|
} else if (optionsIndex > 0) {
|
|
args.splice(optionsIndex, 0, ensureSandboxEnv({}));
|
|
}
|
|
return originalExec.apply(childProcess, args);
|
|
};
|
|
|
|
// Wrap execSync
|
|
childProcess.execSync = function(...args) {
|
|
if (args[1]) {
|
|
args[1] = ensureSandboxEnv(args[1]);
|
|
} else {
|
|
args[1] = ensureSandboxEnv({});
|
|
}
|
|
return originalExecSync.apply(childProcess, args);
|
|
};
|
|
|
|
// Wrap execFile
|
|
childProcess.execFile = function(...args) {
|
|
const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] : undefined;
|
|
const optionsIndex = callback ? args.length - 2 : args.length - 1;
|
|
|
|
if (args[optionsIndex] && typeof args[optionsIndex] === 'object') {
|
|
args[optionsIndex] = ensureSandboxEnv(args[optionsIndex]);
|
|
} else if (optionsIndex > 1) {
|
|
args.splice(optionsIndex, 0, ensureSandboxEnv({}));
|
|
}
|
|
return originalExecFile.apply(childProcess, args);
|
|
};
|
|
|
|
// Wrap execFileSync
|
|
childProcess.execFileSync = function(...args) {
|
|
if (args[2]) {
|
|
args[2] = ensureSandboxEnv(args[2]);
|
|
} else if (args.length >= 3) {
|
|
args[2] = ensureSandboxEnv({});
|
|
}
|
|
return originalExecFileSync.apply(childProcess, args);
|
|
};
|
|
|
|
// Wrap fork
|
|
childProcess.fork = function(...args) {
|
|
if (args[2]) {
|
|
args[2] = ensureSandboxEnv(args[2]);
|
|
} else if (args.length >= 3) {
|
|
args[2] = ensureSandboxEnv({});
|
|
}
|
|
return originalFork.apply(childProcess, args);
|
|
};
|
|
|
|
childProcessWrapped = true;
|
|
}
|
|
|
|
// Prevent requiring the original fs or child_process modules to bypass sandbox
|
|
const originalRequire = Module.prototype.require;
|
|
Module.prototype.require = function(id) {
|
|
const module = originalRequire.apply(this, arguments);
|
|
|
|
// Return wrapped fs module
|
|
if (id === 'fs' || id === 'node:fs') {
|
|
return fs;
|
|
}
|
|
|
|
// For child_process, we already wrapped it above, so just return it
|
|
// (no need to re-require as that would cause recursion)
|
|
|
|
return module;
|
|
};
|
|
|
|
module.exports = {
|
|
sandboxEnabled,
|
|
isPathProtected,
|
|
protectedPaths,
|
|
allowedWritePaths,
|
|
};
|