{"record":{"id":"86893b0a56bbb3c5","repo":"ruvnet/ruflo","slug":"invalid-git-ref-suspicious-pattern","errorCode":null,"errorMessage":"Invalid git ref: suspicious pattern","messagePattern":"Invalid git ref: suspicious pattern","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/ruvector/diff-classifier.ts","lineNumber":378,"sourceCode":"// ============================================================================\n\n// Cache for diff results (TTL-based)\nconst diffCache = new Map<string, { files: DiffFile[]; timestamp: number }>();\nconst CACHE_TTL_MS = 5000; // 5 seconds - short TTL since diffs change frequently\n\n/**\n * Validate git ref to prevent command injection\n * Only allows safe characters: alphanumeric, -, _, /, ., ~, ^\n */\nfunction validateGitRef(ref: string): void {\n  // Block shell metacharacters and dangerous patterns\n  if (!/^[a-zA-Z0-9_\\-./~^@]+$/.test(ref)) {\n    throw new Error(`Invalid git ref: contains unsafe characters`);\n  }\n  // Block multiple dots (path traversal)\n  if (ref.includes('..') && !ref.match(/^[a-zA-Z0-9_\\-]+\\.\\.\\.?[a-zA-Z0-9_\\-]+$/)) {\n    if (!/^\\w+\\.\\.[.\\w]+$/.test(ref)) {\n      throw new Error(`Invalid git ref: suspicious pattern`);\n    }\n  }\n  // Max length check\n  if (ref.length > 256) {\n    throw new Error(`Invalid git ref: too long`);\n  }\n}\n\n/**\n * Get git diff statistics using SINGLE combined command (optimized)\n * Replaces two separate git commands with one\n */\nexport function getGitDiffNumstat(ref: string = 'HEAD'): DiffFile[] {\n  // SECURITY: Validate git ref to prevent command injection\n  validateGitRef(ref);\n\n  // Check cache first\n  const cacheKey = `numstat:${ref}`;","sourceCodeStart":360,"sourceCodeEnd":396,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/ruvector/diff-classifier.ts#L360-L396","documentation":"The second stage of validateGitRef(): the ref already passed the charset check but contains '..' and does not match the narrow range syntaxes the guard permits (side \\w+\\..[.\\w]+ forms and simple A..B / A...B ranges of alphanumerics, underscores and hyphens). It exists to block path traversal and malformed range arguments before git is invoked.","triggerScenarios":"Passing a range whose sides contain slashes, e.g. 'refs/heads/main..refs/heads/feature'; chained dots like 'a..b..c'; trailing/leading dots like 'main..'; a path-like string '..' or '../..' that got past the charset check; refspecs with '..' embedded in a longer path.","commonSituations":"Comparing full refnames (refs/heads/...) instead of short names; constructing ranges by string concatenation without validation; tools forwarding directory-ish user input into diff stats; traversal payloads aimed at the git subprocess.","solutions":["Use short names for ranges: 'main..feature' or 'main...feature' (letters, digits, _ and - only on each side)","Resolve each side to a full SHA with git rev-parse and pass 'SHA1..SHA2'","Reject any ref containing '..' at your input boundary unless it matches /^\\w+\\.\\.[.\\w]+$/","Never build refs from filesystem paths or free user text"],"exampleFix":"// before\nconst files = getGitDiffNumstat('refs/heads/main..refs/heads/feature'); // throws: suspicious pattern\n// after\nimport { execSync } from 'node:child_process';\nconst a = execSync('git rev-parse --verify refs/heads/main').toString().trim();\nconst b = execSync('git rev-parse --verify refs/heads/feature').toString().trim();\nconst files = getGitDiffNumstat(`${a}..${b}`); // SHA..SHA passes","handlingStrategy":"validation","validationCode":"function isSafeRangeRef(ref: string): boolean {\n  if (!/^[a-zA-Z0-9_\\-./~^@]+$/.test(ref)) return false;\n  if (ref.includes('..') && !/^\\w+\\.\\.[.\\w]+$/.test(ref)) return false;\n  return true;\n}","typeGuard":"function asRangeRef(a: string, b: string): string {\n  if (!/^\\w+$/.test(a) || !/^\\w+$/.test(b)) throw new Error('range sides must be plain names or SHAs');\n  return `${a}..${b}`;\n}","tryCatchPattern":"try {\n  files = getGitDiffNumstat(ref);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('suspicious pattern')) {\n    // rebuild the range from resolved SHAs and retry once\n    files = getGitDiffNumstat(`${shaA}..${shaB}`);\n  } else throw e;\n}","preventionTips":["Build '..' ranges only from short branch names or 40-char SHAs — never from refs/heads/... full names","Resolve both sides to SHAs before constructing range strings","Reject any input containing '..' unless it matches the narrow range shape"],"tags":["git","security","input-validation","path-traversal","diff"],"backgroundTag":"invalid-git-ref","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}