|
7 | 7 | import { FastifyInstance } from 'fastify'; |
8 | 8 | import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs'; |
9 | 9 | import fs from 'node:fs/promises'; |
10 | | -import { join, resolve, relative, isAbsolute } from 'node:path'; |
| 10 | +import { join, resolve } from 'node:path'; |
11 | 11 | import { homedir } from 'node:os'; |
12 | 12 | import type { ApiResponse, CaseInfo } from '../../types.js'; |
13 | 13 | import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js'; |
14 | 14 | import { CreateCaseSchema, LinkCaseSchema } from '../schemas.js'; |
15 | 15 | import { generateClaudeMd } from '../../templates/claude-md.js'; |
16 | 16 | import { writeHooksConfig } from '../../hooks-config.js'; |
17 | | -import { CASES_DIR } from '../route-helpers.js'; |
| 17 | +import { CASES_DIR, validatePathWithinBase } from '../route-helpers.js'; |
18 | 18 | import { SseEvent } from '../sse-events.js'; |
19 | 19 | import type { EventPort, ConfigPort } from '../ports/index.js'; |
20 | 20 |
|
@@ -74,13 +74,8 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config |
74 | 74 | } |
75 | 75 | const { name, description } = result.data; |
76 | 76 |
|
77 | | - const casePath = join(CASES_DIR, name); |
78 | | - |
79 | | - // Security: Path traversal protection - use relative path check |
80 | | - const resolvedPath = resolve(casePath); |
81 | | - const resolvedBase = resolve(CASES_DIR); |
82 | | - const relPath = relative(resolvedBase, resolvedPath); |
83 | | - if (relPath.startsWith('..') || isAbsolute(relPath)) { |
| 77 | + const casePath = validatePathWithinBase(name, CASES_DIR); |
| 78 | + if (!casePath) { |
84 | 79 | return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case path'); |
85 | 80 | } |
86 | 81 |
|
@@ -167,11 +162,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config |
167 | 162 | app.get('/api/cases/:name', async (req) => { |
168 | 163 | const { name } = req.params as { name: string }; |
169 | 164 |
|
170 | | - // Security: Path traversal protection |
171 | | - const resolvedPath = resolve(join(CASES_DIR, name)); |
172 | | - const resolvedBase = resolve(CASES_DIR); |
173 | | - const relPath = relative(resolvedBase, resolvedPath); |
174 | | - if (relPath.startsWith('..') || isAbsolute(relPath)) { |
| 165 | + if (!validatePathWithinBase(name, CASES_DIR)) { |
175 | 166 | return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name'); |
176 | 167 | } |
177 | 168 |
|
@@ -210,11 +201,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config |
210 | 201 | app.get('/api/cases/:name/fix-plan', async (req) => { |
211 | 202 | const { name } = req.params as { name: string }; |
212 | 203 |
|
213 | | - // Security: Path traversal protection |
214 | | - const resolvedPath = resolve(join(CASES_DIR, name)); |
215 | | - const resolvedBase = resolve(CASES_DIR); |
216 | | - const relPath = relative(resolvedBase, resolvedPath); |
217 | | - if (relPath.startsWith('..') || isAbsolute(relPath)) { |
| 204 | + if (!validatePathWithinBase(name, CASES_DIR)) { |
218 | 205 | return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name'); |
219 | 206 | } |
220 | 207 |
|
@@ -334,13 +321,8 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config |
334 | 321 |
|
335 | 322 | app.get('/api/cases/:caseName/ralph-wizard/files', async (req) => { |
336 | 323 | const { caseName } = req.params as { caseName: string }; |
337 | | - let casePath = join(CASES_DIR, caseName); |
338 | | - |
339 | | - // Security: Path traversal protection - use relative path check |
340 | | - const resolvedCase = resolve(casePath); |
341 | | - const resolvedBase = resolve(CASES_DIR); |
342 | | - const relPath = relative(resolvedBase, resolvedCase); |
343 | | - if (relPath.startsWith('..') || isAbsolute(relPath)) { |
| 324 | + let casePath = validatePathWithinBase(caseName, CASES_DIR); |
| 325 | + if (!casePath) { |
344 | 326 | return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name'); |
345 | 327 | } |
346 | 328 |
|
@@ -394,21 +376,16 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config |
394 | 376 | // Cache disabled to ensure fresh prompts when starting new plan generations |
395 | 377 | app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req, reply) => { |
396 | 378 | const { caseName, filePath } = req.params as { caseName: string; filePath: string }; |
397 | | - let casePath = join(CASES_DIR, caseName); |
| 379 | + let casePath = validatePathWithinBase(caseName, CASES_DIR); |
| 380 | + if (!casePath) { |
| 381 | + return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name'); |
| 382 | + } |
398 | 383 |
|
399 | 384 | // Prevent browser caching - prompts change between plan generations |
400 | 385 | reply.header('Cache-Control', 'no-store, no-cache, must-revalidate'); |
401 | 386 | reply.header('Pragma', 'no-cache'); |
402 | 387 | reply.header('Expires', '0'); |
403 | 388 |
|
404 | | - // Security: Path traversal protection for case name - use relative path check |
405 | | - const resolvedCase = resolve(casePath); |
406 | | - const resolvedBase = resolve(CASES_DIR); |
407 | | - const relPath = relative(resolvedBase, resolvedCase); |
408 | | - if (relPath.startsWith('..') || isAbsolute(relPath)) { |
409 | | - return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name'); |
410 | | - } |
411 | | - |
412 | 389 | // Check linked cases if path doesn't exist |
413 | 390 | if (!existsSync(casePath)) { |
414 | 391 | const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json'); |
|
0 commit comments