Skip to main content

Python CVE-2026-40353

MEDIUM
Cross-site Scripting (XSS) (CWE-79)
2026-04-16 https://github.com/wger-project/wger GHSA-6f54-qjvm-wwq3
5.1
CVSS 4.0 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
5.1 MEDIUM
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Primary rating from GitHub Advisory · only source for this CVE.

CVSS VectorGitHub Advisory

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
P
Scope
X

Lifecycle Timeline

4
CVSS changed
Apr 17, 2026 - 22:22 NVD
5.1 (MEDIUM)
Analysis Generated
Apr 16, 2026 - 01:50 vuln.today
Analysis Generated
Apr 16, 2026 - 01:45 vuln.today
CVE Published
Apr 16, 2026 - 01:37 nvd
MEDIUM 5.1

DescriptionGitHub Advisory

Stored XSS via Unescaped License Attribution Fields

Summary

The AbstractLicenseModel.attribution_link property in wger/utils/models.py constructs HTML strings by directly interpolating user-controlled fields (license_author, license_title, license_object_url, license_author_url, license_derivative_source_url) without any escaping. The resulting HTML is rendered in the ingredient view template using Django's |safe filter, which disables auto-escaping. An authenticated user can create an ingredient with a malicious license_author value containing JavaScript, which executes when any user (including unauthenticated visitors) views the ingredient page.

Severity

High (CVSS 3.1: ~7.6)

  • Low-privilege attacker (any authenticated non-temporary user)
  • Stored XSS - persists in database
  • Triggers on a public page (no authentication needed to view)
  • Can steal session cookies, perform actions as other users, redirect to phishing

CWE

CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Affected Components

Vulnerable Property

File: wger/utils/models.py:88-110

python
@property
def attribution_link(self):
    out = ''
    if self.license_object_url:
        out += f'<a href="{self.license_object_url}">{self.license_title}</a>'
    else:
        out += self.license_title
# NO ESCAPING
    out += ' by '
    if self.license_author_url:
        out += f'<a href="{self.license_author_url}">{self.license_author}</a>'
    else:
        out += self.license_author
# NO ESCAPING
    out += f' is licensed under <a href="{self.license.url}">{self.license.short_name}</a>'
    if self.license_derivative_source_url:
        out += (
            f'/ A derivative work from <a href="{self.license_derivative_source_url}">the '
            f'original work</a>'
        )
    return out

Unsafe Template Rendering

File: wger/nutrition/templates/ingredient/view.html

  • Line 171: {{ ingredient.attribution_link|safe }}
  • Line 226: {{ image.attribution_link|safe }}

Writable Entry Point

File: wger/nutrition/views/ingredient.py:154-175

python
class IngredientCreateView(WgerFormMixin, CreateView):
    model = Ingredient
    form_class = IngredientForm
# includes license_author field

URL: login_required(ingredient.IngredientCreateView.as_view()) - any authenticated non-temporary user.

Form fields (from wger/nutrition/forms.py:295-313): includes license_author (TextField, max_length=3500) - no sanitization.

Models Affected

6 models inherit from AbstractLicenseModel:

  • Exercise, ExerciseImage, ExerciseVideo, Translation (exercises module)
  • Ingredient, Image (nutrition module)

Only the Ingredient and nutrition Image models' attribution links are currently rendered with |safe in templates.

Root Cause

  1. attribution_link constructs raw HTML by string interpolation of user-controlled fields without calling django.utils.html.escape() or django.utils.html.format_html()
  2. The template renders the result with |safe, bypassing Django's auto-escaping
  3. The license_author field in IngredientForm has no input sanitization
  4. The set_author() method only sets a default value if the field is empty - it does not sanitize user-provided values

Reproduction Steps (Verified)

Prerequisites

  • A wger instance with user registration enabled (default)
  • An authenticated user account (non-temporary)

Steps

  1. Register/login to a wger instance
  2. Create a malicious ingredient via the web form at /en/nutrition/ingredient/add/:
  • Set Name to any valid name (e.g., "XSS Form Verified")
  • Set Energy to 125, Protein to 10, Carbohydrates to 10, Fat to 5 (energy must approximately match macros)
  • Set Author(s) (license_author) to:
     <img src=x onerror="alert(document.cookie)">
  • Submit the form - the form validates and saves successfully with no sanitization
  1. View the ingredient page (public URL, no auth needed):
  • Navigate to the newly created ingredient's detail page
  • The XSS payload executes in the browser

Verified PoC Output

The rendered HTML in the ingredient detail page (line 171 of ingredient/view.html) contains:

html
<small>
     by <img src=x onerror=alert(1)> is licensed under <a href="https://creativecommons.org/licenses/by-sa/3.0/deed.en">CC-BY-SA 3</a>
</small>

The <img> tag with onerror handler is injected directly into the page DOM and executes JavaScript when the browser attempts to load the non-existent image.

Alternative API Path (ExerciseImage)

For users who are "trustworthy" (account >3 weeks old + verified email):

bash
# Upload exercise image with XSS in license_author
curl -X POST https://wger.example.com/api/v2/exerciseimage/ \
  -H "Authorization: Token <token>" \
  -F "exercise=1" \
  -F "image=@photo.jpg" \
  -F 'license_author=<img src=x onerror="alert(document.cookie)">' \
  -F "license=2"

Note: ExerciseImage's attribution_link is not currently rendered with |safe in exercise templates, but the data is stored with XSS payloads and would execute if any template renders it with |safe in the future. The API serializer also returns the unescaped attribution_link data, which could cause XSS in API consumers (mobile apps, SPAs).

Impact

  • Session hijacking: Steal admin session cookies to gain full control
  • Account takeover: Modify other users' passwords or email addresses
  • Data theft: Access other users' workout plans, nutrition data, and personal measurements
  • Worm-like propagation: Malicious ingredient could inject XSS that creates more malicious ingredients
  • Phishing: Redirect users to fake login pages

Suggested Fix

Replace the attribution_link property with properly escaped HTML using Django's format_html():

python
from django.utils.html import format_html, escape

@property
def attribution_link(self):
    parts = []

    if self.license_object_url:
        parts.append(format_html('<a href="{}">{}</a>', self.license_object_url, self.license_title))
    else:
        parts.append(escape(self.license_title))

    parts.append(' by ')

    if self.license_author_url:
        parts.append(format_html('<a href="{}">{}</a>', self.license_author_url, self.license_author))
    else:
        parts.append(escape(self.license_author))

    parts.append(format_html(
        ' is licensed under <a href="{}">{}</a>',
        self.license.url, self.license.short_name
    ))

    if self.license_derivative_source_url:
        parts.append(format_html(
            '/ A derivative work from <a href="{}">the original work</a>',
            self.license_derivative_source_url
        ))

    return mark_safe(''.join(str(p) for p in parts))

Alternatively, remove the |safe filter from the templates and escape in the property, though this would break the anchor tags.

References

AnalysisAI

Stored cross-site scripting (XSS) in wger fitness application allows authenticated users to inject malicious JavaScript via unescaped license attribution fields in ingredient and image models, which executes when any visitor views the affected page. The vulnerability persists in the database and can be exploited to steal session cookies, perform unauthorized actions as other users, or conduct phishing attacks. Affected versions allow low-privilege authenticated users (any non-temporary account) to create ingredients with JavaScript payloads in the license_author field, which bypasses all input sanitization and is rendered with Django's |safe filter, disabling auto-escaping.

Technical ContextAI

The vulnerability resides in wger's AbstractLicenseModel.attribution_link property in wger/utils/models.py, which constructs HTML strings by directly concatenating user-controlled fields (license_author, license_title, license_object_url, license_author_url, license_derivative_source_url) without calling Django's escape() or format_html() functions. The property returns a raw HTML string that is rendered in templates using Django's |safe filter (in wger/nutrition/templates/ingredient/view.html at lines 171 and 226), which explicitly disables Django's automatic context-aware escaping. The root cause is CWE-79 (Improper Neutralization of Input During Web Page Generation): the application fails to neutralize user-supplied data before rendering it in an HTML context. This affects six models inheriting from AbstractLicenseModel (Exercise, ExerciseImage, ExerciseVideo, Translation, Ingredient, Image), though only Ingredient and Image models currently render the vulnerable attribution_link property with |safe. The writable entry point is the IngredientCreateView and related forms in wger/nutrition/forms.py, which include the license_author TextField (max_length=3500) without any sanitization or validation beyond length checks.

RemediationAI

The primary remediation is to replace the vulnerable attribution_link property in wger/utils/models.py with Django's format_html() function to properly escape user-controlled fields while preserving safe HTML anchor tags for license URLs. Specifically, replace the string interpolation approach with format_html() calls for each URL field and escape() calls for text fields (license_author, license_title), then use mark_safe() to return the final safe string. Alternatively, remove the |safe filter from templates (wger/nutrition/templates/ingredient/view.html lines 171 and 226) and ensure escaping is applied in the property itself. A complete fix requires both: (1) modifying the property to use format_html()/escape() instead of f-string interpolation, and (2) removing or conditionalizing the |safe filter in templates. A shorter-term compensating control is to apply Django's escape_html() or bleach library to the license_author form field in wger/nutrition/forms.py at field definition, explicitly stripping HTML tags and encoding special characters on form submission; however, this does not address the root cause in the model property and does not protect the API endpoint. The advised fix from the advisory is to use format_html() with proper escaping for all user-controlled fields. See the GitHub advisory GHSA-6f54-qjvm-wwq3 for the exact patched code and version bump information.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

CVE-2026-40353 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy