security: scope /opt mount to /opt/projects
Splits the previous /opt:/opt:rw bind into two mounts to narrow the writable scope of the container: - /opt:/opt:ro — read-only mount for legacy/existing project add-existing flow. resolveProjectPath still uses PROJECT_ROOT_WHITELIST (/opt by default) so existing projects under /opt/<name> (analytics, boolab, boocode itself) continue to resolve and serve their file-tree via the read-only tools. - /opt/projects:/opt/projects:rw — writable mount targeted at the create-new-project bootstrap path. Picked Option B from the spec (simpler than two scan roots): PROJECT_ROOT_WHITELIST stays /opt, new BOOTSTRAP_ROOT env var defaults to /opt/projects and is used by project_bootstrap.ts as the mkdir target. Bootstrap path-escape check now compares against BOOTSTRAP_ROOT. Prereq: host must `mkdir -p /opt/projects` before next container restart. Documented in CLAUDE.md and .env.example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ const ConfigSchema = z.object({
|
||||
DATABASE_URL: z.string().url(),
|
||||
LLAMA_SWAP_URL: z.string().url(),
|
||||
PROJECT_ROOT_WHITELIST: z.string().default('/opt'),
|
||||
BOOTSTRAP_ROOT: z.string().default('/opt/projects'),
|
||||
DEFAULT_MODEL: z.string().default('qwen3.6-35b-a3b-mxfp4'),
|
||||
LOG_LEVEL: z.string().default('info'),
|
||||
GITEA_BASE_URL: z.string().url().default('https://git.indifferentketchup.com'),
|
||||
|
||||
@@ -82,11 +82,13 @@ export async function bootstrapProject(
|
||||
throw new BootstrapNameError(`invalid name after sanitization: "${folder}"`);
|
||||
}
|
||||
|
||||
// Whitelist resolution
|
||||
const whitelistReal = await realpath(config.PROJECT_ROOT_WHITELIST);
|
||||
const fullPath = resolve(whitelistReal, folder);
|
||||
if (!fullPath.startsWith(whitelistReal + sep)) {
|
||||
throw new BootstrapPathError('path escapes whitelist');
|
||||
// Bootstrap target resolution. Uses BOOTSTRAP_ROOT (writable), not
|
||||
// PROJECT_ROOT_WHITELIST (which may be a wider read-only scope for
|
||||
// add-existing flow).
|
||||
const bootstrapReal = await realpath(config.BOOTSTRAP_ROOT);
|
||||
const fullPath = resolve(bootstrapReal, folder);
|
||||
if (!fullPath.startsWith(bootstrapReal + sep)) {
|
||||
throw new BootstrapPathError('path escapes bootstrap root');
|
||||
}
|
||||
if (existsSync(fullPath)) {
|
||||
throw new BootstrapCollisionError(`path already exists: ${fullPath}`);
|
||||
|
||||
Reference in New Issue
Block a user