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.
Summary
When
MD_ALLOWED_PATHSorMD_SHARE_DIRis 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 withrealpathbefore 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-server1.1.0, which is also the current npm version at the time of reporting.Affected code paths
src/utils.ts:assertPathAllowed()andisWithinDirectory()authorize using normalized path strings andstartsWith().src/Markdownify.ts:Markdownify.get()callsassertPathAllowed()and then reads the original path withfs.promises.readFile().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/allowed2as 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.