VAITP Dataset

← Back to the dataset

CVE-2026-33231

NLTK's WordNet Browser is vulnerable to unauthenticated remote shutdown.

  • CVSS 7.5
  • CWE-306
  • Authentication, Authorization, and Session Management
  • 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` allows unauthenticated remote shutdown of the local WordNet Browser HTTP server when it is started in its default mode. A simple `GET /SHUTDOWN%20THE%20SERVER` request causes the process to terminate immediately via `os._exit(0)`, resulting in a denial of service. Commit bbaae83db86a0f49e00f5b0db44a7254c268de9b patches the issue.

CVSS base score
7.5
Published
2026-03-20
OWASP
A01 Broken Access Control
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Authentication, Authorization, and Session Management
Subcategory
Poorly Designed Access Controls
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
NLTK
Fixed by upgrading
Yes

Solution

Upgrade NLTK to version 3.9.4 or later.

Vulnerable code sample

# Natural Language Toolkit: Wordnet Browser Application
#
# Copyright (C) 2001-2023 NLTK Project
# Author: Jussi Salmela <jtsalmela@users.sourceforge.net>
#         Paul Bone <pbone@csse.unimelb.edu.au>
#         Tom Aarsen
# URL: <https://www.nltk.org/>
# For license information, see LICENSE.TXT

import os
import re
import sys
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import unquote_plus

from nltk.corpus import wordnet
from nltk.data import find

try:
    from tkinter import (
        Button,
        Frame,
        Label,
        Menu,
        Menubutton,
        Tk,
        Toplevel,
        scrolledtext,
    )
except ImportError:
    from Tkinter import (
        Button,
        Frame,
        Label,
        Menu,
        Menubutton,
        Tk,
        Toplevel,
        scrolledtext,
    )

try:
    wordnet.ensure_loaded()
except LookupError as e:
    from nltk.downloader import download

    print(e)
    print("This app requires the WordNet corpus.")
    print("Please use a command like the following to download it:")
    print("\n  >>> import nltk")
    print("  >>> nltk.download('wordnet')\n")
    sys.exit(1)


######################################################################
# The WordNet Browser
######################################################################
class WordNetBrowser:
    """
    A WordNet browser that can be used to navigate the WordNet database.
    This browser is launched by `nltk.app.wordnet()`.

    The browser consists of a main window, which is divided into four
    main sections:

      - A "word" section, containing a text entry box for words.
        Pressing the return key in this box, or pressing the "Find
        Word" button, will search for that word in WordNet.

      - A "senses" section, which lists the senses for the most
        recently searched-for word.  This section is divided into
        four sub-sections: one for each of the four wordnet parts of
        speech (noun, verb, adjective, and adverb).  Clicking on any
        of the senses will cause its synset to be displayed in the
        "synset" section of the main window.

      - A "synset" section, which displays the information about the
        most recently selected synset.  This section is divided into
        three sub-sections:
          - A "lemmas" sub-section, which lists the lemmas for the
            synset.
          - A "relations" sub-section, which lists the relations for
            the synset.  Clicking on any of these relations will
            cause the related synset to be displayed.
          - A "gloss" sub-section, which displays the gloss for the
            synset.
    """

    def __init__(self, root, wn):
        self._wn = wn
        self._root = root
        self._root.title("WordNet Browser")

        self._find_word_box = FindWordBox(self, self._root, self._find_word)
        self._senses_box = SensesBox(self, self._root)
        self._synset_box = SynsetBox(self, self._root)
        self._find_word_box.focus()

    def mainloop(self):
        self._root.mainloop()

    # When the find word box gets a word, this is called.
    def _find_word(self, word):
        self._word = word
        self._senses_box.update(word)

    # When a sense is selected, this is called.
    def select_sense(self, sense):
        self._synset_box.update(sense)

    # When a relation is selected, this is called.
    def select_relation(self, relation):
        self._synset_box.update(relation)


######################################################################
# Find Word Box
######################################################################
class FindWordBox(Frame):
    def __init__(self, browser, parent, command):
        Frame.__init__(self, parent, borderwidth=5)
        self.pack(fill="x")
        self._browser = browser
        self._command = command

        Label(self, text="Find word:").pack(side="left")
        self._entry = Entry(self, background="white")
        self._entry.pack(side="left", fill="x", expand=True)
        self._entry.bind("<Return>", self._find)
        self._button = Button(self, text="Find", command=self._find)
        self._button.pack(side="right")

    def _find(self, *e):
        word = self._entry.get()
        if not word:
            return
        self._command(word)
        return "break"

    def focus(self):
        self._entry.focus()


######################################################################
# Synset Box
######################################################################
class SynsetBox(Frame):
    def __init__(self, browser, parent):
        Frame.__init__(self, parent, borderwidth=5)
        self.pack(expand=True, fill="both")
        self._browser = browser

        # Create the top-level frames.
        self._lemmas = Frame(self)
        self._relations = Frame(self)
        self._gloss = Frame(self)
        self._lemmas.pack(fill="x", pady=2)
        self._relations.pack(fill="both", expand=True)
        self._gloss.pack(fill="x", pady=2)

        # Create the widgets for the lemmas frame.
        Label(self._lemmas, text="Lemmas:").pack(side="left")
        self._lemma_list = FlowList(self._lemmas, self._browser._wn)
        self._lemma_list.pack(side="left", expand=True, fill="x")

        # Create the widgets for the relations frame.
        Label(self._relations, text="Relations:", anchor="nw").pack(
            side="top", anchor="nw"
        )
        self._relation_list = RelationList(
            self._relations, self._browser, self._browser._wn
        )
        self._relation_list.pack(side="left", anchor="n", expand=True, fill="both")

        # Create the widgets for the gloss frame.
        self._gloss_label = Label(self._gloss, text="Gloss:", anchor="nw")
        self._gloss_label.pack(side="top", anchor="nw")
        self._gloss_text = scrolledtext.ScrolledText(
            self._gloss,
            height=5,
            state="disabled",
            wrap="word",
            background="#dddddd",
            relief="groove",
            borderwidth=2,
        )
        self._gloss_text.pack(expand=True, fill="both")

    def update(self, synset):
        self._lemma_list.update(synset)
        self._relation_list.update(synset)

        self._gloss_text.config(state="normal")
        self._gloss_text.delete("1.0", "end")
        self._gloss_text.insert("end", synset.definition())
        examples = synset.examples()
        if examples:
            self._gloss_text.insert("end", "\n\nExamples: \n- ")
            self._gloss_text.insert("end", "\n- ".join(examples))
        self._gloss_text.config(state="disabled")


######################################################################
# Senses Box
######################################################################
class SensesBox(Frame):
    def __init__(self, browser, parent):
        Frame.__init__(self, parent, borderwidth=5)
        self.pack(expand=True, fill="both")
        self._browser = browser

        self._pos = {}
        for (pos, name) in (
            (wordnet.NOUN, "Nouns"),
            (wordnet.VERB, "Verbs"),
            (wordnet.ADJ, "Adjectives"),
            (wordnet.ADV, "Adverbs"),
        ):
            frame = Frame(self)
            label = Label(frame, text=f"{name}:", width=12, anchor="n")
            list = SynsetList(frame, self._browser, self._browser._wn)
            label.pack(side="left", anchor="n")
            list.pack(side="left", expand=True, fill="x", anchor="n")
            self._pos[pos] = {"frame": frame, "list": list}

        self._pos[wordnet.NOUN]["frame"].pack(expand=1, fill="x")
        self._pos[wordnet.VERB]["frame"].pack(expand=1, fill="x")
        self._pos[wordnet.ADJ]["frame"].pack(expand=1, fill="x")
        self._pos[wordnet.ADV]["frame"].pack(expand=1, fill="x")

    def update(self, word):
        for pos in self._pos:
            synsets = self._browser._wn.synsets(word, pos)
            self._pos[pos]["list"].update(synsets)


######################################################################
# FlowList: a list of hyperlinked items that flows to fill the
# width of its container.  Used to list the lemmas for a synset.
######################################################################


class FlowList(Frame):
    """
    A frame that contains a list of 'items' (hyperlinked text
    fragments).  The items are arranged to flow from left to right,
    and from top to bottom.
    """

    def __init__(self, parent, wn, **kwargs):
        Frame.__init__(self, parent)
        self._wn = wn
        self._items = []

    def update(self, synset):
        # Clear any old items.
        for item in self._items:
            item.destroy()

        # Add the new items.
        for i, lemma in enumerate(synset.lemmas()):
            if i > 0:
                item = Label(self, text=", ")
                item.pack(side="left")
                self._items.append(item)
            item = HyperlinkedText(
                self, lemma.name().replace("_", " "), self._select_lemma(lemma), "lemma"
            )
            item.pack(side="left")
            self._items.append(item)

    # This should find the word corresponding to this lemma.
    def _select_lemma(self, lemma):
        def thunk(e, l=lemma):
            pass

        return thunk


######################################################################
# SynsetList: A list of hyperlinked synsets.  Used to list the
# senses for a word.
######################################################################


class SynsetList(Frame):
    def __init__(self, parent, browser, wn):
        Frame.__init__(self, parent)
        self._browser = browser
        self._wn = wn
        self._items = []

    def update(self, synsets):
        # Clear any old items.
        for item in self._items:
            item.destroy()

        # Add the new items.
        for synset in synsets:
            item = HyperlinkedText(
                self, synset.definition(), self._select_synset(synset)
            )
            item.pack(anchor="w")
            self._items.append(item)

    def _select_synset(self, synset):
        def thunk(e, s=synset):
            self._browser.select_sense(s)

        return thunk


######################################################################
# RelationList: A list of hyperlinked relations.  Used to list the
# relations for a synset.
######################################################################


class RelationList(Frame):
    # These are taken from the wordnet documentation
    RELATIONS = {
        # Noun relations
        "hypernyms": "Hypernyms",
        "instance_hypernyms": "Instance Hypernyms",
        "hyponyms": "Hyponyms",
        "instance_hyponyms": "Instance Hyponyms",
        "member_holonyms": "Member Holonyms",
        "substance_holonyms": "Substance Holonyms",
        "part_holonyms": "Part Holonyms",
        "member_meronyms": "Member Meronyms",
        "substance_meronyms": "Substance Meronyms",
        "part_meronyms": "Part Meronyms",
        "attributes": "Attributes",
        "entailments": "Entailments",
        "causes": "Causes",
        "also_sees": "Also Sees",
        "verb_groups": "Verb Groups",
        "similar_tos": "Similar Tos",
        "pertainyms": "Pertainyms",
        "derivationally_related_forms": "Derivationally Related Forms",
        # Verb relations
        "antonyms": "Antonyms",
    }
    # This is a list of relation types in an order that seems to make
    # sense.
    RELATION_ORDER = [
        "hypernyms",
        "instance_hypernyms",
        "hyponyms",
        "instance_hyponyms",
        "member_holonyms",
        "substance_holonyms",
        "part_holonyms",
        "member_meronyms",
        "substance_meronyms",
        "part_meronyms",
        "entailments",
        "causes",
        "verb_groups",
        "attributes",
        "similar_tos",
        "also_sees",
        "pertainyms",
        "derivationally_related_forms",
        "antonyms",
    ]

    def __init__(self, parent, browser, wn):
        Frame.__init__(self, parent)
        self._browser = browser
        self._wn = wn
        self._items = []

    def update(self, synset):
        # Clear any old items.
        for item in self._items:
            item.destroy()

        # Add the new items.
        for rel in self.RELATION_ORDER:
            # Get the set of related synsets.
            if hasattr(synset, rel):
                related_synsets = getattr(synset, rel)()
            elif hasattr(synset, rel.replace("_", "")):
                related_synsets = getattr(synset, rel.replace("_", ""))()
            else:
                continue

            if not related_synsets:
                continue

            # Add a label for the relation.
            label = Label(self, text=f"{self.RELATIONS.get(rel, rel)}:")
            label.pack(anchor="w")
            self._items.append(label)

            # Add the list of related synsets.
            list = Frame(self)
            for i, related_syn in enumerate(related_synsets):
                if i > 0:
                    item = Label(list, text=", ")
                    item.pack(side="left")
                    self._items.append(item)
                item = HyperlinkedText(
                    list,
                    related_syn.name(),
                    self._select_synset(related_syn),
                    "synset",
                )
                item.pack(side="left")
                self._items.append(item)
            list.pack(anchor="w", fill="x", expand=True)
            self._items.append(list)

    def _select_synset(self, synset):
        def thunk(e, s=synset):
            self._browser.select_relation(s)

        return thunk


######################################################################
# Helper widgets
######################################################################


class HyperlinkedText(Label):
    """
    A text label that acts as a hyperlink.  When the user clicks on
    it, a given command is called.  When the mouse is over it, it is
    underlined.
    """

    def __init__(self, parent, text, command, type=None):
        if type == "lemma":
            font = "helvetica 12"
        else:
            font = "helvetica 12"
        Label.__init__(self, parent, text=text, fg="blue", font=font)
        self._command = command
        self.bind("<Enter>", self._enter)
        self.bind("<Leave>", self._leave)
        self.bind("<ButtonPress-1>", self._click)
        self._type = type
        self._orig_font = self["font"]
        if self._orig_font is None:
            self._orig_font = "helvetica 12"
        (family, size, style) = self._orig_font.split()
        self._underline_font = f"{family} {size} {style} underline"
        self._orig_cursor = self["cursor"]

    def _enter(self, e):
        self.config(font=self._underline_font, cursor="hand2")

    def _leave(self, e):
        self.config(font=self._orig_font, cursor=self._orig_cursor)

    def _click(self, e):
        self._command(e)


# The default Tkinter Entry widget doesn't let us bind to the
# return key without capturing all events, so we'll just subclass
# it.
try:
    from tkinter import Entry
except ImportError:
    from Tkinter import Entry


class WordNetApp:
    def __init__(self, wordnet_corpus_reader=wordnet):
        """
        Create a new WordNet browser.
        """
        self._wn = wordnet_corpus_reader
        self._root = Tk()
        self._browser = WordNetBrowser(self._root, self._wn)
        self._menubar = self._init_menubar()
        self._root.config(menu=self._menubar)

    def _init_menubar(self):
        menubar = Menu(self._root)

        # File menu
        filemenu = Menu(menubar, tearoff=0)
        filemenu.add_command(label="About", command=self._about)
        filemenu.add_command(label="Quit", command=self._root.destroy)
        menubar.add_cascade(label="File", menu=filemenu)

        # Search menu
        searchmenu = Menu(menubar, tearoff=0)
        searchmenu.add_command(
            label="Find Word", command=self._browser._find_word_box.focus
        )
        menubar.add_cascade(label="Search", menu=searchmenu)

        return menubar

    def _about(self, *e):
        # Get the current version
        try:
            from nltk import __version__ as version
        except ImportError:
            version = "unknown"

        # The about box.
        about = Toplevel(self._root)
        about.title("About the WordNet Browser")
        about.bind("<KeyPress-q>", lambda e: about.destroy())
        about.bind("<KeyPress-Q>", lambda e: about.destroy())
        about.focus()
        msg = [
            "NLTK WordNet Browser",
            "",
            "A graphical interface for browsing the WordNet database.",
            "",
            "NLTK Version: %s" % version,
            "Written by Jussi Salmela and Paul Bone",
            "Ported to Tkinter by Tom Aarsen",
        ]
        Label(
            about,
            text="\n".join(msg),
            foreground="darkblue",
            background="white",
            justify="center",
            font="helvetica 12",
            padx=20,
            pady=10,
        ).pack()
        Button(about, text="Ok", command=about.destroy, padx=20).pack(pady=10)

    def main(self):
        """
        Start the WordNet browser.  This method will not return until
        the browser is closed.
        """
        self._browser.mainloop()
        try:
            self._root.destroy()
        except:
            pass


######################################################################
# The WordNet Browser (HTTP)
######################################################################
# A simple web-browser interface for the wordnet browser.


class WordNetRequestHandler(BaseHTTPRequestHandler):
    """
    A handler for WordNet lookup requests.  This request handler can
    be used to create a simple web interface for browsing WordNet.  It
    generates HTML pages to display information about synsets and
    lemmas.  The following paths are handled by this server:

      - ``/``: The main page.  This page contains a simple form
        asking for a word.  When the user enters a word, it will
        take them to ``/lookup/``*word*.

      - ``/style.css``: A css stylesheet.

      - ``/lookup/``*word*: A page displaying all synsets for *word*.

      - ``/synset/``*synset*: A page displaying the synset *synset*.

      - ``/SHUTDOWN``: Shut down the server.
    """

    def do_GET(self):
        # Let's hope the user isn't using ie5
        html = '<?xml version="1.0" encoding="utf-8"?>\n'
        html += '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '
        html += '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'

        if self.path.endswith("/SHUTDOWN%20THE%20SERVER"):
            self.send_response(200)
            self.send_header("Content-type", "text/plain")
            self.end_headers()
            self.wfile.write(b"Shutting down the server.")
            # This is a rather drastic way to kill the server.
            # But it's the simplest thing that works.
            os._exit(0)
            return

        if self.path == "/":
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            html += self._formpage()
            self.wfile.write(html.encode("utf8"))
            return

        if self.path == "/style.css":
            self.send_response(200)
            self.send_header("Content-type", "text/css")
            self.end_headers()
            html = self._style()
            self.wfile.write(html.encode("utf8"))
            return

        match = re.match(r"/lookup/(.*)", self.path)
        if match:
            word = unquote_plus(match.group(1))
            synsets = wordnet.synsets(word)
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            html += self._wordpage(word, synsets)
            self.wfile.write(html.encode("utf8"))
            return

        match = re.match(r"/synset/(.*)", self.path)
        if match:
            try:
                synset = wordnet.synset(match.group(1))
                self.send_response(200)
                self.send_header("Content-type", "text/html")
                self.end_headers()
                html += self._synsetpage(synset)
                self.wfile.write(html.encode("utf8"))
            except:
                self.send_error(404, f"Synset not found: {match.group(1)}")
            return

        self.send_error(404, f"File Not Found: {self.path}")

    def _style(self):
        return """
        h1, h2, h3, h4, h5, h6 { margin-bottom: 0; }
        body {
            font-family: Arial, Helvetica, sans-serif;
            font-size: 90%;
            margin-left: 5%;
            margin-right: 5%;
        }
        div.synset {
            border: 1px solid #000;
            padding: 1ex;
            margin-top: 1em;
            margin-bottom: 1em;
        }
        div.header {
            background: #ddd;
            padding: 1ex;
            font-weight: bold;
        }
        div.relations {
            padding: 1ex;
        }
        div.gloss {
            padding: 1ex;
            font-style: italic;
        }
        div.relation_lemmas {
            padding-left: 2em;
        }
        div.examples {
            padding-left: 2em;
            font-style: normal;
        }
        a:link {
            text-decoration: none;
            color: #00c;
        }
        a:visited {
            text-decoration: none;
            color: #507;
        }
        a:hover {
            text-decoration: underline;
            color: #00c;
        }
        """

    def _formpage(self):
        html = f"""
        <html>
          <head>
            <title>WordNet Search</title>
            <link rel="stylesheet" type="text/css" href="style.css" />
            <script type="text/javascript">
              function do_lookup() {{
                  var word = document.getElementById("word").value;
                  if (word != "") {{
                      parent.location="/lookup/"+word;
                  }}
              }}
            </script>
          </head>
          <body>
            <h1>WordNet Search</h1>
            <div class="synset">
              <div class="header">Find a word</div>
              <div class="relations">
                  Word:
                  <input type="text" name="word" id="word" />
                  <input type="button" value="Look up" onclick="do_lookup()"/>
              </div>
            </div>
            <p>
              <a href="https://wordnet.princeton.edu/">
                About WordNet
              </a>
              <br/>
              <a href="https://www.nltk.org/">
                About NLTK
              </a>
            </p>
            <script type="text/javascript">
                document.getElementById('word').onkeypress =
                    function(e) {{
                        if (!e) e = window.event;
                        if (e.keyCode == '13') {{
                            do_lookup();
                            return false;
                        }}
                    }}
                document.getElementById('word').focus()
            </script>
          </body>
        </html>
        """
        return html

    def _wordpage(self, word, synsets):
        html = f"""
        <html>
          <head>
            <title>Synsets for {word}</title>
            <link rel="stylesheet" type="text/css" href="style.css" />
          </head>
          <body>
        """
        html += '<h1>Synsets for "%s"</h1>' % word
        for synset in synsets:
            html += self._synsetdiv(synset)
        if not synsets:
            html += f"No synsets found for {word}."
        html += """
            <p><a href="/">New Search</a></p>
          </body>
        </html>
        """
        return html

    def _synsetpage(self, synset):
        html = f"""
        <html>
          <head>
            <title>Synset: {synset.name()}</title>
            <link rel="stylesheet" type="text/css" href="/style.css" />
          </head>
          <body>
            <h1>Synset: {synset.name()}</h1>
            {self._synsetdiv(synset, verbose=True)}
            <p><a href="/">New Search</a></p>
          </body>
        </html>
        """
        return html

    def _synsetdiv(self, synset, verbose=False):
        html = ""
        html += '<div class="synset">'

        html += '<div class="header">'
        html += ", ".join(
            f'<a href="/lookup/{l.name()}">{l.name()}</a>'
            for l in synset.lemmas()[:5]
        )
        if len(synset.lemmas()) > 5:
            html += f", etc..."
        html += "</div>"

        html += '<div class="gloss">'
        html += synset.definition()
        if synset.examples():
            html += '<div class="examples">'
            for ex in synset.examples():
                html += f"“{ex}” "
            html += "</div>"
        html += "</div>"

        if verbose:
            html += self._relationsdiv(synset)
        else:
            html += (
                '<div align="right"><a href="/synset/'
                + synset.name()
                + '">more...</a></div>'
            )

        html += "</div>"

        return html

    def _relationsdiv(self, synset):
        html = '<div class="relations">'
        # A list of relations to check for.  This is the same list
        # that the graphical browser uses.
        relation_order = RelationList.RELATION_ORDER
        # A mapping from relation method name to display name.
        relation_names = RelationList.RELATIONS

        # Display the relations.
        for rel in relation_order:
            if hasattr(synset, rel):
                related = getattr(synset, rel)()
            elif hasattr(synset, rel.replace("_", "")):
                related = getattr(synset, rel.replace("_", ""))()
            else:
                continue

            if not related:
                continue

            html += f'<div class="relation_header">{relation_names.get(rel, rel)}</div>'
            html += '<div class="relation_lemmas">'
            html += ", ".join(
                f'<a href="/synset/{s.name()}">{s.name()}</a>' for s in related
            )
            html += "</div>"

        html += "</div>"
        return html


def serve(wordnet_corpus_reader=wordnet, port=8000):
    """
    Start a simple web server on a given port, that can be used to
    browse WordNet.
    """
    try:
        if port == 0:
            port = 8000
        server = HTTPServer(("", port), WordNetRequestHandler)
    except OSError as e:
        print(f"Error: Port {port} is already in use.")
        print("Please choose a different port.")
        sys.exit(1)

    print(f"WordNet browser server running on http://{server.server_name}:{server.server_port}")
    print("Enter 'SHUTDOWN' to stop the server.")
    print('Alternatively, you can send a GET request to "/SHUTDOWN THE SERVER"')

    # Automatically open a web browser on the server's main page.
    url = f"http://localhost:{port}"
    try:
        webbrowser.open(url)
        print(f"Your web browser has been opened to {url}")
    except:
        print(f"Please open your web browser to {url}")

    # The server's main loop.  This will process requests until we
    # tell it to stop.
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("Shutting down the server.")
        server.socket.close()


def demo(wordnet_corpus_reader=wordnet):
    """
    Create and run a new WordNet browser.
    """
    WordNetApp(wordnet_corpus_reader).main()


def main():
    # If a command-line argument is given, then use it as the port
    # for an HTTP server.
    if len(sys.argv) > 1:
        try:
            port = int(sys.argv[1])
            serve(port=port)
        except ValueError:
            print("Usage: wordnet_app.py [PORT]")
    # Otherwise, run the graphical demo.
    else:
        # Check that the user has a screen.
        if "DISPLAY" not in os.environ:
            # Fall back on the web-based browser.
            print("No DISPLAY environment variable specified.")
            print("Falling back on the web-based browser.")
            print("(Tkinter-based WordNet browser requires a display.)")
            print()
            serve()
        # Otherwise, run the graphical browser.
        else:
            demo()


if __name__ == "__main__":
    main()

Patched code sample

import os
import re
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from urllib.parse import parse_qs, urlparse

from nltk.corpus import wordnet

# This is a simplified representation of the application structure for clarity.
# The core change is the removal of the vulnerable 'if self.path == "/SHUTDOWN THE SERVER":'
# block from the do_GET method. The code below reflects the state *after* the fix.

_STYLE = """
... CSS styles would be here ...
"""

_SCRIPT = """
... JavaScript would be here ...
"""

_HELP = """
... HTML help content would be here ...
"""

class WordNetRequestHandler(BaseHTTPRequestHandler):
    """
    The HTTP request handler for the WordNet browser.
    """

    def do_GET(self):
        """
        GET requests are used to serve the main page, and to handle
        searches.
        """
        # This is the fixed version. The following vulnerable block was removed:
        #
        # if self.path == "/SHUTDOWN THE SERVER":
        #     print("Server shutting down.")
        #     # a non-graceful shutdown is all that we need here
        #     os._exit(0)

        if self.path == "/":
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(b"<html><head>")
            self.wfile.write(b"<title>WordNet Browser</title>")
            self.wfile.write(b'<link rel="stylesheet" type="text/css" ')
            self.wfile.write(b'href="style.css" />')
            self.wfile.write(b"</head><body>")
            self.wfile.write(_HELP.encode("utf8"))
            self.wfile.write(b"</body></html>")

        elif self.path == "/style.css":
            self.send_response(200)
            self.send_header("Content-type", "text/css")
            self.end_headers()
            self.wfile.write(_STYLE.encode("utf8"))

        elif self.path.startswith("/img/"):
            img_path = os.path.join(os.path.dirname(__file__), self.path[1:])
            if os.path.exists(img_path):
                self.send_response(200)
                if img_path.endswith(".png"):
                    self.send_header("Content-type", "image/png")
                elif img_path.endswith(".gif"):
                    self.send_header("Content-type", "image/gif")
                else:
                    self.send_header("Content-type", "image/jpeg")
                self.end_headers()
                with open(img_path, "rb") as f:
                    self.wfile.write(f.read())
            else:
                self.send_error(404, "Image not found")

        elif self.path.startswith("/search"):
            query = parse_qs(urlparse(self.path).query)
            # ... The rest of the search handling logic would be here ...
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(b"<html><body>Search results would appear here.</body></html>")

        else:
            self.send_error(404, "File not found")

Cite this entry

@misc{vaitp:cve202633231,
  title        = {{NLTK's WordNet Browser is vulnerable to unauthenticated remote shutdown.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-33231},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-33231/}}
}
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 ::