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

  1. Reject the request at the API layer — this is a security guard, do not bypass it
  2. Sanitize slugs: allow only [a-z0-9-] segments before calling parseFile
  3. If a legitimate nested doc fails, confirm the file truly lives under docsPath and the slug uses correct relative separators
  4. 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

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


AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27). Data as JSON: /api/errors/76116ef93fd82c98. Report an issue: GitHub.