Skip to content

Path allowlist can be bypassed through symlinked files #108

Description

@3em0

Summary

When MD_ALLOWED_PATHS or MD_SHARE_DIR is configured, local file tools are expected to reject paths outside the configured directories. The current path authorization check is lexical: it normalizes/resolves the requested path and then checks whether it starts with an allowed directory string. It does not canonicalize the requested file or allowed root with realpath before authorization.

As a result, a path that appears to be inside an allowed directory can point to a file outside that directory through a symlink. The check accepts the allowed-looking path, and later file reads/conversion follow the symlink.

Affected version

Observed in mcp-markdownify-server 1.1.0, which is also the current npm version at the time of reporting.

Affected code paths

  • src/utils.ts: assertPathAllowed() and isWithinDirectory() authorize using normalized path strings and startsWith().
  • src/Markdownify.ts: Markdownify.get() calls assertPathAllowed() and then reads the original path with fs.promises.readFile().
  • File conversion paths that call Markdownify.toMarkdown({ filePath }) use the same authorization helper before passing the path to the converter.

Impact

This can bypass the intended read boundary enforced by MD_ALLOWED_PATHS. In deployments where the allowed directory is writable or influenced by less-trusted users, an attacker may be able to read files outside the allowlisted directory that are readable by the server process.

The issue does not bypass operating-system file permissions. Practical severity depends on deployment: it is higher for shared/upload/CI/container directories and lower when the allowlisted directory is trusted and read-only.

Additional boundary issue

The use of startsWith() can also confuse sibling paths, for example treating a path under /tmp/allowed2 as inside /tmp/allowed. A path-boundary-aware comparison would avoid this class of issue.

Suggested fix direction

Canonicalize both the requested path and each allowed root before authorization, then compare using path-boundary-aware logic such as path.relative() with separator handling. Consider TOCTOU-resistant handling for files that are opened after the check, and decide whether symlinks inside allowlisted directories should be rejected or allowed only when their canonical target is still inside an allowed root.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions