CVE-2026-40353
wger: Stored XSS via Unescaped License Attribution Fields
CVE-2026-40353 is a medium vulnerability (CVSS 5.1) in Wger, published April 16, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.21%, the 10th percentile. No source names a fixed version.
wger is a free, open-source workout and fitness manager. In versions 2.5 and below, the attribution_link property in AbstractLicenseModel constructs HTML by directly interpolating user-controlled license fields (such as license_author) without escaping, and templates render the result using Django's |safe filter. An authenticated user can create an ingredient with a malicious license_author value containing JavaScript, which executes in the browser of any visitor viewing the ingredient page, resulting in stored XSS. This issue has been fixed in version 2.5.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- Passive
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
5.1
wger: Stored XSS via Unescaped License Attribution Fields
wger is a free, open-source workout and fitness manager. In versions 2.5 and below, the attribution_link property in AbstractLicenseModel constructs HTML by directly interpolating user-controlled license fields (such as license_author) without escaping, and templates render the result using Django's |safe filter. An authenticated user can create an ingredient with a malicious license_author value containing JavaScript, which executes in the browser of any visitor viewing the ingredient page, resulting in stored XSS. This issue has been fixed in version 2.5.
- Record
CVE-2026-40353- CVSS 4.0
- 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 - Weakness
- CWE-79
- Updated
- 2026-11-02
EUVDEUVD-2026-23561
5.1
wger is a free, open-source workout and fitness manager. In versions 2.5 and below, the attribution_link property in AbstractLicenseModel constructs HTML by directly interpolating user-controlled license fields (such as license_author) without escaping, and templates render the result using Django's |safe filter. An authenticated user can create an ingredient with a malicious license_author value containing JavaScript, which executes in the browser of any visitor viewing the ingredient page, resulting in stored XSS. This issue has been fixed in version 2.5.
- Record
EUVD-2026-23561- CVSS 4.0
- 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 - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-6f54-qjvm-wwq3
5.1
wger has Stored XSS via Unescaped License Attribution Fields
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 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
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
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…
- Set
- Record
GHSA-6f54-qjvm-wwq3- CVSS 4.0
- 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 - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
NVD
5.1
wger is a free, open-source workout and fitness manager. In versions 2.5 and below, the attribution_link property in AbstractLicenseModel constructs HTML by directly interpolating user-controlled license fields (such as license_author) without escaping, and templates render the result using Django's |safe filter. An authenticated user can create an ingredient with a malicious license_author value containing JavaScript, which executes in the browser of any visitor viewing the ingredient page, resulting in stored XSS. This issue has been fixed in version 2.5.
- Record
CVE-2026-40353- CVSS 4.0
- 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 - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSV
5.1
wger: Stored XSS via Unescaped License Attribution Fields
wger is a free, open-source workout and fitness manager. In versions 2.5 and below, the attribution_link property in AbstractLicenseModel constructs HTML by directly interpolating user-controlled license fields (such as license_author) without escaping, and templates render the result using Django's |safe filter. An authenticated user can create an ingredient with a malicious license_author value containing JavaScript, which executes in the browser of any visitor viewing the ingredient page, resulting in stored XSS. This issue has been fixed in version 2.5.
- Record
CVE-2026-40353- CVSS 4.0
- 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 - Weakness
- CWE-79
- Updated
- 2026-11-02
OSVGHSA-6f54-qjvm-wwq3
5.1
wger has Stored XSS via Unescaped License Attribution Fields
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 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
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
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…
- Set
- Record
GHSA-6f54-qjvm-wwq3- CVSS 4.0
- 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 - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Weakness
- CWE-79
- Updated
- 2026-11-02
OSVPYSEC-2026-3419
5.1
wger has Stored XSS via Unescaped License Attribution Fields
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 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
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
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…
- Set
- Record
PYSEC-2026-3419- CVSS 4.0
- 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 - CVSS 3.1
- 5.4 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N - Updated
- 2026-11-02
Affected products
Affected packages
History
| When | Changed | Sources |
|---|---|---|
| 2026-11-02 | created | CVE List, EUVD, GitHub Advisories, NVD, OSV |
References
github.com/wger-project/wger/security/advisories/GHSA-6f54-qjvm-wwq3
github.com/wger-project/wger/releases/tag/2.5
nvd.nist.gov/vuln/detail/CVE-2026-40353
github.com/advisories/GHSA-6f54-qjvm-wwq3
github.com/CVEProject/cvelistV5/tree/main/cves/2026/40xxx/CVE-2026-40353.json
github.com/wger-project/wger
pypi.org/project/wger
Left out here: the rest of 3 source descriptions. The JSON record has them.