honojs/hono · critical · Error
Path traversal detected: "${filePath}" is outside the output
Error message
Path traversal detected: "${filePath}" is outside the output directory What it means
A security check in the SSG utils: before writing a generated page to disk, it verifies the resolved output file path is still inside the configured output directory. If the route path resolves outside outDir — via ../ segments, absolute paths, or mismatched path roots (e.g. Windows drive vs UNC) — it throws rather than writing outside the directory.
Source
Thrown at src/helper/ssg/utils.ts:116
}
export const ensureWithinOutDir = (outDir: string, filePath: string): void => {
const outDirSegments = toSegments(joinPaths(outDir))
const filePathSegments = toSegments(joinPaths(filePath))
const hasMismatchedPathRoot = getPathRoot(outDir) !== getPathRoot(filePath)
// `joinPaths` collects every remaining `..` at the head, so a `..` right after
// the outDir segments means the file path climbs above outDir
const climbsAboveOutDir = filePathSegments[outDirSegments.length] === '..'
if (
hasMismatchedPathRoot ||
filePathSegments.length <= outDirSegments.length ||
!outDirSegments.every((segment, i) => segment === filePathSegments[i]) ||
climbsAboveOutDir
) {
throw new Error(`Path traversal detected: "${filePath}" is outside the output directory`)
}
}
View on GitHub (pinned to e2740d5a1b)
Solutions
- Sanitize route segments before generating pages: strip '..', leading slashes, and decode-then-validate slugs
- Pass a clean absolute outDir to toSSG and ensure generated paths are joined relative to it
- If intentional nesting is needed, configure outDir to a common parent containing all outputs
- Add a check comparing path.resolve(outDir, filePath) prefix with path.resolve(outDir) before calling the SSG writer
Example fix
// before
app.get('/docs/:slug', (c) => c.html(render(c.req.param('slug'))))
// slug = '..%2F..%2Fetc' -> Path traversal detected
// after
app.get('/docs/:slug', (c) => {
const slug = c.req.param('slug').replace(/\.\./g, '').replace(/^[\/\\]+/, '')
if (!slug) return c.notFound()
return c.html(render(slug))
}) Defensive patterns
Strategy: validation
Validate before calling
import path from 'node:path'
function isWithinOutDir(outDir: string, filePath: string): boolean {
const rel = path.relative(path.resolve(outDir), path.resolve(outDir, filePath))
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel)
} Type guard
const isSafeSegment = (s: string): boolean => /^[\w.-]+$/.test(s) && !s.includes('..') Try / catch
try { await toSSG(app, fs, { outDir }) } catch (e) { if (e instanceof Error && e.message.includes('Path traversal detected')) { /* sanitize slugs/routes and re-run */ } throw e } Prevention
- Sanitize dynamic route params: reject/strip '..' and absolute prefixes before using them as filenames
- Use simple slugs (id/hash) as file names, never raw user input
- Keep outDir absolute and consistent; verify generated path prefixes on Windows
When it happens
Trigger: A route whose path (after joining with outDir and normalizing) escapes the output directory: paths containing encoded ../, routes generating absolute file names, or outDir/root mismatch where segment prefixes don't align (filePathSegments not starting with outDirSegments, or fewer segments than outDir).
Common situations: Dynamic routes using user-supplied or URL-decoded segments (e.g. /docs/:slug with slug='../..'); passing a wrong (relative vs absolute, trailing-slash) outDir to toSSG; Windows path-root mismatches between outDir and generated file path; attempts/scan payloads targeting the SSG writer.
Related errors
- Error processing response: ${error instanceof Error ? error.
- token(${token}) signature mismatched
- symmetric algorithm "${alg}" is not allowed for JWK verifica
- algorithm "${alg}" is not in the allowed list: [${allowedAlg
AI-assisted analysis of honojs/hono@e2740d5a1b (2026-08-28).
Data as JSON: /api/errors/5c39868e917473e9.
Report an issue: GitHub.