Summary
The Opencast Paella player renders caption cue text into innerHTML without escaping. The captions canvas clears _captionsContainer.innerHTML and then appends each active cue with _captionsContainer.innerHTML += cue, so HTML inside a WebVTT or DFXP cue becomes live DOM and executes in the Opencast origin.
The caption track is read from any media package element with a captions/* flavor and is served, with the player manifest, to anonymous viewers through /search/episode.json. The caption plugins that consume it are enabled in the default player configuration, the "Subtitles" upload that produces a captions/source track is active by default, and no caption processing step escapes the cue text.
A user who can upload a subtitle to an event and publish it stores the payload in the published caption file. Any viewer who opens the event and turns captions on runs the script.
Result: a non-admin content author stores JavaScript in a subtitle cue that executes in the browser session of every viewer who enables captions on that event, including anonymous viewers and authenticated staff.
Affected
opencast/opencast, engage-paella-player module. Supported release lines 19.x and 20.x are affected (and 18.x). Live-confirmed on 18.8 (Paella 7, paella-core 1.50.2) and 20.0 (Paella 8, paella-core 1.50.4); 19.5 ships the code-identical caption path (paella-core 1.50.4, same EpisodeConversor and default plugin config as 20.0). The captions canvas uses the same innerHTML += cue sink across these versions.
Default configuration: the WebVTT and DFXP caption plugins are enabled: true in etc/ui-config/mh_default_org/paella7/config.json, the "Subtitles" upload option (captions/source, .vtt) is active in etc/listproviders/event.upload.asset.options.properties, and the fast workflow publishes captions/* to the engage player.
Condition: an event with a caption track published to the engage player. No non-default flag required.
Root cause
The captions canvas appends each cue to _captionsContainer.innerHTML in the bundled paella-core (served at /paellaN/ui/paella-player.js / the 20.x core chunk), so markup in a cue becomes live DOM. The caption entry is built from any media package element whose flavor matches captions/* at modules/engage-paella-player-7/src/js/EpisodeConversor.js:392, and /search/episode.json serves the manifest and the caption file to anonymous clients. The WebVTT and DFXP plugins that consume it are enabled by default at etc/ui-config/mh_default_org/paella7/config.json:571 and :574. The cue text is not HTML-escaped before assignment to innerHTML, and partial-process-uploaded-captions only cuts and tags the file, it never sanitizes it. Opencast sets neither a Content-Security-Policy nor an X-Content-Type-Options header, so the injected script runs without restriction.
Reproduction
Default config, default caption plugins enabled, a non-admin user with ROLE_API_EVENTS_CREATE, ROLE_API_EVENTS_TRACK_EDIT, and ROLE_UI_TASKS_CREATE (no ROLE_ADMIN).
- As the non-admin user, create an event, then upload a WebVTT subtitle as
captions/source whose cue text is an XSS payload, and publish the event.
WEBVTT
00:00:00.000 --> 00:00:30.000
<img src=x onerror=document.title=window.__xss=document.domain>
- The anonymous search manifest then exposes the caption and serves the cue raw.
GET /search/episode.json?id=<event>
"type":"captions/source", "url":".../static/.../x.vtt"
GET .../static/.../x.vtt -> cue text returned verbatim
- Open the event in the player as an anonymous viewer, open the captions menu, and select the track; the cue is written to
innerHTML and the onerror handler runs.
Live-verified: on Opencast 18.8 (build 8705223) in Chrome, a non-admin author published the subtitle and an anonymous viewer enabled captions, rendering the cue as a live <img> node and setting window.__xss and document.title to document.domain. On Opencast 20.0 (build d919405, Paella 8), the served core bundle contains the identical innerHTML += cue sink and the player loads the cue raw; the sink executes JavaScript in the engage origin when fed the player's own published caption file.
Impact
- JavaScript execution in the Opencast origin in the session of any viewer who enables captions on the event.
- Anonymous viewers and authenticated staff are equally affected; an instructor or admin viewer exposes that session context to the script.
- Session and CSRF-token theft, actions performed as the victim against the Opencast REST API.
- Stored by a non-admin content author, triggered by viewing with captions on, no attacker authentication at view time.
Credit
Jan Kahmen, turingpoint (jan@turingpoint.de)
References
Summary
The Opencast Paella player renders caption cue text into
innerHTMLwithout escaping. The captions canvas clears_captionsContainer.innerHTMLand then appends each active cue with_captionsContainer.innerHTML += cue, so HTML inside a WebVTT or DFXP cue becomes live DOM and executes in the Opencast origin.The caption track is read from any media package element with a
captions/*flavor and is served, with the player manifest, to anonymous viewers through/search/episode.json. The caption plugins that consume it are enabled in the default player configuration, the "Subtitles" upload that produces acaptions/sourcetrack is active by default, and no caption processing step escapes the cue text.A user who can upload a subtitle to an event and publish it stores the payload in the published caption file. Any viewer who opens the event and turns captions on runs the script.
Result: a non-admin content author stores JavaScript in a subtitle cue that executes in the browser session of every viewer who enables captions on that event, including anonymous viewers and authenticated staff.
Affected
opencast/opencast,
engage-paella-playermodule. Supported release lines 19.x and 20.x are affected (and 18.x). Live-confirmed on 18.8 (Paella 7, paella-core 1.50.2) and 20.0 (Paella 8, paella-core 1.50.4); 19.5 ships the code-identical caption path (paella-core 1.50.4, sameEpisodeConversorand default plugin config as 20.0). The captions canvas uses the sameinnerHTML += cuesink across these versions.Default configuration: the WebVTT and DFXP caption plugins are
enabled: trueinetc/ui-config/mh_default_org/paella7/config.json, the "Subtitles" upload option (captions/source,.vtt) is active inetc/listproviders/event.upload.asset.options.properties, and thefastworkflow publishescaptions/*to the engage player.Condition: an event with a caption track published to the engage player. No non-default flag required.
Root cause
The captions canvas appends each cue to
_captionsContainer.innerHTMLin the bundled paella-core (served at/paellaN/ui/paella-player.js/ the 20.x core chunk), so markup in a cue becomes live DOM. The caption entry is built from any media package element whose flavor matchescaptions/*atmodules/engage-paella-player-7/src/js/EpisodeConversor.js:392, and/search/episode.jsonserves the manifest and the caption file to anonymous clients. The WebVTT and DFXP plugins that consume it are enabled by default atetc/ui-config/mh_default_org/paella7/config.json:571and:574. The cue text is not HTML-escaped before assignment toinnerHTML, andpartial-process-uploaded-captionsonly cuts and tags the file, it never sanitizes it. Opencast sets neither a Content-Security-Policy nor an X-Content-Type-Options header, so the injected script runs without restriction.Reproduction
Default config, default caption plugins enabled, a non-admin user with
ROLE_API_EVENTS_CREATE,ROLE_API_EVENTS_TRACK_EDIT, andROLE_UI_TASKS_CREATE(noROLE_ADMIN).captions/sourcewhose cue text is an XSS payload, and publish the event.innerHTMLand theonerrorhandler runs.Live-verified: on Opencast 18.8 (build 8705223) in Chrome, a non-admin author published the subtitle and an anonymous viewer enabled captions, rendering the cue as a live
<img>node and settingwindow.__xssanddocument.titletodocument.domain. On Opencast 20.0 (build d919405, Paella 8), the served core bundle contains the identicalinnerHTML += cuesink and the player loads the cue raw; the sink executes JavaScript in the engage origin when fed the player's own published caption file.Impact
Credit
Jan Kahmen, turingpoint (jan@turingpoint.de)
References