{"record":{"id":"75739133c087071e","repo":"KeygraphHQ/shannon","slug":"path-traversal-detected-in-include-rawpath","errorCode":null,"errorMessage":"Path traversal detected in @include(): ${rawPath}","messagePattern":"Path traversal detected in @include\\(\\): (.+?)","errorType":"validation","errorClass":"PentestError","httpStatus":null,"severity":"error","filePath":"apps/worker/src/services/prompt-manager.ts","lineNumber":260,"sourceCode":"    const errMsg = error instanceof Error ? error.message : String(error);\n    throw new PentestError(`Failed to build login instructions: ${errMsg}`, 'config', false, {\n      authentication,\n      originalError: errMsg,\n    });\n  }\n}\n\n// Pure function: Process @include() directives\nasync function processIncludes(content: string, baseDir: string): Promise<string> {\n  const includeRegex = /@include\\(([^)]+)\\)/g;\n  const resolvedBase = path.resolve(baseDir);\n\n  const replacements: IncludeReplacement[] = await Promise.all(\n    Array.from(content.matchAll(includeRegex)).map(async (match) => {\n      const rawPath = match[1] ?? '';\n      const includePath = path.resolve(baseDir, rawPath);\n      if (!includePath.startsWith(resolvedBase + path.sep) && includePath !== resolvedBase) {\n        throw new PentestError(`Path traversal detected in @include(): ${rawPath}`, 'prompt', false, {\n          includePath,\n          baseDir: resolvedBase,\n        });\n      }\n      const sharedContent = await fs.readFile(includePath, 'utf8');\n      return {\n        placeholder: match[0],\n        content: sharedContent,\n      };\n    }),\n  );\n\n  for (const replacement of replacements) {\n    content = replaceLiteral(content, replacement.placeholder, replacement.content);\n  }\n  return content;\n}\n","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/KeygraphHQ/shannon/blob/1ae0a142f8525410a688f0309fd003cc5b1d92de/apps/worker/src/services/prompt-manager.ts#L242-L278","documentation":"Thrown by processIncludes when an @include(rawPath) directive in a prompt template resolves to a path outside the allowed base directory. The check resolves rawPath against baseDir and requires the result to either equal resolvedBase or start with resolvedBase + path.sep, blocking directory traversal. This is a security guard preventing prompt files from reading arbitrary files. Category 'prompt', non-retryable.","triggerScenarios":"A prompt template contains an include that escapes the prompts tree: @include(../secrets.env), @include(../../etc/passwd), @include(/absolute/elsewhere), or on Windows a drive/path that resolves outside baseDir. Any user-supplied or edited prompt that uses .. segments or an absolute path in an @include() directive.","commonSituations":"An operator edits a prompt under apps/worker/prompts/ and adds @include(../shared/login-instructions.txt) thinking the path is relative to the file rather than baseDir (processIncludes resolves relative to baseDir, not the including file). A symlink inside the prompts dir points outside it. A custom prompt accidentally includes an absolute path.","solutions":["Open the offending prompt and rewrite the @include path to be relative to the prompts base directory and stay within it (e.g. @include(shared/login-instructions.txt)).","If you genuinely need a shared partial, place it under the prompts tree (e.g. prompts/shared/) and reference it without .. segments.","Remove any absolute paths or symlinks that escape the prompts directory from @include directives.","Re-run the scan after fixing the template."],"exampleFix":"// before: path escapes baseDir\n//   @include(../../configs/secret.yaml)\n// after: keep the partial inside the prompts tree\n//   @include(shared/_secret-section.txt)","handlingStrategy":"validation","validationCode":"// Pre-validate every @include in a prompt stays inside the prompts base dir\nimport { resolve, relative, sep } from 'node:path';\nfunction includesAreSafe(content: string, baseDir: string): boolean {\n  const resolvedBase = resolve(baseDir);\n  for (const m of content.matchAll(/@include\\(([^)]+)\\)/g)) {\n    const p = resolve(baseDir, m[1] ?? '');\n    if (p !== resolvedBase && !p.startsWith(resolvedBase + sep)) return false;\n  }\n  return true;\n}","typeGuard":"function isSafeIncludePath(rawPath: string, baseDir: string): boolean {\n  const resolvedBase = resolve(baseDir);\n  const p = resolve(baseDir, rawPath);\n  return p === resolvedBase || p.startsWith(resolvedBase + path.sep);\n}","tryCatchPattern":"try {\n  await processIncludes(template, promptsDir);\n} catch (e) {\n  if (e instanceof PentestError && /Path traversal detected/.test(e.message)) {\n    // the offending rawPath is in context.includePath — fix the prompt template, do not catch-and-continue\n    throw new Error(`Unsafe @include in prompt: ${(e.context as any)?.includePath}`);\n  }\n  throw e;\n}","preventionTips":["Never accept untrusted user input inside @include directives; treat prompts as trusted code.","Reference shared partials relative to the prompts base dir, not the including file.","Avoid absolute paths and '..' segments in @include().","Audit custom prompts for traversal patterns before mounting them into the worker."],"tags":["prompt","security","path-traversal","validation","include"],"backgroundTag":null,"analyzedSha":"1ae0a142f8525410a688f0309fd003cc5b1d92de","analyzedAt":"2026-08-12T17:40:03.583Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}