{"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":null,"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/9c61c86f06b439af2a95085ae9bb0ca839662e41/v3/@claude-flow/cli/src/mcp-tools/memory-tools.ts#L65-L101","documentation":"validateMemoryInput rejects keys containing shell metacharacters or traversal sequences via DANGEROUS_KEY_PATTERN: ; & | ` $ ( ) { } [ ] < > ! # \\ and NUL, plus ../ or ..\\ (#1425 — anti shell-injection and path-traversal for the memory backends). The key fails before anything is written or read. Spaces, unicode, /, and : are allowed by this particular check — only the listed metacharacters and dot-dot-slash trigger it.","triggerScenarios":"Keys derived from markdown headings ('## Overview' contains #), code symbols ('resize(width)' contains parentheses), template placeholders ('user$name'), URLs with query strings ('a?x=1&y=2' contains &), or path-like keys ('../etc/passwd', 'a\\..\\b').","commonSituations":"Auto-keying memory entries from headings, function signatures, or pasted prose; LLM-generated keys that copy punctuation; migrating from a store that accepted arbitrary strings; the library's own read path hits this too, which is why it ships sanitizeMemoryKey to strip these characters.","solutions":["Sanitize before the call with the same replacement the library uses: key.replace(/[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[/\\\\]/g, '_')","Generate keys through a slug function restricted to [A-Za-z0-9_-]","For markdown-derived keys, strip leading # and punctuation before storing","If the original string must survive verbatim, hash it into the key and keep the original inside the value"],"exampleFix":"// before\nconst key = `## ${heading} (${section})`; // contains # ( ) -> Key contains disallowed characters\nawait mcp.callTool('memory_store', { key, value });\n\n// after — mirror sanitizeMemoryKey\nconst safeKey = heading.replace(/[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[/\\\\]/g, '_').slice(0, 1024);\nawait mcp.callTool('memory_store', { key: safeKey, value });","handlingStrategy":"validation","validationCode":"// Same dangerous-character set the library enforces (memory-tools DANGEROUS_KEY_CHARS).\nconst DANGEROUS_KEY_CHARS = /[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[/\\\\]/g;\nfunction sanitizeMemoryKey(key: string, maxLen = 1024): string {\n  const safe = key.replace(DANGEROUS_KEY_CHARS, '_');\n  return safe.length > maxLen ? safe.slice(0, maxLen) : safe;\n}\n// const key = sanitizeMemoryKey(rawHeadingOrSymbol);","typeGuard":"function isSafeMemoryKey(key: string): boolean {\n  return key.length <= 1024 && !/[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[/\\\\]/.test(key);\n}","tryCatchPattern":"try {\n  await memoryStore({ key, value });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('Key contains disallowed characters')) {\n    // sanitize and retry once: key = key.replace(/[;&|`$(){}[\\]<>!#\\\\\\0]|\\.\\.[/\\\\]/g, '_')\n  }\n  throw e;\n}","preventionTips":["Generate keys through a slug/allowlist function ([A-Za-z0-9_-]) instead of pasting raw prose, headings, or URLs","Strip markdown '#' prefixes and punctuation before keying from headings","Keep the sanitizer in sync with the library's set: ; & | ` $ ( ) { } [ ] < > ! # \\ NUL and ../","Add a unit test that round-trips your key generator through the same dangerous-character regex"],"tags":["memory","mcp","security","validation","injection","key"],"backgroundTag":"invalid-identifier-characters","analyzedSha":"9c61c86f06b439af2a95085ae9bb0ca839662e41","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}