GitHub Security Advisory: GHSA-6f54-qjvm-wwq3
Package: wger (PIP)
Severity: MODERATE (CVSS 5.4)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: <= 2.4
Patched in: unpatched

Summary: wger has Stored XSS via Unescaped License Attribution Fields

Details:
# 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