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:
@@ -3,5 +3,6 @@ PORT=3000
|
||||
DATABASE_URL=postgres://boocode:CHANGE_ME@boocode_db:5432/boocode
|
||||
LLAMA_SWAP_URL=http://100.101.41.16:8401
|
||||
PROJECT_ROOT_WHITELIST=/opt
|
||||
BOOTSTRAP_ROOT=/opt/projects
|
||||
DEFAULT_MODEL=qwen3.6-35b-a3b-mxfp4
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
|
||||
@@ -88,7 +88,7 @@ Position-shift pattern for panes (legacy `session_panes` table): negate-and-rest
|
||||
|
||||
## Environment
|
||||
|
||||
Required: `DATABASE_URL`, `LLAMA_SWAP_URL`. Optional: `PORT` (3000), `HOST` (0.0.0.0), `PROJECT_ROOT_WHITELIST` (/opt), `DEFAULT_MODEL`, `LOG_LEVEL`.
|
||||
Required: `DATABASE_URL`, `LLAMA_SWAP_URL`. Optional: `PORT` (3000), `HOST` (0.0.0.0), `PROJECT_ROOT_WHITELIST` (/opt, read-only scope for add-existing path resolution), `BOOTSTRAP_ROOT` (/opt/projects, writable scope for create-new-project bootstrap mkdir target — host must `mkdir -p /opt/projects` before container start), `DEFAULT_MODEL`, `LOG_LEVEL`.
|
||||
|
||||
## Workflow
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -9,7 +9,11 @@ services:
|
||||
environment:
|
||||
DATABASE_URL: postgres://boocode:${POSTGRES_PASSWORD}@boocode_db:5432/boocode
|
||||
volumes:
|
||||
- /opt:/opt:rw
|
||||
# Read-only mount for legacy/existing project add-existing flow.
|
||||
- /opt:/opt:ro
|
||||
# Writable mount only for the create-new-project bootstrap target.
|
||||
# Host must `mkdir -p /opt/projects` before container start.
|
||||
- /opt/projects:/opt/projects:rw
|
||||
depends_on:
|
||||
- boocode_db
|
||||
networks:
|
||||
|
||||
Reference in New Issue
Block a user