{"record":{"id":"71aa022bd5e8cda2","repo":"Yeachan-Heo/oh-my-codex","slug":"invalid-state-key","errorCode":null,"errorMessage":"invalid state key","messagePattern":"invalid state key","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/hooks/extensibility/sdk/plugin-state.ts","lineNumber":20,"sourceCode":"import { mkdir, readFile, unlink, writeFile } from 'fs/promises';\nimport { dirname, join } from 'path';\nimport type { HookPluginSdk } from '../types.js';\nimport { hookPluginDataPath, hookPluginRootDir, sanitizeHookPluginName } from './paths.js';\n\nasync function readJsonIfExists<T>(path: string, fallback: T): Promise<T> {\n  if (!existsSync(path)) return fallback;\n  try {\n    return JSON.parse(await readFile(path, 'utf-8')) as T;\n  } catch {\n    return fallback;\n  }\n}\n\nexport function normalizeHookPluginStateKey(key: string): string {\n  const trimmed = key.trim();\n  if (!trimmed) throw new Error('state key is required');\n  if (trimmed.includes('..') || trimmed.startsWith('/')) {\n    throw new Error('invalid state key');\n  }\n  return trimmed;\n}\n\nexport function createHookPluginStateApi(\n  cwd: string,\n  pluginName: string,\n): HookPluginSdk['state'] {\n  const dataPath = hookPluginDataPath(cwd, pluginName);\n\n  async function readData(): Promise<Record<string, unknown>> {\n    return readJsonIfExists<Record<string, unknown>>(dataPath, {});\n  }\n\n  async function writeData(value: Record<string, unknown>): Promise<void> {\n    await mkdir(dirname(dataPath), { recursive: true });\n    await writeFile(dataPath, JSON.stringify(value, null, 2));\n  }","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/hooks/extensibility/sdk/plugin-state.ts#L2-L38","documentation":"Thrown by normalizeHookPluginStateKey when the key contains '..' or starts with '/'. These patterns would allow path traversal out of the plugin's state directory, so they are rejected as a path-safety guard.","triggerScenarios":"Calling plugin state APIs with a key like '../other-plugin', 'a/../b', '/absolute/path', or any key containing '..' after trimming. Keys are used to build state file paths, so traversal segments are forbidden.","commonSituations":"Deriving state keys from user input or file names without sanitization; using slash-prefixed keys copied from a path constant; keys built from external identifiers that legitimately contain '..' sequences.","solutions":["Sanitize keys: strip leading '/', replace '..' segments, or encode the raw identifier (e.g. base64/hex or slugify) before passing it as a state key","Use flat, slug-like keys such as 'my-plugin-session-1' derived from trusted identifiers","Validate external keys against /^[A-Za-z0-9._-]+$/ before using them as state keys"],"exampleFix":"// before\nconst key = userInput; // '../../etc/passwd'\nstate.get(key);\n\n// after\nconst key = slugify(userInput); // 'etc-passwd' with dots/traversal removed\nstate.get(key);","handlingStrategy":"validation","validationCode":"import { normalizeHookPluginStateKey } from './plugin-state.js';\n\nfunction safeStateKey(raw: string): string {\n  return normalizeHookPluginStateKey(raw.replace(/\\.\\./g, '').replace(/^\\/+/, '').replace(/[^\\w.-]+/g, '-'));\n}","typeGuard":"function isSafeStateKey(key: string): boolean {\n  const t = key.trim();\n  return t.length > 0 && !t.includes('..') && !t.startsWith('/') && /^[A-Za-z0-9._-]+$/.test(t);\n}","tryCatchPattern":"try { state.get(key); } catch (err) {\n  if ((err as Error).message === 'invalid state key') return state.get(sanitize(key));\n  throw err;\n}","preventionTips":["Never build state keys from raw user input or file paths","Slugify or encode external identifiers before using them as keys","Add a regex allow-list check in one place (key builder) rather than at each call site"],"tags":["path-traversal","security","validation","plugin-state"],"backgroundTag":"path-traversal-detected","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}