VYPR
Moderate severityNVD Advisory· Published Oct 8, 2025· Updated Oct 8, 2025

Opencast Paella Player 7 vulnerable to Cross-Site-Scripting

CVE-2025-61788

Description

Opencast is a free, open-source platform to support the management of educational audio and video content. Prior to Opencast 17.8 and 18.2, the paella would include and render some user inputs (metadata like title, description, etc.) unfiltered and unmodified. The vulnerability allows attackers to inject and malicious HTML and JavaScript in the player, which would then be executed in the browsers of users watching the prepared media. This can then be used to modify the site or to execute actions in the name of logged-in users. To inject malicious metadata, an attacker needs write access to the system. For example, the ability to upload media and modify metadata. This cannot be exploited by unauthenticated users. This issue is fixed in Opencast 17.8 and 18.2.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.opencastproject:opencast-commonMaven
<= 16.10

Affected products

1

Patches

1
2809520fa88d

Merge commit from fork

https://github.com/opencast/opencastKatrin IhlerSep 26, 2025via ghsa
5 files changed · +115 60
  • modules/engage-paella-player-7/src/plugins/org.opencast.paella.descriptionPlugin.js+59 30 modified
    @@ -36,108 +36,137 @@ export default class DescriptionPlugin extends PopUpButtonPlugin {
         const metadata = this.player.videoManifest.metadata;
     
         const presenters = metadata.presenters
    -      ?.map((p) => `<a href="/engage/ui/index.html?q=${p}">${p}</a>`)
    +      ?.map((p) => {
    +        const elm = createElementWithHtmlText('<a href=""> </a>');
    +        elm.href = `/engage/ui/index.html?q=${p}`;
    +        elm.innerText = p;
    +        return elm.outerHTML;
    +      })
           ?.join(', ');
         const contributors = metadata.contributors
    -      ?.map((p) => `<a href="/engage/ui/index.html?q=${p}">${p}</a>`)
    +      ?.map((p) => {
    +        const elm = createElementWithHtmlText('<a href=""> </a>');
    +        elm.href = `/engage/ui/index.html?q=${p}`;
    +        elm.innerText = p;
    +        return elm.outerHTML;
    +      })
           ?.join(', ');
         const language = metadata.language
           ? (new Intl.DisplayNames([metadata.language], {type: 'language'}))
             .of(metadata.language)
           : '';
     
         const content = createElementWithHtmlText('<div class="description-plugin"></div>');
    -    createElementWithHtmlText(`
    +    const elm_title = createElementWithHtmlText(`
           <div class="row">
             <div class="key"> ${translate('Title')}: </div>
    -        <div class="value"> ${metadata.title || ''} </div>
    +        <div class="value">  </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`
    +    elm_title.querySelector('.value').innerText = metadata.title || '';
    +    const elm_subject = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Subject')}:</div>
             <div class="value">
    -          <a href="/engage/ui/index.html?q=${metadata.subject}">${metadata.subject || ''}</a>
    +          <a href=""></a>
              </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`    
    +    elm_subject.querySelector('.value a').innerText = metadata.subject || '';
    +    elm_subject.querySelector('.value a').href = `/engage/ui/index.html?q=${metadata.subject}`;
    +
    +    const elm_description = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Description')}:</div>
    -        <div class="value"> ${metadata.description || ''} </div>
    +        <div class="value"> </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`    
    +    elm_description.querySelector('.value').innerText = metadata.description || '';
    +    const elm_language = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Language')}:</div>
    -        <div class="value"> ${language} </div>
    +        <div class="value"> </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`    
    +    elm_language.querySelector('.value').innerText = language || '';
    +    const elm_rights = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Rights')}:</div>
    -        <div class="value"> ${metadata.rights || ''} </div>
    +        <div class="value"> </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`        
    +    elm_rights.querySelector('.value').innerText = metadata.rights || '';
    +
    +    const elm_license = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('License')}:</div>
    -        <div class="value"> ${metadata.license || ''} </div>
    +        <div class="value"> </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`    
    +    elm_license.querySelector('.value').innerText = metadata.license || '';
    +    const elm_series = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Series')}:</div>
             <div class="value">
    -          <a href="/engage/ui/index.html?epFrom=${metadata.series}">${metadata.seriestitle || ''}</a>
    +          <a href=""> </a>
             </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`
    +    elm_series.querySelector('.value a').href = `/engage/ui/index.html?epFrom=${metadata.series}`;
    +    elm_series.querySelector('.value a').innerText = metadata.seriestitle || '';
    +    const elm_presenters = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Presenter(s)')}:</div>
    -        <div class="value"> ${presenters || ''} </div>
    +        <div class="value"> </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`
    +    elm_presenters.querySelector('.value').innerHTML = presenters || '';
    +    const elm_contributors = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Contributor(s)')}:</div>
    -        <div class="value"> ${contributors || ''} </div>
    +        <div class="value"> </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`
    +    elm_contributors.querySelector('.value').innerHTML = contributors || '';
    +    const elm_startDate = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Start date')}:</div>
    -        <div class="value"> ${(new Date(metadata.startDate)).toLocaleDateString()} </div>
    +        <div class="value"> </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`
    +    elm_startDate.querySelector('.value').innerText = (new Date(metadata.startDate)).toLocaleDateString() || '';
    +    const elm_duration = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Duration')}:</div>
    -        <div class="value"> ${utils.secondsToTime(metadata.duration) || ''} </div>
    +        <div class="value"> </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`    
    +    elm_duration.querySelector('.value').innerText = utils.secondsToTime(metadata.duration) || '';
    +    const elm_location = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('Location')}:</div>
    -        <div class="value"> ${metadata.location || ''} </div>
    +        <div class="value"> </div>
           </div>
         `, content);
    -    createElementWithHtmlText(`    
    +    elm_location.querySelector('.value').innerText = metadata.location || '';
    +    const elm_uid = createElementWithHtmlText(`
           <div class="row">
             <div class="key">${translate('UID')}:</div>
             <div class="value"> 
    -          <a href="?id=${metadata.UID}">${metadata.UID}</a>
    +          <a href=""></a>
             </div>
           </div>
         `, content);
    +    elm_uid.querySelector('.value a').href = `?id=${metadata.UID}`;
    +    elm_uid.querySelector('.value a').innerText = metadata.UID || '';
         if (metadata.views) {
    -      createElementWithHtmlText(`    
    +      const elm_views = createElementWithHtmlText(`
             <div class="row">
               <div class="key">${translate('Views')}:</div>
    -          <div class="value"> ${metadata.views} </div>
    +          <div class="value"> </div>
             </div>      
           `, content);
    +      elm_views.querySelector('.value').innerText = metadata.views;
         }
     
         return content;
    
  • modules/engage-paella-player-7/src/plugins/org.opencast.paella.downloadsPlugin.js+17 11 modified
    @@ -153,11 +153,12 @@ export default class DownloadsPlugin extends PopUpButtonPlugin {
             </div>`);
         const downloadKeys = Object.keys(this._downloads);
         downloadKeys.forEach(k => {
    -      const J = createElementWithHtmlText(`
    +      const downloadStreamElem = createElementWithHtmlText(`
             <div class="downloadStream">
    -          <div class="title">${k}</div>
    +          <div class="title"> </div>
             </div>`, container);
    -      const list = createElementWithHtmlText('<ul></ul>', J);
    +      downloadStreamElem.querySelector('.title').innerText = k;
    +      const downloadStreamElemlist = createElementWithHtmlText('<ul></ul>', downloadStreamElem);
           const streamDownloads = this._downloads[k];
           streamDownloads.forEach(d => {
             if (d.mimetype == 'text/vtt') {
    @@ -176,15 +177,17 @@ export default class DownloadsPlugin extends PopUpButtonPlugin {
     
               const elm = createElementWithHtmlText(`
                 <li>
    -              <a href="${d.url}" download target="_blank">
    -                <span class="mimetype">[${d.mimetype}]</span><span class="res">${cmeta}</span>
    +              <a class="link" download target="_blank">
    +                <span class="mimetype"> </span> <span class="res"> </span>
                   </a>
                   <a href="#">
                     <span class="transcript">[transcript]</span>
                   </a>
                 </li>
    -          `, list);
    -
    +          `, downloadStreamElemlist);
    +          elm.querySelector('.link').href = d.url;
    +          elm.querySelector('.mimetype').innerText = `[${d.mimetype}]`;
    +          elm.querySelector('.res').innerText = cmeta;
               elm.getElementsByTagName('a')[1].onclick = downloadTranscript(d.url);
             }
             else {
    @@ -193,13 +196,16 @@ export default class DownloadsPlugin extends PopUpButtonPlugin {
     
               const meta = vmeta ?? ameta ?? '';
     
    -          createElementWithHtmlText(`
    +          const elm = createElementWithHtmlText(`
                 <li>
    -              <a href="${d.url}" download>
    -                <span class="mimetype">[${d.mimetype}]</span><span class="res">${meta}</span>
    +              <a download>
    +                <span class="mimetype">[mimetype]</span><span class="res"></span>
                   </a>
                 </li>
    -          `, list);
    +          `, downloadStreamElemlist);
    +          elm.querySelector('a').href = d.url;
    +          elm.querySelector('.mimetype').innerText = `[${d.mimetype}]`;
    +          elm.querySelector('.res').innerText = meta;
             }
           });
         });
    
  • modules/engage-paella-player-7/src/plugins/org.opencast.paella.episodesFromSeries.js+9 5 modified
    @@ -84,14 +84,18 @@ export default class EpisodesFromSeriesPlugin extends PopUpButtonPlugin {
             if (id !== thisId) {
               const preview = getVideoPreview(mediapackage,this.player.config);
               const url = `watch.html?id=${id}`;
    -          createElementWithHtmlText(`
    +          const elm = createElementWithHtmlText(`
                       <li>
    -                      <a href="${url}">
    -                          <img src="${preview}" alt="${dcTitle}">
    -                          <span>${dcTitle}</span>
    +                      <a href="">
    +                          <img src="" alt="">
    +                          <span> </span>
                           </a>
                       </li>
    -                  `,list);
    +                  `, list);
    +          elm.querySelector('a').href = url;
    +          elm.querySelector('img').src = preview;
    +          elm.querySelector('img').alt = dcTitle;
    +          elm.querySelector('span').textContent = dcTitle;
             }
           });
         }
    
  • modules/engage-paella-player-7/src/plugins/org.opencast.paella.transcriptionsPlugin.js+7 2 modified
    @@ -96,10 +96,15 @@ export default class TranscriptionsPlugin extends PopUpButtonPlugin {
           const instant = t.time - trimmingOffset;
           const transcriptionItem = createElementWithHtmlText(`
             <li>
    -          <img id="${id}" src="${t.preview}" alt="${t.text}"/>
    -          <span><strong>${utils.secondsToTime(instant)}:</strong> ${t.text}</span>
    +          <img id="${id}" src="" alt=""/>
    +          <span><strong>hh:mm:ss:</strong> <span class="text"> </span></span>
             </li>`,
           this._transcriptionsContainer);
    +      transcriptionItem.querySelector('strong').textContent = `${utils.secondsToTime(instant)}:`;
    +      transcriptionItem.querySelector('img').alt = t.text;
    +      transcriptionItem.querySelector('img').title = t.text;
    +      transcriptionItem.querySelector('img').src = t.preview;
    +      transcriptionItem.querySelector('span.text').textContent = t.text;
           transcriptionItem.addEventListener('click', async evt => {
             const trimmingOffset = videoContainer.isTrimEnabled ? videoContainer.trimStart : 0;
             this.player.videoContainer.setCurrentTime(t.time - trimmingOffset);
    
  • modules/engage-paella-player-7/src/plugins/org.opencast.paella.versionButton.js+23 12 modified
    @@ -36,24 +36,35 @@ export default class OpencastPaellaVersionPlugin extends PopUpButtonPlugin {
       async getContent() {
         const pluginVersionsHTML = this.player.version.pluginModules.map(p => {
           const i = p.split(':');
    -      return `<div class="row">
    -                <div class="component"> ${i[0].trim()} </div>
    -                <div class="version"> ${i[1].trim()}</div>
    -              </div>`;
    +      const c =  createElementWithHtmlText(`<div class="row">
    +                <div class="component"> </div>
    +                <div class="version"> </div>
    +              </div>`);
    +      c.querySelector('div.component').innerText = i[0].trim();
    +      c.querySelector('div.version').innerText = i[1].trim();
    +      return c.outerHTML;
         }).join('');
    +
    +    const row_oc = createElementWithHtmlText(`<div class="row">
    +      <div class="component"> ${translate('Opencast player')} </div>
    +      <div class="version"> </div>
    +    </div>`);
    +    row_oc.querySelector('.version').innerText = this.player.version.player;
    +
    +    const row_core = createElementWithHtmlText(`<div class="row">
    +      <div class="component"> ${translate('Paella core version')} </div>
    +      <div class="version"> </div>
    +    </div>`);
    +    row_core.querySelector('.version').innerText = this.player.version.coreLibrary;
    +
    +
         const container = createElementWithHtmlText(`
             <div class="OpencastPaellaVersionPlugin">
                 <h4>${translate('Opencast player version')}</h4>
                 <div class="downloadStream">
    -              <div class="row">
    -                <div class="component"> ${translate('Opencast player')} </div>
    -                <div class="version"> ${this.player.version.player} </div>
    -              </div>
    +              ${row_oc.outerHTML}
                   <div class="paella-plugins">
    -                <div class="row">
    -                  <div class="component"> ${translate('Paella core version')} </div>
    -                  <div class="version"> ${this.player.version.coreLibrary} </div>
    -                </div>
    +                ${row_core.outerHTML}
                     ${pluginVersionsHTML}
                   </div>
                 </div>
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.