Repository navigation
chore(deps): update dependency nltk to v3.10.3 [security] - autoclosed - #62
Closed
renovate[bot] wants to merge 1 commit into
Closed
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
renovate
Bot
force-pushed
the
renovate/pypi-nltk-vulnerability
branch
from
September 7, 2026 18:06
78f6bea to
8581cf0
Compare
renovate
Bot
force-pushed
the
renovate/pypi-nltk-vulnerability
branch
from
September 15, 2026 13:42
8581cf0 to
84a969c
Compare
pplmx
added a commit
that referenced
this pull request
Oct 11, 2026
…ckler RCE + CVE-2026-81723) Locked nltk was 3.10.1 (transitive via sacrebleu, eval extra). The prior bump only reached 3.10.1, missing two newer CVEs: - CVE-2026-71513 / GHSA-5gh2-94qg-qppq: AllowlistUnpickler validates the pickle module string but not the global name, enabling dotted-name resolution into arbitrary code execution. - CVE-2026-81723: inefficient algorithmic complexity (< 3.10.3). Re-locked (nltk 3.10.3, minimal 8-line diff preserving the tuna mirror). Evaluator suite (129 tests) passes on 3.10.3. Renovate PR #62 was not adopted: it regenerates the whole lock against pypi.org (1918-line rewrite discarding the project's mirrors.tuna index) rather than a clean security bump.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
3.10.1→3.10.3NLTK: Uncontrolled search path when invoking the Graphviz 'dot' binary
CVE-2026-78680 / GHSA-6hwm-xvph-95vm
More information
Details
Two NLTK sites executed the Graphviz
dotprogram by bare name, so process creation resolved it via the search path — and on Windows via the current working directory — rather than a validated absolute location. An attacker who can place a file nameddotwhere resolution looks (the CWD on Windows, or a writable/relative entry such as.onPATH) has their binary executed in place of Graphviz (arbitrary code execution).Affected (<= 3.10.2):
nltk.parse.dependencygraph.dot2img— calledfind_binary("dot")but discarded the returned validated path and then ran the bare name["dot", ...], so the validation had no effect.nltk.translate.api.AlignedSent._repr_svg_— ran the bare name with no validation at all (IPython SVG rendering).This is the same class already fixed for the senna, weka, boxer, malt, repp and hunpos wrappers.
nltk.internals.find_binaryrefuses a CWD-relative match for a bare tool name and returns only a trusted absolute path; the fix runs that path in both sites.Attack demonstration
Captured output, not illustrative. A
./dotthat writes aPWNEDmarker, planted in the CWD with.prepended toPATH.The vulnerable behaviour (old bare-name exec):
The patched functions refuse it:
And
find_binaryitself was attacked directly (the fix trusts nothing else):Attack 4 is out of scope: trusting an absolute directory that is already on
PATHis the operating system's own trust model — an attacker who can write to aPATHdirectory owns the account regardless of NLTK.find_binarydefends specifically against the CWD/relative injection that bare-name exec is vulnerable to (attacks 1–3), which is exactly what this fix inherits.Environment: python 3.13.7.
dotis not required to reproduce — the planted binary is the payload.Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK: JVM argument injection bypass via per-call options in the NLTK Stanford wrappers (incomplete fix of CVE-2026-12841)
CVE-2026-79675 / GHSA-m4rf-3fr8-xwx3
More information
Details
Vulnerability
The fix for CVE-2026-12841 (CWE-88, JVM argument injection) added
_validate_java_options()to block dangerous JVM flags such as-agentlib,-agentpath,-javaagent,-Xrunjdwp, and@argfilereferences. However, the validation is only applied when setting global options viaconfig_java(). Thejava()function's per-calloptionsparameter -- added by PR #3683 (CVE-2026-12615 fix) -- passes options directly tosubprocess.Popenwithout calling_validate_java_options().All four Stanford Java wrapper classes accept user-supplied
java_optionsand route them through the unvalidated per-call path, bypassing the CVE-2026-12841 fix entirely.Root Cause
In
nltk/internals.py, thejava()function (line 128) accepts anoptionskeyword argument. Whenoptionsis not None, it is converted to a list and prepended to the JVM command (lines 211-217) without any validation:Compare with
config_java()(line 92) which does validate:The four affected wrapper classes store user-supplied
java_optionswithout validation and pass them through the unvalidated per-call path:GenericStanfordParser(nltk/parse/stanford.py): constructor parameter at line 39, stored at line 78, passed at lines 247 and 256StanfordTagger(nltk/tag/stanford.py): constructor parameter at line 51, stored at line 79, passed at line 118StanfordTokenizer(nltk/tokenize/stanford.py): constructor parameter at line 43, stored at line 66, passed at line 109StanfordSegmenter(nltk/tokenize/stanford_segmenter.py): constructor parameter at line 68, stored at line 117, passed at line 337Proof of Concept
Impact
An attacker who controls the
java_optionsparameter to any NLTK Stanford wrapper class can inject arbitrary JVM flags, including:-agentpath:/path/to/malicious.so-- loads a native agent, achieving arbitrary code execution-javaagent:/path/to/malicious.jar-- loads a Java agent for bytecode manipulation-agentlib:jdwp=transport=dt_socket,server=y,address=*:5005-- enables remote debugging, allowing remote code execution@/path/to/argfile-- expands an argument file, which can smuggle any of the aboveThis is exploitable in scenarios where NLTK is deployed as a service and
java_optionsis derived from user input, configuration files, or environment variables. The PR #3647 commit message explicitly states the fix was intended to cover "StanfordSegmenter, and GenericStanfordParser" but the implementation only validates inconfig_java().Suggested Fix
Add
_validate_java_options()to thejava()function's per-call options handling:This single-line addition closes the bypass for all four Stanford wrapper classes and any future callers of
java(options=...).AI tooling
AI assistance was used for the code audit and for drafting this report. The finding were manually verified against the project's source at the location cited above before reporting it, and the severity and impact assessment are the reporters.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK: Uncontrolled recursion in nltk.featstruct.FeatStructReader causes unhandled RecursionError (DoS) via deeply nested feature-structure input
CVE-2026-81724 / GHSA-cw6x-m8jw-qmrh
More information
Details
Summary
nltk.featstruct.FeatStructReader(used byFeatStruct(str)and byFeatureGrammar.fromstring()) parses feature-structure strings such as[a=1]with a recursive-descent parser that has no nesting-depth limit. A small, trivially-crafted input (~700 bytes) with deeply nested brackets drives the parser past Python's recursion limit and raises an unhandledRecursionErrorinstead of the library's normal, catchableValueError/LogicalExpressionException. Any application that parses user-supplied feature-structure or feature-grammar text (e.g. NLP teaching tools, grammar "playgrounds", unification-grammar-based NLU pipelines) can be crashed by an unauthenticated input with no special privileges. This is a Denial of Service issue (CWE-674, Uncontrolled Recursion), not a memory-safety or code-execution issue.This appears to be the same bug class as two issues already fixed elsewhere in the codebase —
nltk/jsontags.py(JSONTaggedDecoder.decode_obj, guarded byMAX_DECODE_DEPTH = 200) andnltk/sem/logic.py(LogicParser, guarded byMAX_PARSE_DEPTH = 200) — butnltk/featstruct.pydoes not have an equivalent guard.Details
The recursive call chain (current
developbranch,nltk/featstruct.py):FeatStructReader.fromstring()(featstruct.py:2184) callsread_partial()→_read_partial()(featstruct.py:2250)._read_partial()dispatches to_read_partial_featdict(), which calls_read_value()(featstruct.py:2436) for each feature's value._read_value()callsread_value()(featstruct.py:2442), which matches the value againstVALUE_HANDLERS(featstruct.py:2478).[(a nested feature structure), the matched handler isread_fstruct_value(featstruct.py:2479, defined atfeatstruct.py:2495):read_partial()again, which re-enters_read_partial()— the same function from step 1.This closes a recursive cycle (
_read_partial → _read_value → read_value → read_fstruct_value → read_partial → _read_partial → ...) with no depth counter, noMAX_*_DEPTHconstant, and notry/except RecursionErroranywhere in the class. Each additional[in the input adds one more full cycle of Python stack frames. Once the input nests deeply enough, Python's own recursion-limit protection fires and raisesRecursionError, which is not a subclass ofValueError(the exception type this parser's own_error()helper raises for normal, well-formed parse errors) and therefore propagates uncaught through this API.For comparison,
nltk/sem/logic.py'sLogicParserwas hardened against exactly this class of issue:(
nltk/sem/logic.py:102-107), andnltk/jsontags.py'sJSONTaggedDecodersimilarly hasMAX_DECODE_DEPTH = 200with an explicit depth check.nltk/featstruct.pyhas no analogous protection.FeatureGrammar.fromstring()(nltk/grammar.py) parses feature structures embedded in FCFG grammar rules via the sameFeatStructReader, so the same crash is reachable through grammar-string parsing as well as throughFeatStruct()directly.PoC
Verified against the current
developbranch in a clean virtualenv (Python 3.12, NLTK installed from this checkout viapip install -e .):Result:
Minimal reproduction (no server required):
Illustrative server-side context (not part of NLTK itself, but representative of how the bug becomes reachable):
A POST of
{"grammar": "[a=" * 200 + "1" + "]" * 200}to this endpoint raises the uncaughtRecursionErrorinside the request handler.Impact
Vulnerability type: Denial of Service via uncontrolled recursion (CWE-674). This is not a memory-corruption bug and does not lead to code execution or data disclosure — Python's own recursion-limit safety net converts what would be a C-level stack overflow into a catchable (but here, uncaught)
RecursionError.Who is affected: Any application that passes externally-supplied text into
nltk.featstruct.FeatStruct()ornltk.grammar.FeatureGrammar.fromstring()— for example, NLP/computational-linguistics teaching tools, unification-grammar demo services, or NLU pipelines that accept user-authored feature grammars. This is a narrower slice of NLTK's user base than, e.g., tokenization or POS tagging, since feature-structure/unification-grammar parsing is a more specialized part of the library.Practical severity depends on deployment:
RecursionErrorcan terminate the entire process; without a process supervisor that auto-restarts it, this is a persistent outage until manually restarted. An attacker who repeats the payload can keep such a worker in a crash loop for as long as the attack continues.Suggested fix: Add a depth counter and a
MAX_PARSE_DEPTH-style constant toFeatStructReader, mirroring the existing fix innltk/sem/logic.py, and raise the library's normalValueError-based parse error once the limit is exceeded instead of lettingRecursionErrorpropagate.Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK: Uncontrolled resource consumption in RecursiveDescentParser via ambiguous or left-recursive grammars
CVE-2026-12876 / GHSA-ff5c-cp5c-9wjf
More information
Details
nltk.parse.RecursiveDescentParser(andSteppingRecursiveDescentParser) enumerate parses top-down with no bound on the number of recursive steps. A small, crafted context-free grammar makes a short input consume unbounded CPU (and/or exhaust the Python recursion stack), pinning a process indefinitely — a denial of service.Proof of concept
Both of the following hang on a 24-token input (killed after 8s; growth is super-linear in input length), on NLTK develop:
Impact
An application that runs
RecursiveDescentParseron a grammar (or an input) drawn from an untrusted source can be driven into an unbounded CPU / stack-exhaustion loop by a tiny payload. No confidentiality or integrity impact; single-process availability only.Sibling
The RegexpTokenizer ReDoS reported alongside this (CVE-2026-12875) is a different class (caller-supplied regex) and is addressed under GHSA-w3v8-gmh9-3wv7.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK: Quadratic CPU Exhaustion in
XMLCorpusView._read_xml_fragment()CVE-2026-81723 / GHSA-vp2x-qp44-57v7
More information
Details
Summary
XMLCorpusView._read_xml_fragment()reads a corpus file in 1 KiB blocks, appendingeach block to a growing
fragmentstring, then calls_VALID_XML_RE.match(fragment)on the full accumulated buffer every iteration. Because each iteration rescans the
entire accumulated fragment, the total amount of work grows quadratically with input
size.
Commit
c9c332284(CWE-1333) made eachmatch()call linear. The quadratic behavioris separate: the loop calls
match()once per 1 KiB block, each time on a longerbuffer.
On the test system, an 8 MiB malformed XML file consumed approximately 48 CPU-seconds
through the public
BNCCorpusReader.words()API with no source modification. Absolutetimings vary by hardware.
_read_xml_fragment()imposes no limit on fragment size oriteration count.
Details
File:
nltk/corpus/reader/xmldocs.pyFunction:
XMLCorpusView._read_xml_fragment(), lines 261–308The relevant loop:
For a payload of
b'<' + b'a' * (N-1):_VALID_XML_RE.match(fragment)does not succeed becausethe unterminated tag prevents the expression from matching before EOF.
fragment.rfind("<")returns0; the guardlast_open_bracket > 0isFalse, sothe backtrack branch is never taken.
Affected readers -> readers that rely on
XMLCorpusView, includingBNCCorpusReader,NPSChatCorpusReader,SemcorCorpusReader,MTECorpusReader,NKJPCorpusReader,FrameNetCorpusReader,VerbNetCorpusReader, and directXMLCorpusViewinstantiation.XMLCorpusReader.xml()is not affected -> it callsdefusedxml.safe_parse().PoC
Requires only
pip install nltk. No corpus data needed.Runtime should increase by approximately fourfold for each doubling of input size,
although absolute timings vary by hardware.
During verification,
_VALID_XML_RE.match()was instrumented to record the size ofeach input. For a 256 KiB malformed file it was invoked 257 times on monotonically
increasing buffers (1024, 2048, …, 262144 bytes), with the final call occurring after
EOF. This confirms that every iteration rescans the accumulated fragment.
Impact
Applications that process attacker-controlled XML corpus files through an affected reader
are vulnerable. The attacker needs only write access to a path the reader will open. No
NLTK credentials or special privileges required. Offline tools reading only trusted
local corpora are not at risk.
Affected versions: Verified in NLTK 3.9.4, 3.10.0, and the current develop branch.
Historical inspection indicates the same loop structure has existed since the
introduction of
XMLCorpusView(2007), but only the listed versions wereexperimentally verified. No patch exists in any published release.
This issue results in CPU exhaustion and may allow denial of service in applications
that process attacker-controlled XML corpus files.
Suggested Fix
Avoid rescanning the accumulated fragment from the beginning after each 1 KiB read.
Incremental parsing, bounded fragment accumulation, or another streaming approach would
eliminate the quadratic behavior while preserving existing semantics.
A regression test should verify that
BNCCorpusReader.words()raisesValueErrorwithin a fixed timeout (e.g. 5 seconds) against a 2 MiB malformed input. The existing
test_xmldocs_security.pycovers only the prior ReDoS payloads and does not exercisethis path.
Severity
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK: Downloader.download follows hardlinks and overwrites outside-root files
CVE-2026-81727 / GHSA-f794-5jv7-7672
More information
Details
Summary
NLTK's downloader now blocks symlink escapes during ZIP extraction, but it still treats pre-existing hardlinks inside the install tree as ordinary in-root files. A normal package install can therefore overwrite an outside-root inode through that hardlink.
Details
nltk.downloader.Downloader.download,nltk.downloader.Downloader.incr_download3.9.4and current sourcev3.10.0-rc2both reproduced for the extraction-stage overwrite.The install flow correctly rejects a pre-existing symlink at an extraction target, yet it accepts a pre-existing hardlink at the same path. When the package is installed, extracted member data is written through the hardlink and mutates the outside inode.
PoC
Preconditions
Steps
Downloader.download()package install whose extracted member lands on the hardlink path.Minimal reproducible excerpt
Impact
A shared or attacker-influenced downloader directory can be turned into an overwrite primitive against same-filesystem files outside the intended install root.
Remediation
Treat pre-existing hardlinks as unsafe in extraction targets, verify that each write path stays within the intended install tree at the inode level, and add regression tests that pair hardlinks with existing symlink controls.
Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK: Quadratic-time DoS in PorterStemmer via long runs of 'y'
CVE-2026-81722 / GHSA-ww6m-cw3f-q94g
More information
Details
nltk.stem.PorterStemmer.stem()-- a ubiquitous public API applied to arbitrary, often untrusted, tokens -- runs in O(n^2) time on a token containing a long run of the letter 'y', letting a single ~20-50 KB token pin a CPU core (CWE-407).Root cause
_is_consonant(word, i)was made iterative (commit for #3633, GHSA/CWE-674) to fix an earlier unbounded-recursionRecursionErroron'y'*10000. The iterative form walks backward over the whole run of 'y's on every call:_measure()then calls_is_consonant(stem, i)once for every positioniof the stem. For a run of n 'y's that is sum_{i} O(i) = O(n^2). The recursion fix therefore traded a CWE-674 RecursionError for a CWE-407 quadratic-time DoS.Proof of concept
Measured (Python 3.13):
stem('y'*5000 + 'ness')= 2.6s,stem('y'*10000 + 'ness')= 11.3s (2x input -> ~4.3x time = quadratic),stem('y'*20000 + 'ness')> 20s. A pure run of 'y' with no matching suffix is fast because the stemmer rules that call_measuredo not fire; a real suffix such as 'ness' triggers_measureon the long stem.Impact
Stemming is routinely applied to untrusted text (search, indexing, NLP pipelines). A single unbroken ~20-50 KB token of 'y' characters (no whitespace, so it survives tokenization) causes multi-second-to-minutes CPU consumption per request. No confidentiality/integrity impact; single-process availability only.
Fix direction
Classify each character's consonant/vowel status in a single left-to-right O(n) pass (memoise the 'y' run parity) instead of re-walking the run on every
_is_consonantcall, so_measureand stemming are linear. This is a sibling of the corpus-reader quadratic advisories GHSA-vp2x-qp44-57v7 and GHSA-8mpw-7fpc-4gqj (CWE-407).Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK AllowlistUnpickler dotted-name validation bypass allows remote code execution
CVE-2026-71513 / GHSA-5gh2-94qg-qppq
More information
Details
NLTK before 3.10.3 contains a remote code execution vulnerability in AllowlistUnpickler that validates only the pickle module string and not the global name, allowing attackers to resolve dotted names by attribute traversal to callables outside the allowlisted namespace. Attackers can craft untrusted transition-parser models that execute arbitrary commands when TransitionParser.parse loads the model through allowlisted_pickle_load.
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK CrubadanCorpusReader path traversal allows arbitrary file disclosure
CVE-2026-71514 / GHSA-cv22-g7mw-8v73
More information
Details
NLTK 3.9.4 through 3.10.2 contains a path traversal vulnerability in CrubadanCorpusReader. _load_lang_ngrams joins the corpus root with crubadan_code, the column-0 value read from the corpus table.txt mapping file, and opens the result with the builtin open() rather than the pathsec-validated opener, so os.path.join discards the root when that value is absolute and the read escapes the corpus directory without the containment check nltk.pathsec applies when ENFORCE is set. An attacker who controls a corpus package can disclose file contents outside the corpus root through lang_freq, limited to paths ending in -3grams.txt whose contents parse as token count lines.
Severity
CVSS:4.0/AV:L/AC:H/AT:P/PR:N/UI:P/VC:L/VI:N/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK: Symlink-based sandbox bypass in FramenetCorpusReader (bypasses the fix for CVE-2026-54292)
CVE-2026-62384 / GHSA-f833-7jw8-xwrv
More information
Details
This is a new, distinct vulnerability: a bypass of the fix already published as GHSA-xh95-f55m-82fw ("Path traversal in NLTK FramenetCorpusReader.frame() allows arbitrary XML file read, bypassing the nltk.pathsec sandbox"), not a duplicate of it.
Summary
The original advisory was fixed (PR #3581) by adding
_reject_unsafe_path_component(), which blocks literal/,\,.., and Windows drive prefixes in caller-/corpus-supplied names. It never resolves symlinks. All three call sites that use this guard still resolve the resulting path throughself.abspath()(nltk/corpus/reader/api.py,self._root.join(fileid)), which is a plain lexical join, not the symlink-resolving,required_root-scoped check thatCorpusReader.open()(andNKJPCorpusReader's own fix for its sibling advisory) correctly use elsewhere in this same codebase.A symlink placed inside the corpus's own subdirectory, with a name containing no separators at all, passes the guard cleanly and reads a file completely outside the corpus root.
Affected code (
nltk/corpus/reader/framenet.py)frame_by_name()reads<frame_dir>/<name>.xml_lu_file()reads<lu_dir>/lu<id>.xmldoc()reads<fulltext_dir>/<filename>All three follow the same chain:
_reject_unsafe_path_component(value, ...), thenself.abspath(os.path.join(subdir, value)), thenXMLCorpusView(...), opened viaPathPointer.open()with norequired_root.Proof of concept
Self-contained, runnable end to end.
Actual output when run against unpatched
main(commit35813c8):That content was read from
secret_path, a file entirely outsidecorpus_root, via a single, unmodified, public API call. No exception is raised anywhere in the chain;_reject_unsafe_path_componentpasses because"evil_link"contains no separators,.., or drive prefix.Verified the same way for the other two affected call sites,
_lu_file()(lu<id>.xmlsymlink underlu/) anddoc()(arbitrary filename symlink underfulltext/), both succeeding identically with no exception raised.Why this is in scope
SECURITY.mdnames "shared environments... multi-tenant pipelines" as its threat model) plus a completely normal API call.nltk.pathsecsandbox... including the strictENFORCE=Truemode" and be "consistent with the validation already used elsewhere in NLTK." It doesn't achieve that, sinceabspath()never reaches the scoped, symlink-resolving check that exists and is used correctly elsewhere in the same file tree (NKJPCorpusReader).Suggested fix
Route all three call sites through
CorpusReader.open()(or passrequired_root=self._roottovalidate_path()directly, asNKJPCorpusReaderalready does), instead ofself.abspath()plus rawPathPointer.open().Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK: Symlink-based arbitrary file read in IPIPANCorpusReader, bypasses nltk.pathsec entirely
CVE-2026-62383 / GHSA-3hhw-38pf-pxj6
More information
Details
Summary
IPIPANCorpusReader(nltk/corpus/reader/ipipan.py) exposes public methods,channels(),domains(),categories(), andfileids(channels=...), that accept a caller suppliedfileidslist and read a file via a completely unprotected builtinopen()call, with nonltk.pathsecinvolvement at all. A symlink placed inside the corpus root, with a name containing no separators or.., passes NLTK's existing traversal checks and is opened directly, reading a file from anywhere on the filesystem the process can access.Root cause
All four methods route through
_get_tag():farrives via_list_header_files()/_list_morph_files_by(), both of which call:on the result of
self.abspath(...)orself.abspaths(...).FileSystemPathPointersubclassesstr, so.replace()returns a plain Python string, silently discarding thePathPointerwrapper. That plain string is handed straight to builtinopen().This is a more severe variant of the same CWE-59 class already fixed elsewhere in this codebase (
CorpusReader.open(),NKJPCorpusReader.add_root(), and the recentFramenetCorpusReaderfix): those route file access throughnltk.pathsec.validate_path(), at minimum the global, non-scoped check, before opening. Here, converting thePathPointerto a plain string before callingopen()skipspathseccompletely, not just the corpus-root-scoped check, so the symlink target does not even need to land under a registerednltk.data.pathroot.Plain literal
../traversal in the fileid is still blocked byFileSystemPathPointer.join(), so this is specifically the symlink variant, not a regression of the older, simpler traversal class.Proof of concept
Constructed the normal, documented way,
fileidsas a regex over file paths, so the reader auto-discovers whatever.xmlfiles exist in its root with no special knowledge of the planted symlink.Actual output when run against current
develop:That content was read from
secret_path, a file entirely outsidecorpus_root. No exception raised anywhere. The planted symlink even surfaces naturally in the reader's ownfileids()listing, exactly as a real file would.Verified separately that literal
../traversal in the fileid is still rejected (ValueError: Traversal blocked), confirming this is specifically the symlink gap, not a broader regression.Why this is in scope
SECURITY.mdnames "shared environments... multi-tenant pipelines" as the project's own stated threat model) plus a completely normal API call.import nltkand documented, programmatic usage (words(),sents(),channels(), etc.), not a demo or GUI tool.FramenetCorpusReaderandNKJPCorpusReader.ipipan.pyhas never had security hardening applied, and has no dedicated test coverage at all.CVSS v3.1
Suggested fix
Route
_get_tag()throughnltk.pathsec.validate_path()with the corpus root asrequired_root, or throughCorpusReader.open(), instead of converting thePathPointerto a plain string and calling builtinopen()directly. The same fix pattern already applied toFramenetCorpusReaderandNKJPCorpusReaderapplies directly here.Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
NLTK: pathsec SSRF protection can be bypassed when a proxy is configured
CVE-2026-78682 / GHSA-6ww7-3frv-cqxh
More information
Details
Summary
Current NLTK source reopens SSRF in proxied environments.
pathsec.urlopen()validates the requested hostname locally, but once proxy inheritance is enabled the real fetch is performed by the proxy rather than by the validated direct-connect socket path.Details
nltk.pathsec.urlopen,nltk.data.load,nltk.downloader.Downloader.index,nltk.downloader.Downloader.downloadv3.10.0-rc2; published3.9.4was a negative control and did not reproduce._SafeHTTPHandlerand_SafeHTTPSHandler, so the validated hostname no longer matches the actual egress destination.The hardened direct path pins the validated numeric destination IP before opening the socket. The proxied branch instead copies
ProxyHandlerinstances from the global opener, marks the request as proxied, and skips the pinned handlers. I confirmed that a validated public URL can be fetched from a loopback-only internal service through the proxy path viapathsec.urlopen(),nltk.data.load(),Downloader.index(), andDownloader.download().PoC
Preconditions
pathsecto keep network fetches SSRF-safe.Steps
pathsec.urlopen()ornltk.data.load()on the public URL and observe the internal response is returned.Downloader(server_index_url=...), callindex()anddownload(), and observe internal-only content is parsed and installed.Minimal reproducible excerpt
Impact
Consumers that trust
pathsecas an SSRF barrier in proxied environments can be made to read internal-only HTTP resources, load forged downloader indexes, and install attacker-chosen package content fetched from the proxy's network view.Remediation
Preserve destination validation for the actual proxy egress target or fail closed when the request would otherwise downgrade into an unpinned proxied path. Add regression tests across
pathsec.urlopen,nltk.data.load, and downloader fetches with a configured proxy.References
Fix + attack demonstration (verified)
NLTK cannot pin the egress through a proxy, so it stops pretending to: under
ENFORCEa proxied fetch is refused rather than performed unvalidated. Operators who trust their proxy opt back in withNLTK_ALLOW_PROXIED_URLOPEN=1or `nltk.pathsec