GHSA-FG9W-WFP5-J933

Vulnerability from github – Published: 2026-11-13 23:20 – Updated: 2026-11-13 23:20
VLAI
Summary
NLTK: BllipParser passes caller-supplied model paths to the bllipparser C extension with no pathsec check, enabling arbitrary read
Details

Summary

BllipParser in nltk/parse/bllip.py hands caller-supplied filesystem paths to the native bllipparser C extension without routing them through NLTK's pathsec validation. The constructor parameters parser_model, reranker_features, and reranker_weights, and the alternative from_unified_model_dir() entry point, flow directly to the C extension's load_parser_model() and load_reranker_model() calls, which open and read the given path at the OS level. Any application that passes an attacker-influenced path to this API reads a file of the attacker's choosing from anywhere on the filesystem, outside the NLTK data roots that every other NLTK tool wrapper confines access to.

Root Cause

As of NLTK 3.10.0, SECURITY.md documents a centralized I/O security module (nltk.pathsec) with strict enforcement enabled by default, under which file-backed resources must resolve inside allowed NLTK data directories. For paths handed to external tools and native extensions that open files themselves, the module provides validate_tool_path(), which runs name-safety checks, rejects option-shaped, URL-shaped, and traversal inputs, opens the path with O_NOFOLLOW requiring a regular single-linked file, and returns the validated string the caller must use. Every sibling tool wrapper applies it. BllipParser applies none of it: the module imports only ParserI, safe_print, and Tree, with zero pathsec references, so the three model-path parameters reach the C extension unchecked. The violated invariant is that NLTK tool wrappers must bound caller-supplied native-tool paths to the data roots before the native code opens them.

nltk/parse/bllip.py:125-131   BllipParser.__init__ accepts parser_model,
                              reranker_features, reranker_weights (no validation)
nltk/parse/bllip.py:164       self.rrp.load_parser_model(parser_model, ...)   -> C extension open
nltk/parse/bllip.py:166-170   self.rrp.load_reranker_model(features_filename=...,
                              weights_filename=...)                          -> C extension open
nltk/parse/bllip.py:218-255   from_unified_model_dir(model_dir) -> __init__ (no validation)
nltk/parse/bllip.py:9-11      imports: ParserI, safe_print, Tree (no pathsec import)

Impact

An application that uses BllipParser (requiring the optional bllipparser package to be installed) and that passes a model path derived from untrusted input to the constructor or to from_unified_model_dir() allows that input to designate an arbitrary filesystem path. The native extension opens and reads the designated file into process memory to parse it as model data. This defeats the pathsec data-root confinement that NLTK applies to all other native-tool model loads. The attacker-controlled path can target files outside the NLTK data directories, for example local configuration or credential files readable by the process. Content disclosure to the attacker depends on how the application surfaces parser errors or results; file existence, size, and open behavior provide an oracle regardless. Because the same unvalidated path also skips the checks that refuse a non-regular file or an over-large model, the attacker can instead designate a FIFO, socket, device, or directory, or an oversized regular file, so the native load blocks or reads unbounded data into memory and the component performing the parse suffers reduced performance or a stalled worker. The primitive demonstrated by source analysis is attacker control of which file the process opens and reads.

Proof of Concept

Requires the optional bllipparser C extension to be installed
(pip install bllipparser) so that RerankingParser.load_parser_model
is available. No NLTK data download is needed to exercise the path.
# An application forwards an untrusted "model" path into BllipParser.
from nltk.parse.bllip import BllipParser

# Attacker-controlled value reaches the constructor unchanged.
untrusted_path = "/etc/passwd"
BllipParser(parser_model=untrusted_path)
# The bllipparser C extension opens and reads untrusted_path at the OS
# level with no NLTK pathsec confinement to the data roots.
Structural confirmation without the optional extension installed:

$ grep -n "pathsec\|validate_path\|validate_tool" nltk/parse/bllip.py
(no output)

$ grep -n "validate_tool_path" nltk/tag/crf.py nltk/tag/hunpos.py \
      nltk/tag/stanford.py nltk/parse/malt.py
nltk/tag/crf.py:140
nltk/tag/crf.py:311
nltk/tag/hunpos.py:111
nltk/tag/stanford.py:85
nltk/tag/stanford.py:137
nltk/parse/malt.py:340
nltk/parse/malt.py:344

BllipParser is the only native-tool wrapper with no validate_tool_path call.

Attack Chain

  1. Exposure: An application imports BllipParser (exported at nltk/parse/bllip.py:83 and re-exported at nltk/parse/__init__.py:47) and constructs it with a model path influenced by untrusted input.
  2. Control: The attacker controls the parser_model, reranker_features, or reranker_weights string (or the model_dir passed to from_unified_model_dir() at nltk/parse/bllip.py:218); it is accepted as an arbitrary string with no type or content constraint.
  3. Path: The value flows unchanged from BllipParser.__init__ (nltk/parse/bllip.py:125) to self.rrp.load_parser_model(parser_model, ...) at nltk/parse/bllip.py:164, and the reranker values to self.rrp.load_reranker_model(...) at nltk/parse/bllip.py:166-170.
  4. Guard: No pathsec guard is present on this path. nltk/parse/bllip.py imports no pathsec symbol, so validate_tool_path() (the guard applied by every sibling wrapper) is never invoked before the native open.
  5. Primitive: The bllipparser C extension performs an OS-level open and read of the supplied path to parse model data, reaching the filesystem directly.
  6. Result: A file at an attacker-chosen path, including locations outside the NLTK data roots, is opened and read by the process, bypassing the pathsec data-root confinement enforced elsewhere.

Bypass Evidence

The relevant protection is nltk.pathsec.validate_tool_path() (nltk/pathsec.py:950), documented as bounding "a filesystem path handed to an external tool to read or write" and designed exactly for native loaders that open the file themselves. The strongest counter-argument is that SECURITY.md disclaims being "a general operating-system sandbox" and does "not prevent all unsafe behavior an application might perform outside NLTK." That disclaimer concerns application code outside NLTK, not NLTK's own tool wrappers: the project added validate_tool_path() and applied it to CRFTagger.set_model_file() (nltk/tag/crf.py:140), HunposTagger (nltk/tag/hunpos.py:111), StanfordTagger (nltk/tag/stanford.py:85,137), and MaltParser (nltk/parse/malt.py:340,344), with the CRFTagger and HunposTagger wrappers citing a prior tool-path advisory and explaining that pathsec.open cannot be used because the C extension does its own open. BllipParser is the single native-tool wrapper that omits the guard. The alternative from_unified_model_dir() path offers no mitigation: get_unified_model_parameters() belongs to the third-party bllipparser package and performs no NLTK data-root validation, so the derived sub-paths reach the constructor and the C extension unchecked.

Affected Versions

  • Ecosystem: pip
  • Package: nltk
  • Confirmed affected range: >= 3.10.0, <= 3.10.3
  • Latest release checked: 3.10.3 (PyPI)
  • Fix status: 3.10.4

The lower bound is 3.10.0 because the pathsec enforcement model and the validate_tool_path() guard were introduced in that release; before 3.10.0 no such boundary existed for NLTK to violate. The missing validation persists unchanged through 3.10.3, the current PyPI release, and on the repository default branch (no pathsec, validate_path, or validate_tool reference exists in nltk/parse/bllip.py). No fixed release is established.

Suggested Fix

Validate every caller-supplied native-tool path in BllipParser before it reaches the bllipparser C extension, matching the pattern already used by the other wrappers. In BllipParser.__init__, pass parser_model, reranker_features, and reranker_weights through nltk.pathsec.validate_tool_path() (with max_bytes=MAX_TOOL_MODEL_BYTES and require_private=True, as CRFTagger.set_model_file() does) and build the C-extension arguments from the returned strings so a path swapped after the check is not reopened. Because parser_model denotes a model directory for this loader, the fix must cover the directory argument and the reranker file arguments, and should apply equally to the paths derived inside from_unified_model_dir(). As an interim measure, applications should refuse to pass untrusted input as any BllipParser model path and should load models only through nltk.data.find(), which already enforces pathsec confinement.


Reported by zx.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.10.3"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "nltk"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.10.0"
            },
            {
              "fixed": "3.10.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-126295"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22",
      "CWE-400",
      "CWE-73"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-11-13T23:20:56Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\n`BllipParser` in `nltk/parse/bllip.py` hands caller-supplied filesystem paths to the native `bllipparser` C extension without routing them through NLTK\u0027s `pathsec` validation. The constructor parameters `parser_model`, `reranker_features`, and `reranker_weights`, and the alternative `from_unified_model_dir()` entry point, flow directly to the C extension\u0027s `load_parser_model()` and `load_reranker_model()` calls, which open and read the given path at the OS level. Any application that passes an attacker-influenced path to this API reads a file of the attacker\u0027s choosing from anywhere on the filesystem, outside the NLTK data roots that every other NLTK tool wrapper confines access to.\n\n## Root Cause\n\nAs of NLTK 3.10.0, `SECURITY.md` documents a centralized I/O security module (`nltk.pathsec`) with strict enforcement enabled by default, under which file-backed resources must resolve inside allowed NLTK data directories. For paths handed to external tools and native extensions that open files themselves, the module provides `validate_tool_path()`, which runs name-safety checks, rejects option-shaped, URL-shaped, and traversal inputs, opens the path with `O_NOFOLLOW` requiring a regular single-linked file, and returns the validated string the caller must use. Every sibling tool wrapper applies it. `BllipParser` applies none of it: the module imports only `ParserI`, `safe_print`, and `Tree`, with zero `pathsec` references, so the three model-path parameters reach the C extension unchecked. The violated invariant is that NLTK tool wrappers must bound caller-supplied native-tool paths to the data roots before the native code opens them.\n\n```text\nnltk/parse/bllip.py:125-131   BllipParser.__init__ accepts parser_model,\n                              reranker_features, reranker_weights (no validation)\nnltk/parse/bllip.py:164       self.rrp.load_parser_model(parser_model, ...)   -\u003e C extension open\nnltk/parse/bllip.py:166-170   self.rrp.load_reranker_model(features_filename=...,\n                              weights_filename=...)                          -\u003e C extension open\nnltk/parse/bllip.py:218-255   from_unified_model_dir(model_dir) -\u003e __init__ (no validation)\nnltk/parse/bllip.py:9-11      imports: ParserI, safe_print, Tree (no pathsec import)\n```\n\n## Impact\n\nAn application that uses `BllipParser` (requiring the optional `bllipparser` package to be installed) and that passes a model path derived from untrusted input to the constructor or to `from_unified_model_dir()` allows that input to designate an arbitrary filesystem path. The native extension opens and reads the designated file into process memory to parse it as model data. This defeats the pathsec data-root confinement that NLTK applies to all other native-tool model loads. The attacker-controlled path can target files outside the NLTK data directories, for example local configuration or credential files readable by the process. Content disclosure to the attacker depends on how the application surfaces parser errors or results; file existence, size, and open behavior provide an oracle regardless. Because the same unvalidated path also skips the checks that refuse a non-regular file or an over-large model, the attacker can instead designate a FIFO, socket, device, or directory, or an oversized regular file, so the native load blocks or reads unbounded data into memory and the component performing the parse suffers reduced performance or a stalled worker. The primitive demonstrated by source analysis is attacker control of which file the process opens and reads.\n\n## Proof of Concept\n\n```text\nRequires the optional bllipparser C extension to be installed\n(pip install bllipparser) so that RerankingParser.load_parser_model\nis available. No NLTK data download is needed to exercise the path.\n```\n\n```python\n# An application forwards an untrusted \"model\" path into BllipParser.\nfrom nltk.parse.bllip import BllipParser\n\n# Attacker-controlled value reaches the constructor unchanged.\nuntrusted_path = \"/etc/passwd\"\nBllipParser(parser_model=untrusted_path)\n# The bllipparser C extension opens and reads untrusted_path at the OS\n# level with no NLTK pathsec confinement to the data roots.\n```\n\n```text\nStructural confirmation without the optional extension installed:\n\n$ grep -n \"pathsec\\|validate_path\\|validate_tool\" nltk/parse/bllip.py\n(no output)\n\n$ grep -n \"validate_tool_path\" nltk/tag/crf.py nltk/tag/hunpos.py \\\n      nltk/tag/stanford.py nltk/parse/malt.py\nnltk/tag/crf.py:140\nnltk/tag/crf.py:311\nnltk/tag/hunpos.py:111\nnltk/tag/stanford.py:85\nnltk/tag/stanford.py:137\nnltk/parse/malt.py:340\nnltk/parse/malt.py:344\n\nBllipParser is the only native-tool wrapper with no validate_tool_path call.\n```\n\n## Attack Chain\n\n1. **Exposure:** An application imports `BllipParser` (exported at `nltk/parse/bllip.py:83` and re-exported at `nltk/parse/__init__.py:47`) and constructs it with a model path influenced by untrusted input.\n2. **Control:** The attacker controls the `parser_model`, `reranker_features`, or `reranker_weights` string (or the `model_dir` passed to `from_unified_model_dir()` at `nltk/parse/bllip.py:218`); it is accepted as an arbitrary string with no type or content constraint.\n3. **Path:** The value flows unchanged from `BllipParser.__init__` (`nltk/parse/bllip.py:125`) to `self.rrp.load_parser_model(parser_model, ...)` at `nltk/parse/bllip.py:164`, and the reranker values to `self.rrp.load_reranker_model(...)` at `nltk/parse/bllip.py:166-170`.\n4. **Guard:** No pathsec guard is present on this path. `nltk/parse/bllip.py` imports no pathsec symbol, so `validate_tool_path()` (the guard applied by every sibling wrapper) is never invoked before the native open.\n5. **Primitive:** The `bllipparser` C extension performs an OS-level open and read of the supplied path to parse model data, reaching the filesystem directly.\n6. **Result:** A file at an attacker-chosen path, including locations outside the NLTK data roots, is opened and read by the process, bypassing the pathsec data-root confinement enforced elsewhere.\n\n## Bypass Evidence\n\nThe relevant protection is `nltk.pathsec.validate_tool_path()` (`nltk/pathsec.py:950`), documented as bounding \"a filesystem path handed to an external tool to read or write\" and designed exactly for native loaders that open the file themselves. The strongest counter-argument is that `SECURITY.md` disclaims being \"a general operating-system sandbox\" and does \"not prevent all unsafe behavior an application might perform outside NLTK.\" That disclaimer concerns application code outside NLTK, not NLTK\u0027s own tool wrappers: the project added `validate_tool_path()` and applied it to `CRFTagger.set_model_file()` (`nltk/tag/crf.py:140`), `HunposTagger` (`nltk/tag/hunpos.py:111`), `StanfordTagger` (`nltk/tag/stanford.py:85,137`), and `MaltParser` (`nltk/parse/malt.py:340,344`), with the CRFTagger and HunposTagger wrappers citing a prior tool-path advisory and explaining that `pathsec.open` cannot be used because the C extension does its own open. `BllipParser` is the single native-tool wrapper that omits the guard. The alternative `from_unified_model_dir()` path offers no mitigation: `get_unified_model_parameters()` belongs to the third-party `bllipparser` package and performs no NLTK data-root validation, so the derived sub-paths reach the constructor and the C extension unchecked.\n\n## Affected Versions\n\n- **Ecosystem:** `pip`\n- **Package:** `nltk`\n- **Confirmed affected range:** `\u003e= 3.10.0, \u003c= 3.10.3`\n- **Latest release checked:** `3.10.3` ([PyPI](https://pypi.org/project/nltk/3.10.3/))\n- **Fix status:** 3.10.4\n\nThe lower bound is 3.10.0 because the pathsec enforcement model and the `validate_tool_path()` guard were introduced in that release; before 3.10.0 no such boundary existed for NLTK to violate. The missing validation persists unchanged through 3.10.3, the current PyPI release, and on the repository default branch (no `pathsec`, `validate_path`, or `validate_tool` reference exists in `nltk/parse/bllip.py`). No fixed release is established.\n\n## Suggested Fix\n\nValidate every caller-supplied native-tool path in `BllipParser` before it reaches the `bllipparser` C extension, matching the pattern already used by the other wrappers. In `BllipParser.__init__`, pass `parser_model`, `reranker_features`, and `reranker_weights` through `nltk.pathsec.validate_tool_path()` (with `max_bytes=MAX_TOOL_MODEL_BYTES` and `require_private=True`, as `CRFTagger.set_model_file()` does) and build the C-extension arguments from the returned strings so a path swapped after the check is not reopened. Because `parser_model` denotes a model directory for this loader, the fix must cover the directory argument and the reranker file arguments, and should apply equally to the paths derived inside `from_unified_model_dir()`. As an interim measure, applications should refuse to pass untrusted input as any `BllipParser` model path and should load models only through `nltk.data.find()`, which already enforces pathsec confinement.\n\n---\n\nReported by zx.",
  "id": "GHSA-fg9w-wfp5-j933",
  "modified": "2026-11-13T23:20:56Z",
  "published": "2026-11-13T23:20:56Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/nltk/nltk/security/advisories/GHSA-fg9w-wfp5-j933"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-126295"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nltk/nltk/commit/fdde6d9ebf5fd612e817b49ec7fecd752802c268"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/nltk/nltk"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nltk/nltk/releases/tag/v3.10.4"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/nltk-3.10.0-before-3.10.4-arbitrary-file-read-via-bllipparser-model-paths"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "NLTK: BllipParser passes caller-supplied model paths to the bllipparser C extension with no pathsec check, enabling arbitrary read"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
How durations are calculated

Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.

Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.

Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.