{"record":{"id":"b9c5bad9e2dcc4bf","repo":"ruvnet/ruflo","slug":"key-contains-disallowed-characters","errorCode":null,"errorMessage":"Key contains disallowed characters","messagePattern":"Key contains disallowed characters","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/memory-tools.ts","lineNumber":83,"sourceCode":"// #1425 — single source of truth for the dangerous-character set rejected by\n// validateMemoryInput. Imported by sanitizeMemoryKey so write-side sanitization\n// and read-side rejection can never drift apart (the symmetry bug behind #1884).\nconst DANGEROUS_KEY_CHARS = /[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[/\\\\]/g;\nconst DANGEROUS_KEY_PATTERN = /[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[/\\\\]/;\n\nfunction validateMemoryInput(key?: string, value?: string, query?: string, namespace?: string): void {\n  if (key && key.length > MAX_KEY_LENGTH) {\n    throw new Error(`Key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);\n  }\n  if (value && value.length > MAX_VALUE_SIZE) {\n    throw new Error(`Value exceeds maximum size of ${MAX_VALUE_SIZE} bytes`);\n  }\n  if (query && query.length > MAX_QUERY_LENGTH) {\n    throw new Error(`Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`);\n  }\n  // Reject path traversal and shell metacharacters in keys/namespaces (#1425)\n  if (key && DANGEROUS_KEY_PATTERN.test(key)) {\n    throw new Error('Key contains disallowed characters');\n  }\n  if (namespace && DANGEROUS_KEY_PATTERN.test(namespace)) {\n    throw new Error('Namespace contains disallowed characters');\n  }\n}\n\n// #1884 — sanitize a key produced from arbitrary input (markdown headings,\n// frontmatter names, file names) so it survives validateMemoryInput on the\n// read/delete path. Replaces every dangerous char with `_`. Truncates to\n// MAX_KEY_LENGTH so the bound check in validateMemoryInput also passes.\n// Keep this in sync with DANGEROUS_KEY_PATTERN — they share DANGEROUS_KEY_CHARS.\nfunction sanitizeMemoryKey(key: string): string {\n  const safe = key.replace(DANGEROUS_KEY_CHARS, '_');\n  return safe.length > MAX_KEY_LENGTH ? safe.slice(0, MAX_KEY_LENGTH) : safe;\n}\n\n// #1937 — minimal glob → RegExp helper for memory_import_claude exclusion\n// patterns. Anchored. Supports the three operators the issue's voice-fidelity","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/cli/src/mcp-tools/memory-tools.ts#L65-L101","documentation":"Thrown by validateMemoryInput when a memory key matches DANGEROUS_KEY_PATTERN — shell metacharacters (; & | ` $ ( ) { } [ ] < > ! # \\  null) or a path-traversal sequence (../ or ..\\). This is the #1425 hardening against command injection and path traversal: keys eventually form filesystem paths and shell arguments, so dangerous characters are rejected outright. sanitizeMemoryKey exists to scrub derived keys before this check.","triggerScenarios":"Calling memory store/retrieve/delete with a key containing characters like ';', '|', '$', backticks, '../', or null bytes. The regex test is non-global and returns true on the first match.","commonSituations":"User input or file names used directly as keys (e.g. 'my/file'); LLM-generated keys with punctuation; path-traversal attempts; copy-pasted strings with smart quotes or angle brackets; keys built by string concatenation without sanitisation.","solutions":["Sanitise derived keys with sanitizeMemoryKey before the memory call (replaces dangerous chars with '_').","Restrict keys to an allowlist charset ([A-Za-z0-9._-]).","Hash arbitrary input with sha256 and use the hex digest as the key.","Never route raw user/LLM text into a key without scrubbing."],"exampleFix":"// before\nmemory store --key \"a;b | rm -rf\" --value \"x\"\n// after\nconst key = userInput.replace(/[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[\\/\\\\]/g, '_')\nmemory store --key \"$key\" --value \"x\"","handlingStrategy":"validation","validationCode":"const DANGEROUS = /[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[\\/\\\\]/;\nfunction sanitizeKey(key) {\n  if (DANGEROUS.test(key)) {\n    return key.replace(/[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[\\/\\\\]/g, '_');\n  }\n  return key;\n}","typeGuard":"function isSafeMemoryKey(k: string): boolean {\n  return !/[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[\\/\\\\]/.test(k) && k.length <= 1024;\n}","tryCatchPattern":null,"preventionTips":["Restrict keys to [A-Za-z0-9._-].","Hash arbitrary input before use as a key.","Never pass raw user/LLM text as a key."],"tags":["memory","security","injection","path-traversal","validation","input-validation"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}