mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
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>
This commit is contained in:
@@ -1393,7 +1393,7 @@
|
|||||||
<div class="case-manage-list" id="caseManageList">
|
<div class="case-manage-list" id="caseManageList">
|
||||||
<!-- Populated by JS -->
|
<!-- Populated by JS -->
|
||||||
</div>
|
</div>
|
||||||
<span class="form-hint" style="margin-top: 8px; display: block;">Drag or use arrows to reorder. Changes are saved automatically.</span>
|
<span class="form-hint" style="margin-top: 8px; display: block;">Use arrows to reorder. Changes are saved automatically.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { SseEvent } from '../sse-events.js';
|
|||||||
import type { EventPort, ConfigPort } from '../ports/index.js';
|
import type { EventPort, ConfigPort } from '../ports/index.js';
|
||||||
|
|
||||||
const LINKED_CASES_FILE = join(homedir(), '.codeman', 'linked-cases.json');
|
const LINKED_CASES_FILE = join(homedir(), '.codeman', 'linked-cases.json');
|
||||||
|
const SAFE_CASE_NAME = /^[a-zA-Z0-9_-]+$/;
|
||||||
|
|
||||||
/** Read and parse linked-cases.json, returning empty object on missing/invalid file. */
|
/** Read and parse linked-cases.json, returning empty object on missing/invalid file. */
|
||||||
async function readLinkedCases(): Promise<Record<string, string>> {
|
async function readLinkedCases(): Promise<Record<string, string>> {
|
||||||
@@ -46,7 +47,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
|
|||||||
try {
|
try {
|
||||||
const entries = await fs.readdir(CASES_DIR, { withFileTypes: true });
|
const entries = await fs.readdir(CASES_DIR, { withFileTypes: true });
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
if (e.isDirectory()) {
|
if (e.isDirectory() && SAFE_CASE_NAME.test(e.name)) {
|
||||||
cases.push({
|
cases.push({
|
||||||
name: e.name,
|
name: e.name,
|
||||||
path: join(CASES_DIR, e.name),
|
path: join(CASES_DIR, e.name),
|
||||||
@@ -62,7 +63,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
|
|||||||
const linkedCases = await readLinkedCases();
|
const linkedCases = await readLinkedCases();
|
||||||
const existingNames = new Set(cases.map((c) => c.name));
|
const existingNames = new Set(cases.map((c) => c.name));
|
||||||
for (const [name, path] of Object.entries(linkedCases)) {
|
for (const [name, path] of Object.entries(linkedCases)) {
|
||||||
if (!existingNames.has(name) && existsSync(path)) {
|
if (!existingNames.has(name) && SAFE_CASE_NAME.test(name) && existsSync(path)) {
|
||||||
cases.push({
|
cases.push({
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
|
|||||||
Reference in New Issue
Block a user