fix(security): SEC-001 path traversal — add validation to trace + template services

Extended path traversal protection to two services missed in initial audit:

- trace-service.ts: validate attemptId, taskId, traceId before path.join
- template-service.ts: validate templateId in templatePath()

Both now use validatePathSegment() + ensureWithinBase() from utils/sanitize.ts.

Ref: RF-002a Batch 3a Findings (High+Medium severity)
This commit is contained in:
Brad Groux 2026-02-04 09:08:35 -06:00
parent ed1fbb3fb4
commit 30e18393f4
2 changed files with 17 additions and 1 deletions

View file

@ -8,6 +8,7 @@ import type {
UpdateTemplateInput,
} from '@veritas-kanban/shared';
import { createLogger } from '../lib/logger.js';
import { validatePathSegment, ensureWithinBase } from '../utils/sanitize.js';
const log = createLogger('template-service');
export class TemplateService {
@ -30,7 +31,10 @@ export class TemplateService {
}
private templatePath(id: string): string {
return join(this.templatesDir, `${id}.md`);
validatePathSegment(id);
const filepath = join(this.templatesDir, `${id}.md`);
ensureWithinBase(this.templatesDir, filepath);
return filepath;
}
/**

View file

@ -2,6 +2,7 @@ import fs from 'fs/promises';
import path from 'path';
import type { AgentType } from '@veritas-kanban/shared';
import { getTelemetryService } from './telemetry-service.js';
import { validatePathSegment, ensureWithinBase } from '../utils/sanitize.js';
const PROJECT_ROOT = path.resolve(process.cwd(), '..');
const TRACES_DIR = path.join(PROJECT_ROOT, '.veritas-kanban', 'traces');
@ -174,6 +175,9 @@ export class TraceService {
* Get a completed trace from disk
*/
async getTrace(attemptId: string): Promise<Trace | null> {
// Validate attemptId to prevent path traversal
validatePathSegment(attemptId);
// Check active traces first
const active = activeTraces.get(attemptId);
if (active) return active;
@ -181,6 +185,7 @@ export class TraceService {
// Try to load from disk
try {
const filepath = path.join(this.tracesDir, `${attemptId}.json`);
ensureWithinBase(this.tracesDir, filepath);
const content = await fs.readFile(filepath, 'utf-8');
return JSON.parse(content) as Trace;
} catch {
@ -193,6 +198,9 @@ export class TraceService {
* List all traces for a task
*/
async listTraces(taskId: string): Promise<Trace[]> {
// Validate taskId (used for filtering, not path construction, but good practice)
validatePathSegment(taskId);
const traces: Trace[] = [];
// Add active traces for this task
@ -230,8 +238,12 @@ export class TraceService {
* Save a trace to disk
*/
private async saveTrace(trace: Trace): Promise<void> {
// Validate traceId to prevent path traversal
validatePathSegment(trace.traceId);
await fs.mkdir(this.tracesDir, { recursive: true });
const filepath = path.join(this.tracesDir, `${trace.traceId}.json`);
ensureWithinBase(this.tracesDir, filepath);
await fs.writeFile(filepath, JSON.stringify(trace, null, 2), 'utf-8');
}
}