KeygraphHQ/shannon · error · PentestError

Path traversal detected in @include(): ${rawPath}

Error message

Path traversal detected in @include(): ${rawPath}

What it means

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.

Source

Thrown at apps/worker/src/services/prompt-manager.ts:260

    const errMsg = error instanceof Error ? error.message : String(error);
    throw new PentestError(`Failed to build login instructions: ${errMsg}`, 'config', false, {
      authentication,
      originalError: errMsg,
    });
  }
}

// Pure function: Process @include() directives
async function processIncludes(content: string, baseDir: string): Promise<string> {
  const includeRegex = /@include\(([^)]+)\)/g;
  const resolvedBase = path.resolve(baseDir);

  const replacements: IncludeReplacement[] = await Promise.all(
    Array.from(content.matchAll(includeRegex)).map(async (match) => {
      const rawPath = match[1] ?? '';
      const includePath = path.resolve(baseDir, rawPath);
      if (!includePath.startsWith(resolvedBase + path.sep) && includePath !== resolvedBase) {
        throw new PentestError(`Path traversal detected in @include(): ${rawPath}`, 'prompt', false, {
          includePath,
          baseDir: resolvedBase,
        });
      }
      const sharedContent = await fs.readFile(includePath, 'utf8');
      return {
        placeholder: match[0],
        content: sharedContent,
      };
    }),
  );

  for (const replacement of replacements) {
    content = replaceLiteral(content, replacement.placeholder, replacement.content);
  }
  return content;
}

View on GitHub (pinned to 1ae0a142f8)

Solutions

  1. 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)).
  2. If you genuinely need a shared partial, place it under the prompts tree (e.g. prompts/shared/) and reference it without .. segments.
  3. Remove any absolute paths or symlinks that escape the prompts directory from @include directives.
  4. Re-run the scan after fixing the template.

Example fix

// before: path escapes baseDir
//   @include(../../configs/secret.yaml)
// after: keep the partial inside the prompts tree
//   @include(shared/_secret-section.txt)
Defensive patterns

Strategy: validation

Validate before calling

// Pre-validate every @include in a prompt stays inside the prompts base dir
import { resolve, relative, sep } from 'node:path';
function includesAreSafe(content: string, baseDir: string): boolean {
  const resolvedBase = resolve(baseDir);
  for (const m of content.matchAll(/@include\(([^)]+)\)/g)) {
    const p = resolve(baseDir, m[1] ?? '');
    if (p !== resolvedBase && !p.startsWith(resolvedBase + sep)) return false;
  }
  return true;
}

Type guard

function isSafeIncludePath(rawPath: string, baseDir: string): boolean {
  const resolvedBase = resolve(baseDir);
  const p = resolve(baseDir, rawPath);
  return p === resolvedBase || p.startsWith(resolvedBase + path.sep);
}

Try / catch

try {
  await processIncludes(template, promptsDir);
} catch (e) {
  if (e instanceof PentestError && /Path traversal detected/.test(e.message)) {
    // the offending rawPath is in context.includePath — fix the prompt template, do not catch-and-continue
    throw new Error(`Unsafe @include in prompt: ${(e.context as any)?.includePath}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of KeygraphHQ/shannon@1ae0a142f8 (2026-08-12). Data as JSON: /api/errors/75739133c087071e. Report an issue: GitHub.