{"record":{"id":"576086c1ba0ed0fe","repo":"ruvnet/ruflo","slug":"empty-prefixes","errorCode":"EMPTY_PREFIXES","errorMessage":"At least one allowed prefix must be specified","messagePattern":"At least one allowed prefix must be specified","errorType":"exception","errorClass":"PathValidatorError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/security/src/path-validator.ts","lineNumber":264,"sourceCode":"   * every call would hand that control to whoever can write the link, and cost\n   * a syscall per validation. Callers needing to follow a moved target should\n   * construct a new validator.\n   */\n  private readonly canonicalPrefixes: string[];\n\n  constructor(config: PathValidatorConfig) {\n    this.config = {\n      allowedPrefixes: config.allowedPrefixes,\n      blockedExtensions: config.blockedExtensions ?? DEFAULT_BLOCKED_EXTENSIONS,\n      blockedNames: config.blockedNames ?? DEFAULT_BLOCKED_NAMES,\n      maxPathLength: config.maxPathLength ?? 4096,\n      resolveSymlinks: config.resolveSymlinks ?? true,\n      allowNonExistent: config.allowNonExistent ?? true,\n      allowHidden: config.allowHidden ?? false,\n    };\n\n    if (this.config.allowedPrefixes.length === 0) {\n      throw new PathValidatorError(\n        'At least one allowed prefix must be specified',\n        'EMPTY_PREFIXES'\n      );\n    }\n\n    // Pre-resolve all prefixes, lexically and canonically. #3010 — validate()\n    // canonicalizes the *candidate* through fs.realpath (when resolveSymlinks\n    // is on, the default) but prefixes were only ever path.resolve()d, never\n    // realpath'd. On any platform where the prefix itself is reached through a\n    // symlink (e.g. macOS os.tmpdir() -> /var/folders/... while /var is itself\n    // a symlink to /private/var), the realpath'd candidate can never match the\n    // non-realpath'd prefix, and every path under that prefix is rejected as\n    // \"outside allowed directories\" — including the prefix's own contents.\n    this.resolvedPrefixes = this.config.allowedPrefixes.map(p =>\n      path.resolve(p)\n    );\n    // Always a distinct array — `addPrefix` appends to both, so aliasing the\n    // lexical list would push twice. With resolveSymlinks off no candidate is","sourceCodeStart":246,"sourceCodeEnd":282,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/security/src/path-validator.ts#L246-L282","documentation":"PathValidator's constructor is the one config surface with no default: allowedPrefixes must be a non-empty array. Everything else (blockedExtensions, blockedNames, maxPathLength, resolveSymlinks, allowNonExistent, allowHidden) falls back to sane defaults, but an empty prefix list throws PathValidatorError EMPTY_PREFIXS because a validator that permits nothing is almost certainly a config bug.","triggerScenarios":"new PathValidator({ allowedPrefixes: [] }); building prefixes from an env var that is unset (config.allowedPrefixes = (process.env.ALLOWED_DIRS || '').split(':') yields [''] or [''] after filtering); filtering a configured list down to zero entries before construction.","commonSituations":"Per-tenant workspace roots resolved from a database column that is NULL; deployment where the sandbox dirs env var was never set; refactoring that moved prefix computation after construction order changed.","solutions":["Pass at least one absolute, real directory: new PathValidator({ allowedPrefixes: [process.cwd()] }) or [os.tmpdir()].","Fail fast at config load: if the env/config source yields zero prefixes, abort startup with a clear message instead of constructing the validator.","Remember prefixes are canonicalized with realpath at construction (see #3010 note), so pass the symlink-resolved path on platforms like macOS (/private/var not /var).","If 'deny everything' is truly intended, skip creating a PathValidator rather than passing an empty array."],"exampleFix":"// before\nconst dirs = process.env.ALLOWED_DIRS?.split(',').filter(Boolean) ?? [];\nconst validator = new PathValidator({ allowedPrefixes: dirs }); // throws EMPTY_PREFIXS when unset\n\n// after\nconst dirs = process.env.ALLOWED_DIRS?.split(',').map((d) => d.trim()).filter(Boolean);\nif (!dirs?.length) throw new Error('ALLOWED_DIRS must list at least one directory');\nconst validator = new PathValidator({ allowedPrefixes: await Promise.all(dirs.map((d) => fs.realpath(d))) });","handlingStrategy":"validation","validationCode":"if (!Array.isArray(prefixes) || prefixes.length === 0) {\n  throw new Error(\n    `PathValidator requires >= 1 allowed prefix, got: ${JSON.stringify(prefixes)}`\n  );\n}\nconst validator = new PathValidator({ allowedPrefixes: prefixes });","typeGuard":"function hasAllowedPrefixes(cfg: unknown): cfg is { allowedPrefixes: string[] } {\n  return (\n    !!cfg && typeof cfg === 'object' &&\n    Array.isArray((cfg as any).allowedPrefixes) &&\n    (cfg as any).allowedPrefixes.length > 0 &&\n    (cfg as any).allowedPrefixes.every((p: unknown) => typeof p === 'string' && path.isAbsolute(p))\n  );\n}","tryCatchPattern":"try {\n  return new PathValidator(config);\n} catch (err) {\n  if (err instanceof PathValidatorError && err.code === 'EMPTY_PREFIXES') {\n    failStartup('allowedPrefixes is empty — set WORKSPACE_DIRS');\n  }\n  throw err;\n}","preventionTips":["Validate the prefix list at config load and abort startup loudly instead of constructing a broken validator.","Build prefixes with filter(Boolean) after splitting env vars so empty strings never reach the constructor.","Remember the constructor realpath's prefixes; pass resolved directories to avoid later VALIDATION_FAILED surprises."],"tags":["path","security","constructor","validation"],"backgroundTag":"path-traversal-guard-misconfigured","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}