Skip to content

Unsanitized Stream Title Injection on /streamer

High
ornicar published GHSA-v7gh-939r-pfjq Apr 2, 2026

Package

lichess-org/lila (lila)

Affected versions

All current versions (no fix in place at time of report)

Patched versions

N/A

Description

Summary

Any approved streamer can inject arbitrary HTML into /streamer and the homepage “Live streams” widget by placing markup in their Twitch/YouTube stream title. CSP is present and blocks inline script execution, but the issue is still a server-side HTML injection sink.

To trigger this, a Lichess account only needs to satisfy the normal streamer requirements and get approved. Per Streamer.canApply, that means an account older than 2 days with at least 15 games, or a verified/titled account. After moderator approval, once the streamer goes live, Lichess pulls the platform title and renders it into the UI as-is. No extra privileges are needed beyond a normal approved streamer profile.

Details

The title is taken from the upstream platform and kept as HTML:

// modules/streamer/src/main/YoutubeApi.scala
YoutubeStream(
  item.snippet.channelId,
  unescapeHtml(item.snippet.title),
  …
)

...

// modules/streamer/src/main/TwitchApi.scala
case class HelixStream(..., title: Html, …)
case class TwitchStream(stream: HelixStream, …):
  def status = stream.title

cleanStatus does not sanitize anything. It only trims the existing Html value:

// modules/streamer/src/main/model.scala
lazy val cleanStatus = status.map(s => removeMultibyteSymbols(s).trim)

Sink

The unsanitized title is rendered in both the streamer listing and the homepage widget:

// modules/streamer/src/main/ui/StreamerUi.scala  
p(cls := "at")(trs.currentlyStreaming(strong(s.cleanStatus)))

Because cleanStatus is still an Html value, Scalatags renders it as raw HTML rather than escaping it. As a result, attacker-controlled markup from the Twitch/YouTube title is injected directly into the DOM in both locations.

PoC

Because Lichess has a strict CSP, this does not appear to be a straightforward full XSS in the usual sense. Inline scripts, event handlers, and similar easy execution paths are blocked.

That said, the injection is still dangerous because attacker-controlled HTML is rendered into the page, which opens the door to CSP-compatible gadget abuse. In particular, existing site JavaScript such as the tooltip behavior can potentially be abused to mislead users into clicking attacker-controlled UI or interacting with injected elements that appear legitimate.
As a simple proof of impact, we've used a <meta> refresh payload to force visitors on /streamer to be redirected off-site:

<meta http-equiv=refresh content='0;url=https://example.com'>

So while CSP makes full script execution harder, the issue is still exploitable for meaningful client-side impact, including forced navigation and UI redressing/phishing-style abuse through existing frontend gadgets.

A working PoC demonstrating the issue has been recorded:

PoC

Impact

Any approved streamer can control the HTML that gets rendered in /streamer and the homepage live widget through the stream title.

Credits

This finding was discovered and reported by XENOPS

Severity

High

CVE ID

CVE-2026-35208

Weaknesses

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. Learn more on MITRE.

Improper Encoding or Escaping of Output

The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved. Learn more on MITRE.

Credits