Python
CVE-2026-40353
MEDIUM
Severity by source
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
Lifecycle Timeline
4DescriptionGitHub 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
@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 outUnsafe 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
class IngredientCreateView(WgerFormMixin, CreateView):
model = Ingredient
form_class = IngredientForm
# includes license_author fieldURL: 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
attribution_linkconstructs raw HTML by string interpolation of user-controlled fields without callingdjango.utils.html.escape()ordjango.utils.html.format_html()- The template renders the result with
|safe, bypassing Django's auto-escaping - The
license_authorfield inIngredientFormhas no input sanitization - 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
- Register/login to a wger instance
- Create a malicious ingredient via the web form at
/en/nutrition/ingredient/add/:
- Set
Nameto any valid name (e.g., "XSS Form Verified") - Set
Energyto125,Proteinto10,Carbohydratesto10,Fatto5(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
- 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:
<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):
# 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():
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.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
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
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same weakness CWE-79 – Cross-site Scripting (XSS)
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-6f54-qjvm-wwq3