CVE-2026-33230
Reflected XSS in NLTK's WordNet Browser app via the lookup route.
- CVSS 6.1
- CWE-79
- Input Validation and Sanitization
- Remote
NLTK (Natural Language Toolkit) is a suite of open source Python modules, data sets, and tutorials supporting research and development in Natural Language Processing. In versions 3.9.3 and prior, `nltk.app.wordnet_app` contains a reflected cross-site scripting issue in the `lookup_…` route. A crafted `lookup_<payload>` URL can inject arbitrary HTML/JavaScript into the response page because attacker-controlled `word` data is reflected into HTML without escaping. This impacts users running the local WordNet Browser server and can lead to script execution in the browser origin of that application. Commit 1c3f799607eeb088cab2491dcf806ae83c29ad8f fixes the issue.
- CWE
- CWE-79
- CVSS base score
- 6.1
- Published
- 2026-03-20
- OWASP
- A03 Injection
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Cross-Site Scripting (XSS)
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- NLTK
- Fixed by upgrading
- Yes
Solution
Upgrade NLTK to version 3.8.1 or later.
Vulnerable code sample
import os
from bottle import Bottle, request, static_file, template
# This code is a simplified representation of the vulnerable components
# in nltk.app.wordnet_app as they existed prior to the fix. It uses the
# Bottle web framework, as did the original NLTK application.
# To run this code:
# 1. pip install bottle
# 2. python your_file_name.py
# 3. Open a browser to http://127.0.0.1:8080/lookup/<script>alert('xss')</script>
bottle = Bottle()
app_dir = os.path.dirname(__file__)
STATIC_DIR = os.path.join(app_dir, "static")
@bottle.route("/static/:path#.+#")
def static(path):
return static_file(path, root=STATIC_DIR)
@bottle.route("/")
def home():
return template(
"""
<h2>WordNet Browser</h2>
<form action="javascript:lookup(document.getElementById('word_input').value)">
<input type="text" id="word_input"></input>
<input type="submit" value="lookup">
</form>
<div id="results"></div>
<script src="/static/jquery.min.js"></script>
<script src="/static/d3.min.js"></script>
<script>
function lookup(word) {
document.location.href = "/lookup/" + word;
}
</script>
"""
)
@bottle.route("/lookup/:word")
def lookup(word):
"""
Look up a word in WordNet.
This is a trivial function, since the real work is done by the client.
"""
html = """
<h2>Results for: {{word}}</h2>
<div id="synset_plot_wrapper">
<div id="synset_plot"></div>
</div>
"""
# The vulnerability is here: The `word` variable from the URL is passed
# to the template. The `{{word}}` syntax in Bottle's default simple
# template engine does not escape HTML characters. An attacker-controlled
# string in `word` is therefore rendered directly into the page.
return template(html, word=word)
if __name__ == "__main__":
bottle.run(host="localhost", port=8080)Patched code sample
import html
from collections import Counter
# The following is a minimal mock of the NLTK WordNet interface (`nltk.corpus.wordnet`)
# to allow this code to be run standalone for demonstration purposes.
class MockSynset:
def __init__(self, pos):
self._pos = pos
def pos(self):
return self._pos
class MockLemma:
def __init__(self, pos_list):
self.synsets = [MockSynset(p) for p in pos_list]
class MockWordNet:
def lemmas(self, word):
if "script" in word:
# Simulate finding the word to process it
return [MockLemma(["n"])]
return []
wn = MockWordNet()
# End of mock objects
class WordNetApp:
"""
This class contains the patched methods from nltk.app.wordnet_app
that fix the reflected XSS vulnerability CVE-2024-33230.
The fix involves using `html.escape()` on user-controlled input (`word`)
before embedding it into the HTML response.
"""
def _make_lemmas_html(self, word):
"""
Generate the HTML for the list of lemmas.
This is part of the fix.
"""
# FIX: The user-provided 'word' is escaped before being used in HTML.
escaped_word = html.escape(word)
html_content = ""
lemmas = wn.lemmas(word)
if not lemmas:
html_content += "<i>Not in WordNet</i>"
return html_content
counts = Counter(s.pos() for l in lemmas for s in l.synsets())
pos_tags = sorted(counts.keys(), key=lambda x: "nvar".index(x))
html_content += "<h2>POS tags</h2>\n<ul>"
for pos in pos_tags:
# FIX: The escaped word is used in the generated hyperlink.
html_content += f'<li><a href="lemma_{escaped_word}.{pos}">{pos} ({counts[pos]})</a></li>\n'
html_content += "</ul>"
return html_content
def lookup_lemmas(self, word):
"""
Generate the main HTML page for a given word.
This route was the primary source of the vulnerability.
"""
# FIX: The user-provided 'word' is escaped before being used in HTML.
escaped_word = html.escape(word)
# The `escaped_word` variable is now safely used in the f-string,
# preventing HTML injection.
html_content = f"""
<html>
<head>
<title>WordNet Browser: {escaped_word}</title>
</head>
<body>
<h1>{escaped_word}</h1>
"""
html_content += self._make_lemmas_html(word)
html_content += """
</body>
</html>
"""
return html_content
if __name__ == '__main__':
# This block demonstrates the effect of the fix.
app = WordNetApp()
# 1. A non-malicious word
print("--- Processing a normal word: 'test' ---")
safe_word = "test"
safe_output = app.lookup_lemmas(safe_word)
print(safe_output)
print("\n" + "="*40 + "\n")
# 2. A malicious payload that would cause XSS in the vulnerable version
print("--- Processing a malicious payload with the fix ---")
malicious_payload = "<script>alert('XSS Vulnerability');</script>"
fixed_output = app.lookup_lemmas(malicious_payload)
print(fixed_output)
print("\n[DEMO] Note how in the second output, '<' and '>' are replaced with '<' and '>'.")
print("[DEMO] This prevents the browser from executing the script.")Payload
<script>alert('XSS')</script>
Cite this entry
@misc{vaitp:cve202633230,
title = {{Reflected XSS in NLTK's WordNet Browser app via the lookup route.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-33230},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-33230/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
