fix(security): Apply 3 critical security fixes

- sec-001: Add validateAllowedPath to /api/file endpoint (path traversal)
- sec-002: Enable CSRF by default with CCW_DISABLE_CSRF opt-out
- sec-003: Add validateAllowedPath to /api/dialog/browse and /api/dialog/open-file (path traversal)

Ref: fix-1738072800000
This commit is contained in:
catlog22
2026-01-28 22:04:18 +08:00
parent ed0255b8a2
commit 502c8a09a1
5 changed files with 808 additions and 4 deletions

View File

@@ -113,9 +113,9 @@ export async function csrfValidation(ctx: CsrfMiddlewareContext): Promise<boolea
const { pathname, req, res } = ctx;
if (!pathname.startsWith('/api/')) return true;
// CSRF is disabled by default for local deployment scenarios.
// Set CCW_ENABLE_CSRF=1 to enable CSRF protection.
if (!envFlagEnabled('CCW_ENABLE_CSRF')) return true;
// CSRF is enabled by default for security.
// Set CCW_DISABLE_CSRF=1 to disable CSRF protection for local development.
if (envFlagEnabled('CCW_DISABLE_CSRF')) return true;
const method = (req.method || 'GET').toUpperCase();
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) return true;

View File

@@ -6,6 +6,7 @@ import type { Server } from 'http';
import { readFileSync, existsSync, promises as fsPromises } from 'fs';
import { join } from 'path';
import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay } from '../../utils/path-resolver.js';
import { validatePath as validateAllowedPath } from '../../utils/path-validator.js';
import { scanSessions } from '../session-scanner.js';
import { aggregateData } from '../data-aggregator.js';
import {
@@ -286,7 +287,12 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise<boole
}
try {
const content = await fsPromises.readFile(filePath, 'utf-8');
// Validate path is within allowed directories (fix: sec-001-a1b2c3d4)
const validatedPath = await validateAllowedPath(filePath, {
mustExist: true,
allowedDirectories: [process.cwd(), resolvePath('.ccw', 'sessions')]
});
const content = await fsPromises.readFile(validatedPath, 'utf-8');
const json = JSON.parse(content);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(json));
@@ -442,6 +448,15 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise<boole
targetPath = path.resolve(targetPath);
}
// Validate path is within allowed directories (fix: sec-003-c3d4e5f6)
const initialPath = process.cwd();
if (browsePath) {
targetPath = await validateAllowedPath(targetPath, {
mustExist: true,
allowedDirectories: [initialPath, os.homedir()]
});
}
try {
const stat = await fs.promises.stat(targetPath);
if (!stat.isDirectory()) {
@@ -502,6 +517,13 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise<boole
targetPath = path.resolve(targetPath);
}
// Validate path is within allowed directories (fix: sec-003-c3d4e5f6)
const initialPath = process.cwd();
targetPath = await validateAllowedPath(targetPath, {
mustExist: true,
allowedDirectories: [initialPath, os.homedir()]
});
try {
await fs.promises.access(targetPath, fs.constants.R_OK);
const stat = await fs.promises.stat(targetPath);