Skip to content

Commit bd9797b

Browse files
Ark0Nclaude
andcommitted
fix: sanitize case names from filesystem to prevent XSS in inline handlers
Filter readdir and linked-case names through /^[a-zA-Z0-9_-]+$/ before returning them from GET /api/cases. Prevents XSS via maliciously-named directories reaching frontend inline onclick handlers where escapeHtml is insufficient (HTML-decoded back to quotes before JS execution). Also fix misleading "Drag or use arrows" hint (no drag-and-drop exists). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0e6cd94 commit bd9797b

File tree

2 files changed

+4
-3
lines changed

2 files changed

+4
-3
lines changed

src/web/public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1393,7 +1393,7 @@ <h3>Add Case</h3>
13931393
<div class="case-manage-list" id="caseManageList">
13941394
<!-- Populated by JS -->
13951395
</div>
1396-
<span class="form-hint" style="margin-top: 8px; display: block;">Drag or use arrows to reorder. Changes are saved automatically.</span>
1396+
<span class="form-hint" style="margin-top: 8px; display: block;">Use arrows to reorder. Changes are saved automatically.</span>
13971397
</div>
13981398
</div>
13991399
<div class="form-actions">

src/web/routes/case-routes.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { SseEvent } from '../sse-events.js';
1919
import type { EventPort, ConfigPort } from '../ports/index.js';
2020

2121
const LINKED_CASES_FILE = join(homedir(), '.codeman', 'linked-cases.json');
22+
const SAFE_CASE_NAME = /^[a-zA-Z0-9_-]+$/;
2223

2324
/** Read and parse linked-cases.json, returning empty object on missing/invalid file. */
2425
async function readLinkedCases(): Promise<Record<string, string>> {
@@ -46,7 +47,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
4647
try {
4748
const entries = await fs.readdir(CASES_DIR, { withFileTypes: true });
4849
for (const e of entries) {
49-
if (e.isDirectory()) {
50+
if (e.isDirectory() && SAFE_CASE_NAME.test(e.name)) {
5051
cases.push({
5152
name: e.name,
5253
path: join(CASES_DIR, e.name),
@@ -62,7 +63,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
6263
const linkedCases = await readLinkedCases();
6364
const existingNames = new Set(cases.map((c) => c.name));
6465
for (const [name, path] of Object.entries(linkedCases)) {
65-
if (!existingNames.has(name) && existsSync(path)) {
66+
if (!existingNames.has(name) && SAFE_CASE_NAME.test(name) && existsSync(path)) {
6667
cases.push({
6768
name,
6869
path,

0 commit comments

Comments
 (0)