I have been instructed to forward the attached security report to you privately. As far as I can tell, it is accurate, and the vulnerability is still present in the current sources. My understanding is that docutils offers a processing mode that is robust in the presence of malicious inputs, which is why a trust boundary is crossed.
Please let me know how you want to handle this.
An automatically generated proper reproducer looks like this:
#!/usr/bin/env python3
"""Reproducer for XSS in PEP email masking transform (mask_email)."""
import sys
sys.path.insert(0, 'docutils')
from docutils.core import publish_parts
src = """\
PEP: 9999
Title: Test
Author: `"><img src=x onerror=alert(1)> <evil@example.com>`_
Status: Draft
Type: Standards Track
Created: 01-Jan-2026
Abstract
========
Test.
"""
parts = publish_parts(source=src, reader_name='pep', writer_name='html')
body = parts['html_body']
if '<img src=x onerror=alert(1)>' in body:
print('VULNERABLE: unescaped HTML found in output')
print()
for line in body.splitlines():
if 'onerror' in line:
print(' ', line.strip())
sys.exit(1)
else:
print('OK: no unescaped HTML in output')
sys.exit(0)
The reconstructed patch is:
Index: docutils/docutils/transforms/peps.py
===================================================================
--- docutils/docutils/transforms/peps.py (revision 10391)
+++ docutils/docutils/transforms/peps.py (working copy)
@@ -303,8 +303,8 @@
if ref['refuri'][8:] in non_masked_addresses:
replacement = ref[0]
else:
- replacement_text = ref.astext().replace('@', ' at ')
- replacement = nodes.raw('', replacement_text, format='html')
+ replacement_text = ref.astext().replace('@', ' at ')
+ replacement = nodes.Text(replacement_text)
if pepno is None:
return replacement
else:
We received another closely related report.
I've been instructed to mention: Found by AISLE in partnership with Red Hat
The reconstructed reproducer:
The suggested fix:
Thank you for the report. You found a real (but limited) problem in Docutils.
The Docutils Security documentation explicitely states: "Docutils does not come in a through-the-web secure state, because this would inconvenience ordinary users."
OTOH, Docutils offers the “raw_enabled” setting which turns off the "raw" directive. A user should expect that with
raw-enabled: False, no raw HTML could bypass the rST->HTML document conversion.I did an analysis of the problem and created a patch.
Core problem:
The
transforms.peps.mask_email()function is used to "mask the email address inrefand return a replacement node". Unfortunately and errouneously, the replacement text is put in a<raw>node without escaping special HTML characters. This is both, incorrect and insecure behaviour.Limited impact:
The problem only concerns the conversion of Python Enhancement Proposals (PEPs) from rST source to HTML and only the "Author" and "Discussions-To" fields of the header.
Before PEPs are accepted and published, they must undergo a review process that is very likely to spot the malicious intention of an "author" submitting a PEP with evil HTML in the "Author:" or "Discussions-To" fields of the bibliographic info.
Cf. https://peps.python.org/pep-0001/#submitting-a-pep
Web services using Docutils or Sphinx-doc (like readthedocs) do not use the PEP transforms.
This may also explain why this problem was not discovered yet despite existing since 2002.
In my understanding, this means there is no risk in publishing the information about this vulnerability. It should suffice to commit the patch to the development repository and publish the fix with the next release.
Thank. Please proceed to make this issue public (if that's what you want to do). Red Hat does not require an embargo for this.
The issue is fixed in [r10394].
Thank you again for reporting.
Related
Commit: [r10394]