CVE-2026-42197 Overview
CVE-2026-42197 is a stored cross-site scripting (XSS) vulnerability in RELATE, a web-based courseware package. The flaw exists in the get_user() method of ParticipationAdmin, which renders user-controlled first_name and last_name profile fields using mark_safe combined with Python's % string formatting. This bypasses Django's automatic HTML escaping. Any enrolled student can inject JavaScript payloads through the profile page (/profile/). When an administrator views the Participation list in the Django admin panel, the payload executes in the admin's browser session. The issue is tracked under [CWE-79] and fixed in commit 555f0efb1c5bd7531c07cd73724d7e566a81f620.
Critical Impact
A low-privileged authenticated student can achieve full administrator account takeover by executing arbitrary JavaScript in the admin's authenticated session.
Affected Products
- RELATE courseware (inducer/relate)
- All versions prior to commit 555f0efb1c5bd7531c07cd73724d7e566a81f620
- Django admin interface integrations exposing ParticipationAdmin
Discovery Timeline
- 2026-05-27 - CVE-2026-42197 published to NVD
- 2026-05-27 - Last updated in NVD database
Technical Details for CVE-2026-42197
Vulnerability Analysis
The vulnerability resides in course/admin.py within the ParticipationAdmin.get_user() method. The method constructs an HTML anchor tag by concatenating a URL with the user's full name and wraps the result in Django's mark_safe(). Because mark_safe() instructs Django to skip HTML escaping, any HTML or JavaScript embedded in the user's name fields is rendered verbatim in the admin panel. The get_full_name() return value derives directly from the first_name and last_name columns of the User model, which authenticated users edit freely via the profile page with no server-side sanitization.
Exploitation requires only a standard enrolled account. The attacker updates their profile name to include a script payload, then waits for an administrator to load the Participation list view. The injected script executes with the administrator's session cookies, enabling session theft, privilege escalation, course content tampering, or further lateral actions inside the Django admin.
Root Cause
The root cause is unsafe HTML construction using mark_safe combined with %-style string formatting on attacker-controlled input. Django provides format_html() specifically to prevent this pattern by escaping interpolated arguments while preserving the literal template markup.
Attack Vector
The attack vector is network-based and authenticated. An enrolled student submits a malicious payload through /profile/ in the first_name or last_name field. The payload persists in the database and triggers when an administrator browses the Participation admin list. User interaction from the admin is required, and the scope changes because the attacker's input crosses the privilege boundary into the administrator's browser context.
# Patch applied in course/admin.py
# Source: https://github.com/inducer/relate/commit/555f0efb1c5bd7531c07cd73724d7e566a81f620
description=pgettext("real name of a user", "Name"),
ordering="user__last_name",
)
-def get_user(self, obj):
+def get_user(self, obj: Participation):
from django.conf import settings
from django.urls import reverse
- from django.utils.html import mark_safe
-
- return mark_safe(string_concat(
- "<a href='%(link)s'>", "%(user_fullname)s",
- "</a>"
- ) % {
- "link": reverse(
- "admin:{}_change".format(
- settings.AUTH_USER_MODEL.replace(".", "_").lower()),
- args=(obj.user.id,)),
- "user_fullname": obj.user.get_full_name(
- force_verbose_blank=True),
- })
+ from django.utils.html import format_html
+
+ return format_html(
+ "<a href='{}'>{}</a>",
+ reverse(
+ "admin:{}_change".format(
+ settings.AUTH_USER_MODEL.replace(".", "_").lower()),
+ args=(obj.user.id,)),
+ obj.user.get_full_name(force_verbose_blank=True),
+ )
The fix replaces mark_safe plus % formatting with Django's format_html(), which auto-escapes each interpolated argument. See the GitHub Security Advisory for additional context.
Detection Methods for CVE-2026-42197
Indicators of Compromise
- User records where first_name or last_name contain HTML tags such as <script, <img, <svg, or event handlers like onerror= and onload=.
- Profile update requests to /profile/ containing angle brackets, encoded payloads (%3Cscript), or JavaScript URI schemes.
- Anomalous outbound requests from administrator browsers to attacker-controlled domains shortly after viewing the Participation admin list.
- Unexpected admin actions (permission changes, new staff users) following access to /admin/course/participation/.
Detection Strategies
- Query the auth_user table for non-printable characters or HTML control characters in name fields: SELECT id, username, first_name, last_name FROM auth_user WHERE first_name ~ '[<>]' OR last_name ~ '[<>]';.
- Review Django request logs for POST traffic to /profile/ containing payload signatures associated with XSS.
- Correlate administrator visits to the Participation admin view with subsequent anomalous session activity.
Monitoring Recommendations
- Enable Django audit logging on profile updates and admin views to capture the editor, target field, and submitted value.
- Deploy a Content Security Policy (CSP) with script-src 'self' to block inline script execution and surface CSP violation reports.
- Alert on creation of new superuser or staff accounts and on permission elevation events in Django admin.
How to Mitigate CVE-2026-42197
Immediate Actions Required
- Upgrade RELATE to a build that includes commit 555f0efb1c5bd7531c07cd73724d7e566a81f620 or later.
- Audit all existing auth_user records and sanitize or reset any first_name and last_name values containing HTML or script content.
- Invalidate active administrator sessions and rotate credentials for any admin that recently accessed the Participation list view.
Patch Information
The maintainer fixed the issue in commit 555f0efb1c5bd7531c07cd73724d7e566a81f620 by replacing mark_safe with Django's format_html() helper. Deployment requires pulling the latest RELATE source, reinstalling the package, and restarting the application server. Review the GitHub Security Advisory GHSA-37xm-vhx8-g6w3 for full advisory details.
Workarounds
- Apply server-side validation on the profile endpoint to reject first_name and last_name values containing <, >, or quote characters until the patch is deployed.
- Restrict access to the Django admin Participation view to a hardened workstation or VPN segment to reduce exposure.
- Enforce a strict Content Security Policy that disallows inline scripts in the Django admin interface.
# Example CSP header for the Django admin via reverse proxy (nginx)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always;
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

