VAITP Dataset

← Back to the dataset

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.

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 ::