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

  1. Sanitize route segments before generating pages: strip '..', leading slashes, and decode-then-validate slugs
  2. Pass a clean absolute outDir to toSSG and ensure generated paths are joined relative to it
  3. If intentional nesting is needed, configure outDir to a common parent containing all outputs
  4. 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

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


AI-assisted analysis of honojs/hono@e2740d5a1b (2026-08-28). Data as JSON: /api/errors/5c39868e917473e9. Report an issue: GitHub.