">
 

CVE-2026-12243 — How a Percent-Encoded Slash Bypasses NLTK's Path Traversal Guard

Iniciado por joomlamz, Hoje at 10:25

Respostas: 1   |   Visualizações: 1

Tópico anterior - Tópico seguinte

0 Membros e 1 Visitante estão a ver este tópico.

Olá a todos os membros da comunidade **webmastersmz.com**!

Como especialista em tecnologia, analisei o tópico sobre a vulnerabilidade **CVE-2026-12243**, que expõe uma falha crítica de segurança no **NLTK** (*Natural Language Toolkit*). Esta falha é um exemplo clássico de como técnicas de ofuscação podem comprometer mecanismos de defesa bem intencionados.

### Análise Técnica: CVE-2026-12243

O cerne do problema reside numa falha de **Path Traversal** (travessia de diretórios). O NLTK implementa um "guardião" (*guard*) para impedir que utilizadores mal-intencionados acedam a ficheiros fora do diretório pretendido através de caminhos como `../../`.

O que torna esta vulnerabilidade interessante e perigosa é o uso de **Percent-Encoding** (codificação por percentagem). O atacante utiliza uma barra invertida ou normal codificada (ex: `%2F` para `/`) para contornar a validação do filtro. O "guardião" do NLTK falha ao não normalizar (decodificar) o caminho antes de validar o acesso. Consequentemente:

1.  **Bypass do Filtro:** O sistema de segurança vê o caminho codificado como uma string "segura" e inocente.
2.  **Execução no Sistema Operativo:** Quando o NLTK passa este caminho para as funções de leitura de ficheiros do sistema operativo, este interpreta o `%2F` como uma barra literal, permitindo ao atacante saltar diretórios e ler ficheiros sensíveis do servidor.

**Pontos principais a reter:**
*   **Falta de Normalização:** Nunca confie numa entrada de utilizador sem primeiro a normalizar (remover codificações, resolver caminhos relativos).
*   **Input Validation:** A validação deve ocorrer após o processo de descodificação da string.
*   **Princípio do Privilégio Mínimo:** Aplicações como o NLTK deveriam correr em ambientes isolados (*sandboxed*) para mitigar o impacto de uma eventual leitura indevida de ficheiros.

### Vamos debater!

Esta vulnerabilidade levanta questões pertinentes para nós, administradores de sistemas e programadores: **Como é que vocês têm lidado com a sanitização de inputs em aplicações Python nos vossos servidores?** Já tiveram casos em que bibliotecas de terceiros criaram "buracos" de segurança inesperados nas vossas infraestruturas? Partilhem as vossas experiências e estratégias de mitigação aqui no fórum!

---

Para garantir que os vossos projetos e fóruns rodam sem falhas, com a segurança e a performance que o vosso tráfego exige, convido-vos a conhecer as soluções de alojamento de alta performance da **AplicHost** em https://aplichost.com. Estamos prontos para apoiar o crescimento da vossa presença digital em Moçambique!

CVE-2026-12243 — How a Percent-Encoded Slash Bypasses NLTK's Path Traversal Guard



Tópico: CVE-2026-12243 — How a Percent-Encoded Slash Bypasses NLTK's Path Traversal Guard
Categoria: Tutoriais | Programação & Tecnologia
Idioma Principal: Português (Conteúdo de Tecnologia)

Descrição do Conteúdo / Informações:
-------------------------------------------------------------------------
CVE ID
CVE-2026-12243

Affects
NLTK (Natural Language Toolkit) ≤ 3.9.4

Weakness
CWE-22 (Path Traversal)

CVSS 3.1
7.5 High — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Root cause
Percent-encoding bypasses path validation — a classic decode-after-check bug

Impact
Arbitrary file read

Fixed in
3.10.0

Backstory
An earlier fix for GitHub Issue #3504 turned out to be incomplete

NLTK is one of the most widely used NLP libraries in the Python ecosystem, and nltk.data.load() / nltk.data.find() sit on the hot path every time a corpus or model gets loaded. Both functions turn a "resource name" string into a filesystem path, and that conversion had a validation bypass: a literal ../ gets blocked correctly, but its percent-encoded form — %2e%2e%2f or plain %2f — sails through the check and only gets decoded into a real path afterward.



Why it happened — the gap in the earlier fix (Issue #3504)


NLTK had dealt with path traversal before, and the mitigation lived as a regex filter in nltk/data.py:

# nltk/data.py (vulnerable, as of 3.9.4)
_UNSAFE_NO_PROTOCOL_RE = re.compile(
r"(?:\.\./|\.\.$|^/|\\|[A-Za-z]:[/\\])"
)

def find(resource_name, paths=None):
resource_name = normalize_resource_name(resource_name, True)

if _UNSAFE_NO_PROTOCOL_RE.search(resource_name):
raise ValueError(f"Unsafe resource path: {resource_name!r}")

# ... resource_name, having passed the check, is used as-is below
p = os.path.join(path_, url2pathname(resource_name))
if os.path.exists(p):
return FileSystemPathPointer(p)

Literal ../, a leading /, backslashes, and Windows drive letters (C:/) are all caught precisely by this regex. The problem is what gets checked. _UNSAFE_NO_PROTOCOL_RE.search() only ever runs against the raw, still-URL-encoded string. But the very next line calls the standard library's url2pathname(), which has the side effect of decoding %xx percent sequences.

In other words: validation happens on the encoded string, while the filesystem path is built from the decoded one. That gap between check-time and use-time is exactly what this vulnerability exploits — the textbook shape of a "decode-after-check" (or TOCTOU-style) flaw.

Walking through the attack string corpora/..%2f..%2f..%2fetc%2fpasswd step by step:


_UNSAFE_NO_PROTOCOL_RE inspects the raw string. ..%2f contains no literal ../ — it's literally the characters %, 2, f — so the regex doesn't match, and the check passes.

• The now-validated string is handed to url2pathname(), which decodes %2f into / and %2e into ..

• The decoding produces corpora/../../../etc/passwd — exactly the pattern the regex was supposed to stop.


os.path.join(nltk_data_dir, decoded_path) normalizes this and walks straight out of the intended directory, landing on /etc/passwd.



Three payloads, two different outcomes


Based on the PoC filed on huntr, comparing three payloads makes the bypass condition obvious:


nltk:../../../etc/passwd — a literal traversal. _UNSAFE_NO_PROTOCOL_RE catches ../ immediately, a ValueError is raised, and the request is blocked as designed.


nltk:%2fetc%2fpasswd — a percent-encoded leading slash. As a string it matches none of ^/, ../, backslash, or a drive letter, so it sails through the check. url2pathname() decodes it to /etc/passwd.


nltk:corpora/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd — %2e%2e is a different string from .., so it passes the same check. After decoding, it walks five levels up and out.

A related variant, nltk:%2fproc%2fself%2fenviron, targets the process environment file directly. /proc/self/environ frequently leaks API keys, database credentials, and cloud secrets that were passed in as environment variables, which makes it a particularly attractive target once the primary check is bypassed.



There was a second layer of defense — but it isn't enforced by default


NLTK also ships a nltk.pathsec module meant to re-check the path right before the file is actually opened. The catch: this check isn't enforced unless you explicitly opt in.

# typical pattern inside nltk/pathsec.py
ENFORCE = os.environ.get('NLTK_PATHSEC_ENFORCE', '').lower() in ('1', 'true', 'yes')

def validate_something(path):
if is_violation(path):
if ENFORCE:
raise SecurityError('...')      # only raises if the env var is set
else:
warnings.warn('...', RuntimeWarning)   # default: warn and keep going

ENFORCE stays False unless the NLTK_PATHSEC_ENFORCE environment variable is explicitly set. So out of the box, a dangerous path only produces a RuntimeWarning — the open() call itself still goes through. The one backstop you might expect to catch a bypassed regex check ends up being little more than a log line unless you turn it on yourself.



Who's affected


This bug matters for any application that passes externally controlled input into nltk.data.load() or nltk.data.find() as the resource name:

• NLP web services or APIs that let users specify a corpus/model name

• Hosted notebook services that execute user-supplied code

• Multi-tenant ML pipelines that parameterize resource identifiers per tenant

• CI/CD pipelines that build resource paths from external input

The CVSS vector (C:H/I:N/A:N) tells the story: this is a confidentiality-only issue. Nothing gets modified or taken down — it's arbitrary read access to anything the process's user can read. Beyond /etc/passwd and /proc/self/environ, that includes application config files, SSH private keys, and any locally cached cloud-metadata responses.



The fix in 3.10.0


3.10.0 targets the root cause directly — decode-then-check — by adding an _assert_no_encoded_bypass() function that re-runs the same validation against the decoded form of the string.

from urllib.parse import unquote

def _assert_no_encoded_bypass(name, error_label=None):
"""
Reject `name` if its URL-decoded form contains an unsafe pattern.

unquote() is applied exactly once. url2pathname() itself only does a
single decode pass, so this mirrors that behavior; decoding
repeatedly would change the meaning of legitimately encoded values
like "%2520" (a literal "%20").
"""
decoded = unquote(name)
if decoded != name and _UNSAFE_NO_PROTOCOL_RE.search(decoded):
label = name if error_label is None else error_label
raise ValueError(f"Unsafe resource path: {label!r}")

def _reject_unsafe_no_protocol(resource_url):
if _UNSAFE_NO_PROTOCOL_RE.search(resource_url):
raise ValueError(f"Unsafe resource path: {resource_url!r}")
# re-check the decoded form against the same policy
_assert_no_encoded_bypass(resource_url)

Three things matter here:


The same regex is reused, not duplicated. Rather than inventing a new blocklist, _UNSAFE_NO_PROTOCOL_RE is applied to both the raw string and its unquote()-decoded form. There's only one policy to keep in sync.


Decoding happens exactly once. Matching url2pathname()'s single decode pass avoids breaking legitimately double-encoded values such as %2520 (a literal %20), which repeated decoding would otherwise mangle.


Every entry point calls it. _reject_unsafe_no_protocol(), the nltk: scheme handling inside normalize_resource_url(), and the defense-in-depth check inside find() all now call _assert_no_encoded_bypass() — so there's no remaining code path where a resource name turns into a file path without the decoded check running.



What to do about it



Upgrade to NLTK 3.10.0 or later. This is the real fix.

• If an immediate upgrade isn't possible, set NLTK_PATHSEC_ENFORCE=true to activate the pathsec layer's hard block. Treat this as a stopgap, not a substitute for patching — it's a mitigation, not a root-cause fix.

• Audit any code path where a resource name passed to nltk.data.load() / nltk.data.find() originates from user input. An application-level allowlist of permitted corpus/model names is a reasonable defense-in-depth measure on top of the library fix.


Joomlamz
Consultoria em Informática
-------------------------------------------------------
Especialista em Sistemas Web & Manutenção de Servidores.
A desenvolver o novo AplPortal com suporte a PHP 8.
Precisa de ajuda profissional? Contacte-me.

Tags: