Crosstalk-Solutions/project-nomad · warning · Error
Invalid document slug
Error message
Invalid document slug
What it means
Path-traversal guard in DocsService.parseFile: the requested filename is joined onto docsPath, resolved, and required to stay strictly under the resolved base directory (basePath + path.sep prefix). Any slug that escapes the docs directory (via .., absolute paths, or symlinked resolution tricks) throws 'Invalid document slug'.
Source
Thrown at admin/app/services/docs_service.ts:77
} catch (error) {
logger.error('Error parsing Markdoc content:', error)
throw new InternalServerErrorException(`Error parsing content: ${(error as Error).message}`)
}
}
async parseFile(_filename: string) {
try {
if (!_filename) {
throw new Error('Filename is required')
}
const filename = _filename.endsWith('.md') ? _filename : `${_filename}.md`
// Prevent path traversal — resolved path must stay within the docs directory
const basePath = path.resolve(this.docsPath)
const fullPath = path.resolve(path.join(this.docsPath, filename))
if (!fullPath.startsWith(basePath + path.sep)) {
throw new Error('Invalid document slug')
}
const fileExists = await getFileStatsIfExists(fullPath)
if (!fileExists) {
throw new Error(`File not found: ${filename}`)
}
const fileStream = await getFile(fullPath, 'stream')
if (!fileStream) {
throw new Error(`Failed to read file stream: ${filename}`)
}
const content = await streamToString(fileStream)
return this.parse(content)
} catch (error) {
throw new InternalServerErrorException(`Error parsing file: ${(error as Error).message}`)
}
}
View on GitHub (pinned to 0bd1c6f4f9)
Solutions
- Reject the request at the API layer — this is a security guard, do not bypass it
- Sanitize slugs: allow only [a-z0-9-] segments before calling parseFile
- If a legitimate nested doc fails, confirm the file truly lives under docsPath and the slug uses correct relative separators
- Never try to work around by stripping the check; fix the caller's path construction
Example fix
// before
await docs.parseFile(decodeURIComponent(req.query.slug)) // '../../config'
// after
const slug = req.query.slug as string
if (!/^[a-z0-9\/-]+$/i.test(slug) || slug.includes('..')) {
throw new BadRequestException('Invalid slug')
}
await docs.parseFile(slug) Defensive patterns
Strategy: validation
Validate before calling
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*$/
if (!SLUG_RE.test(slug)) throw new BadRequestException('Invalid slug')
await docsService.parseFile(slug) // cannot traverse Type guard
const isSafeDocSlug = (slug: string): boolean =>
!slug.includes('..') && !path.isAbsolute(slug) && SLUG_RE.test(slug) Try / catch
try {
await docsService.parseFile(slug)
} catch (e) {
if ((e as Error).message === 'Invalid document slug')
return res.status(400).json({ error: 'Invalid document slug' })
throw e
} Prevention
- Never pass raw decoded query/path segments as filenames
- Whitelist slug characters at the API boundary
- Treat this error as a security signal: log and audit occurrences, don't just 404
When it happens
Trigger: parseFile('../../etc/passwd'), parseFile('../..\\..\\secret'), a slug containing a leading slash making it absolute on POSIX, or any resolved path equal to or outside the docs root (basePath itself without a trailing separator is also rejected).
Common situations: User-supplied slugs passed unsanitized from query/path params, URL-encoded %2e%2e%2f sequences that decode before resolution, testing with slugs like '.' or empty-ish values that resolve to the base dir itself.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Filename is required
- Invalid world basemap path
- File not found: ${filename}
- Failed to read file stream: ${filename}
- Error parsing file: ${(error as Error).message}
AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27).
Data as JSON: /api/errors/76116ef93fd82c98.
Report an issue: GitHub.