Średnia

wger has Stored XSS via Unescaped License Attribution Fields

  • Ujawniono: 16.04.2026
  • Aktualizacja: 11.10.2026 14:17

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
  3. 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

Czego dotyczy

Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.

PlatformaPaczkaPodatne wersjePoprawka w wersji
Python (PyPI) wger <= 2.4 brak poprawki

Odnośniki

Ocena CVSS 5.4/10
Wektor CVSS
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N
Słabość (CWE)
CWE-79
Platforma
Python (PyPI)
Paczka
wger
Ujawniono
16.04.2026
Aktualizacja
11.10.2026 14:17

Źródła i licencje

OSV / GitHub Advisory Database - PYSEC-2026-3419 - PyPA Advisory Database; CC BY 4.0

OSV / GitHub Advisory Database - GHSA-6f54-qjvm-wwq3 - CC BY 4.0

Historia podatności: wger

Wszystkie opisane podatności tej paczki (Python (PyPI)), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.

Wszystkie (20) w archiwum
Wszystkich wpisów
20
Pierwsza → ostatnia
11.2022 10.2026
Bez poprawki
16
Wykorzystywana w atakach
0

Rozkład istotności

  • Krytyczna 2
  • Wysoka 8
  • Średnia 9
  • Niska 1

Ujawnienia w latach

  1. 2022: 1
  2. 2023: 2
  3. 2024: 0
  4. 2025: 0
  5. 2026: 17
Istotność:
  1. Wysoka · 7.1 CVE-2026-46434 <= 2.1 brak poprawki
  2. Wysoka · 7.1 CVE-2026-43976 <= 2.1 brak poprawki
  3. wger has Stored XSS via Unescaped License Attribution Fields ten wpis
    Średnia · 5.4 CVE-2026-40353 <= 2.4 brak poprawki
  4. Wysoka · 7.6 CVE-2026-40474 <= 2.1 brak poprawki
  5. Krytyczna · 9.8 CVE-2022-2650 < 2.2 2.2

Dowiedz się, które podatności dotyczą Twojej aplikacji

Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.

Podłącz swoją aplikację
Newsletter DockRay

Bezpieczeństwo aplikacji w Twojej skrzynce

Najważniejsze podatności, konkretne zalecenia i praktyczne wskazówki dla Twoich aplikacji. Wybierz technologie i śledź to, co ma znaczenie dla Twojego projektu.

  • Codzienne zestawienie nowych podatności, także krytycznych.
  • Powiadomienia o ważnych aktualizacjach.
  • Praktyczne porady o bezpieczeństwie i monitoringu aplikacji.

Pozostaw puste, aby otrzymywać informacje o wszystkich technologiach.

Zapis jest bezpłatny. Wypiszesz się jednym kliknięciem w wiadomości.