{"record":{"id":"dc8f6c1efceb94b8","repo":"pbakaus/impeccable","slug":"invalid-session-id-id","errorCode":null,"errorMessage":"invalid session id: ${id}","messagePattern":"invalid session id: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"skill/scripts/lib/impeccable-paths.mjs","lineNumber":110,"sourceCode":"  return filePath;\n}\n\nexport function removeLiveServerInfo(cwd = process.cwd(), options = {}) {\n  for (const filePath of [getLiveServerPath(cwd, options), getLegacyLiveServerPath(cwd, options)]) {\n    try { fs.unlinkSync(filePath); } catch {}\n  }\n}\n\n/**\n * Session IDs become path segments (journals, snapshots, accept receipts,\n * preview manifests, generated component dirs). They arrive from CLI `--id`\n * arguments and HTTP payloads, so anything containing a separator or `..` must\n * be rejected before it reaches path.join, which would happily escape\n * `.impeccable/live/`. Real IDs are 8 hex chars; the tests use short slugs.\n */\nexport function safeSessionId(id) {\n  if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id)) {\n    throw new Error('invalid session id: ' + id);\n  }\n  return id;\n}\n\nexport function getLiveSessionsDir(cwd = process.cwd(), options = {}) {\n  return path.join(getLiveDir(cwd, options), 'sessions');\n}\n\nexport function getLegacyLiveSessionsDir(cwd = process.cwd(), options = {}) {\n  return path.join(resolveProjectRoot(cwd, options), '.impeccable-live', 'sessions');\n}\n\nexport function getLiveAnnotationsDir(cwd = process.cwd(), options = {}) {\n  return path.join(getLiveDir(cwd, options), 'annotations');\n}\n\nexport function getCritiqueDir(cwd = process.cwd(), options = {}) {\n  return path.join(getImpeccableDir(cwd, options), CRITIQUE_DIR);","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/pbakaus/impeccable/blob/f88b2837a7d7c3182e46307bbbb091a1ed547571/skill/scripts/lib/impeccable-paths.mjs#L92-L128","documentation":"Thrown by safeSessionId when a session id fails the pattern ^[A-Za-z0-9_-]{1,128}$ or is not a string. Session ids become path segments under .impeccable/live/ (journals, snapshots, accept receipts, preview manifests, generated component dirs) and arrive from untrusted surfaces — CLI --id arguments and HTTP payloads — so separators, '..', or over-long ids must be rejected before path.join can escape the sessions directory. Genuine ids are 8 hex characters; tests use short slugs.","triggerScenarios":"Passing `--id ../../etc` or `--id foo/bar` (path separators); an id with spaces or unicode punctuation from a copy-paste; `--id` with an undefined/empty value interpolated into a request payload; an HTTP client posting {\"id\": 12345} as a number rather than a string.","commonSituations":"Hand-typed ids drifting from the 8-hex-char convention; agents fabricating ids instead of reading them from session output; API consumers assuming any unique string is acceptable; attempt (accidental or deliberate) to traverse out of the live-sessions directory.","solutions":["Use the exact id printed when the session was created (8 hex chars).","If generating ids yourself, stick to [A-Za-z0-9_-] and at most 128 chars — e.g. crypto.randomBytes(4).toString('hex').","Send ids as strings in HTTP payloads, never numbers or objects.","Remove any path separators, dots, or whitespace before passing the id."],"exampleFix":"// before\nconst id = req.body.id;                 // 12345 (number) or \"../oops\"\nconst dir = path.join(sessionsDir, id);   // traversal / NaN-ish join\n\n// after\nimport { safeSessionId } from './lib/impeccable-paths.mjs';\nconst id = safeSessionId(String(req.body.id));   // throws early on bad input\nconst dir = path.join(sessionsDir, id);","handlingStrategy":"type-guard","validationCode":"const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;\nfunction isValidSessionId(id) {\n  return typeof id === 'string' && SESSION_ID_RE.test(id);\n}\n// gate untrusted input (CLI --id, HTTP payload) before any path work:\nif (!isValidSessionId(inputId)) return res.status(400).json({ error: 'invalid session id' });\nconst dir = path.join(sessionsDir, safeSessionId(inputId));","typeGuard":"import { safeSessionId } from './lib/impeccable-paths.mjs';\nfunction asSessionId(id) {\n  try { return safeSessionId(id); } catch { return null; }\n}\nconst id = asSessionId(req.body?.id);\nif (id === null) { /* reject request */ }","tryCatchPattern":"try {\n  const id = safeSessionId(raw);\n} catch (err) {\n  if (/^invalid session id/.test(err.message)) {\n    // untrusted input problem: reject at the trust boundary, never retry or sanitize by stripping characters\n    return badRequest('session id must match [A-Za-z0-9_-]{1,128}');\n  }\n  throw err;\n}","preventionTips":["Always use the id emitted when the session was created (8 hex chars).","Generate ids with crypto.randomBytes(4).toString('hex') — already in-alphabet.","Coerce HTTP payload ids with String() and validate before joining them into paths.","Never sanitize a bad id by deleting characters; reject it — partial sanitizing can still yield a wrong session."],"tags":["validation","security","path-traversal","session-id","cli","http-payload"],"backgroundTag":"path-traversal-validation","analyzedSha":"f88b2837a7d7c3182e46307bbbb091a1ed547571","analyzedAt":"2026-08-18T04:58:36.608Z","contentChangedAt":"2026-08-18T04:58:36.608Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}