Collabora Online Stored XSS (CVE-2024-29182)
13. May 2024, #web #cve #collabora
In March 2024, we’ve identified a stored XSS vulnerability in Collabora Online ↗, a Libreoffice-based online document collaboration platform. This post is a short rundown of how the vulnerability was identified and goes into more detail what component was impacted.
Breakdown #
As with most document solutions, Collabora provides cross references, a feature that can reference parts of a document on other pages that will then automatically update. When you hover over such a reference, a tooltip menu appears, providing further contextual information about the referenced content.
This tooltip is susceptible to stored XSS because the referenced content is not sufficiently sanitized before rendering:
Digging further #
With the vulnerable component identified, we dug further to determine what actually happens when hovering over references. To do so, we fired up BurpSuite and started intercepting the web traffic.
Upon hovering over the reference, a websocket message was dispatched containing our referenced text:

Looking through the codebase of CollaboraOnline/online ↗ git repository, we located the corresponding websocket message handler in browser/src/layer/tile/CanvasTileLayer.js ↗:
_onMessage: function (textMsg, img) {
this._saveMessageForReplay(textMsg);
...
else if (textMsg.startsWith('tooltip:')) {
var tooltipInfo = JSON.parse(textMsg.substring('tooltip:'.length + 1));
if (tooltipInfo.type === 'formulausage') {
this._onCalcFunctionUsageMsg(tooltipInfo.text);
}
else if (tooltipInfo.type === 'generaltooltip') {
var tooltipInfo = JSON.parse(textMsg.substring(textMsg.indexOf('{')));
this._map.uiManager.showDocumentTooltip(tooltipInfo); // [1]
}
else {
console.error('unknown tooltip type');
}
}
...
}
As our message is of type generaltooltip
, the showDocumentTooltip
function
is called. This function is located in browser/src/control/Control.UIManager.js ↗
and reads:
showDocumentTooltip: function(tooltipInfo) {
var split = tooltipInfo.rectangle.split(',');
var latlng = this.map._docLayer._twipsToLatLng(new L.Point(+split[0], +split[1]));
var pt = this.map.latLngToContainerPoint(latlng);
var elem = $('.leaflet-layer');
elem.tooltip();
elem.tooltip('enable');
elem.tooltip('option', 'content', tooltipInfo.text);
elem.tooltip('option', 'items', elem[0]);
elem.tooltip('option', 'position', { my: 'left bottom', at: 'left+' + pt.x + ' top+' + pt.y, collision: 'fit fit' });
elem.tooltip('open');
document.addEventListener('mousemove', function() {
elem.tooltip('close');
elem.tooltip('disable');
}, {once: true});
},
Based on the callchain thus far, we can deduce that upon hovering over the
reference, the referenced text sent over a websocket and is handled by a call to _onMessage
.
This function in turn calls showDocumentTooltip
to render the tooltip with calls to $('.leaflet-layer').tooltip(...)
.
But what does .tooltip(...)
do?
More digging #
Luckily for us, the origin of the .tooltip(...)
calls were easy to track down. According
to jQuery’s selector, it appears to have something to do with “leaflet”. A
quick search-engine-of-choice search later, we were presented with the API
documentation of leafletjs ↗.
The relevant component for us is the tooltip ↗. According to the usage
examples of tooltips, it looks as if the content
property can contain HTML
strings!
var tooltip = L.tooltip()
.setLatLng(latlng)
.setContent('Hello world!<br />This is a nice tooltip.')
.addTo(map);
...
var tooltip = L.tooltip(latlng, {content: 'Hello world!<br />This is a nice tooltip.'})
.addTo(map);
Armed with this knowledge, we went ahead and cloned the leafletjs
repository ↗ to figure out how the content
is processed.
The tooltip component is located in src/layer/Tooltip.js ↗,
but does not actually contain the logic for handling the content. Instead, it
extends the DivOverlay
component:
import {DivOverlay} from './DivOverlay.js';
...
export const Tooltip = DivOverlay.extend({
...
});
The DivOverlay
component is located in src/layer/DivOverlay.js ↗
and contains the logic we were looking for - the _updateContent
function:
_updateContent() {
if (!this._content) { return; }
const node = this._contentNode;
const content = (typeof this._content === 'function') ? this._content(this._source || this) : this._content;
if (typeof content === 'string') {
node.innerHTML = content; // 💣💥 Oops!
} else {
...
}
...
},
As depicted in the code snippet above, if the content
is of type string
it
is simply assigned to the HTML element’s innerHTML
property, which leads to
the stored XSS vulnerability.
Timeline #
- 2024-03-14 Vulnerability identified and initial disclosure contact initiated
- 2024-03-18 Vendor confirms vulnerability, a fix is in the works (CVE-2024-29182 reserved)
- 2024-03-20 Vendor releases fixed versions
- 2024-04-04 CVE-2024-29182 published
- 2024-05-13 Writeup published