Security: DOM XSS via unsanitized setAttribute() in createTree() (worker mode) — distinct from expression XSS (#2828)
Summary
createTree() in player/js/worker_wrapper.js (line 560-595) applies all attributes from the worker-serialized DOM tree to real DOM elements via setAttribute() without filtering. This allows injection of event handler attributes (onclick, onmouseover, onfocus, etc.) from a crafted Lottie animation JSON, resulting in arbitrary JavaScript execution.
This is a different vulnerability from the expression/eval XSS reported in #2828 and #3048. The expression XSS can be mitigated by using lottie_light.js or disabling expressions. This vulnerability cannot be mitigated by any existing workaround.
|
Expression XSS (#2828) |
createTree() XSS (this issue) |
| Root cause |
eval() on "x" property |
setAttribute() in createTree() |
| File |
expressions/ExpressionManager.js |
player/js/worker_wrapper.js:575 |
| Mitigation available? |
Yes — lottie_light.js |
None |
| Disable expressions fixes it? |
Yes |
No |
| Affected builds |
Expression-enabled only |
All builds using worker renderer |
Vulnerable Code
createTree() — worker_wrapper.js:560-595
function createTree(data, container, map, afterElement) {
var elem;
if (data.type === 'div') {
elem = document.createElement('div');
} else {
elem = document.createElementNS(data.namespace, data.type);
}
// ...
for (var attr in data.attributes) {
if (Object.prototype.hasOwnProperty.call(data.attributes, attr)) {
if (attr === 'href') {
elem.setAttributeNS('http://www.w3.org/1999/xlink', attr, data.attributes[attr]);
} else {
elem.setAttribute(attr, data.attributes[attr]); // ← NO FILTERING
}
}
}
// ...
}
The data parameter comes from ProxyElement.serialize() (line 59-67), which serializes all attributes set on the proxy element in the worker:
serialize: function () {
return {
type: this.type,
namespace: this.namespace,
style: this.style.serialize(),
attributes: this.attributes, // ← ALL attributes, unfiltered
children: this.children.map(function (child) { return child.serialize(); }),
textContent: this._textContent,
};
},
updateElementAttributes() — worker_wrapper.js:654-660
Same issue in the update path:
function updateElementAttributes(element, attributes) {
var attribute;
for (var i = 0; i < attributes.length; i += 1) {
attribute = attributes[i];
element.setAttribute(attribute[0], attribute[1]); // ← NO FILTERING
}
}
Attack Vector
- Attacker crafts a malicious Lottie animation JSON
- The JSON is loaded by an application using lottie-web's worker renderer (
renderer: 'html' with worker, or the worker API)
- The worker processes the animation and creates ProxyElements with attacker-controlled attributes
ProxyElement.serialize() sends all attributes (including event handlers) to the main thread via postMessage
createTree() on the main thread calls elem.setAttribute('onclick', 'malicious_code()') on real DOM elements
- User interaction (hover, click, focus) triggers the injected JavaScript
Proof of Concept
Save as HTML and open in a browser:
<!DOCTYPE html>
<html>
<body>
<div id="container"></div>
<script>
// Exact createTree() from worker_wrapper.js:560
function createTree(data, container, map) {
var elem;
if (data.type === 'div') {
elem = document.createElement('div');
} else {
elem = document.createElementNS(data.namespace, data.type);
}
for (var attr in data.attributes) {
if (Object.prototype.hasOwnProperty.call(data.attributes, attr)) {
if (attr === 'href') {
elem.setAttributeNS('http://www.w3.org/1999/xlink', attr, data.attributes[attr]);
} else {
elem.setAttribute(attr, data.attributes[attr]);
}
}
}
data.children.forEach(function (child) { createTree(child, elem, map); });
container.appendChild(elem);
}
// Simulated worker output — in production this comes from
// ProxyElement.serialize() via postMessage
createTree({
type: 'div',
namespace: '',
attributes: {
'id': 'lottie-xss',
'onmouseover': 'alert("XSS via createTree setAttribute")',
},
style: {},
textContent: 'Hover to trigger XSS',
children: []
}, document.getElementById('container'), {});
</script>
</body>
</html>
A full interactive PoC is attached as poc.html.
Impact
- Any application using lottie-web's worker rendering path is vulnerable
- Loading an untrusted Lottie animation JSON results in arbitrary JavaScript execution
- This affects all platforms that accept user-uploaded or third-party Lottie animations (design tools, CMSes, marketing platforms, email builders, etc.)
- No workaround exists — unlike the expression XSS, there is no "light" build or configuration flag to disable this
Suggested Fix
Add an attribute allowlist in createTree() and updateElementAttributes():
// Attributes that are safe to set on DOM elements
var ALLOWED_ATTRIBUTES = {
'id': true, 'class': true, 'style': true, 'viewBox': true,
'd': true, 'fill': true, 'stroke': true, 'opacity': true,
'transform': true, 'cx': true, 'cy': true, 'r': true,
'rx': true, 'ry': true, 'x': true, 'y': true,
'width': true, 'height': true, 'x1': true, 'y1': true,
'x2': true, 'y2': true, 'offset': true, 'stop-color': true,
'stop-opacity': true, 'clip-path': true, 'clip-rule': true,
'fill-rule': true, 'fill-opacity': true, 'stroke-width': true,
'stroke-dasharray': true, 'stroke-dashoffset': true,
'stroke-linecap': true, 'stroke-linejoin': true,
'stroke-opacity': true, 'font-family': true, 'font-size': true,
'font-weight': true, 'font-style': true, 'text-anchor': true,
'dominant-baseline': true, 'letter-spacing': true,
'mask': true, 'filter': true, 'marker-end': true,
'marker-start': true, 'marker-mid': true,
'gradientTransform': true, 'gradientUnits': true,
'patternTransform': true, 'patternUnits': true,
'spreadMethod': true, 'preserveAspectRatio': true,
'href': true, 'xmlns': true, 'xmlns:xlink': true,
'data-name': true, 'data-lottie': true,
};
function isSafeAttribute(attr) {
// Block all event handlers
if (attr.substring(0, 2) === 'on') return false;
// Allow known-safe attributes
if (ALLOWED_ATTRIBUTES[attr]) return true;
// Allow data-* attributes
if (attr.substring(0, 5) === 'data-') return true;
// Allow aria-* attributes
if (attr.substring(0, 5) === 'aria-') return true;
// Block unknown attributes by default
return false;
}
Then in createTree():
if (attr === 'href') {
elem.setAttributeNS('http://www.w3.org/1999/xlink', attr, data.attributes[attr]);
} else if (isSafeAttribute(attr)) {
elem.setAttribute(attr, data.attributes[attr]);
}
Alternative minimal fix — just block event handlers:
} else if (attr.substring(0, 2) !== 'on') {
elem.setAttribute(attr, data.attributes[attr]);
}
Environment
Security: DOM XSS via unsanitized
setAttribute()increateTree()(worker mode) — distinct from expression XSS (#2828)Summary
createTree()inplayer/js/worker_wrapper.js(line 560-595) applies all attributes from the worker-serialized DOM tree to real DOM elements viasetAttribute()without filtering. This allows injection of event handler attributes (onclick,onmouseover,onfocus, etc.) from a crafted Lottie animation JSON, resulting in arbitrary JavaScript execution.This is a different vulnerability from the expression/eval XSS reported in #2828 and #3048. The expression XSS can be mitigated by using
lottie_light.jsor disabling expressions. This vulnerability cannot be mitigated by any existing workaround.eval()on"x"propertysetAttribute()increateTree()expressions/ExpressionManager.jsplayer/js/worker_wrapper.js:575lottie_light.jsVulnerable Code
createTree()—worker_wrapper.js:560-595The
dataparameter comes fromProxyElement.serialize()(line 59-67), which serializes all attributes set on the proxy element in the worker:updateElementAttributes()—worker_wrapper.js:654-660Same issue in the update path:
Attack Vector
renderer: 'html'with worker, or the worker API)ProxyElement.serialize()sends all attributes (including event handlers) to the main thread viapostMessagecreateTree()on the main thread callselem.setAttribute('onclick', 'malicious_code()')on real DOM elementsProof of Concept
Save as HTML and open in a browser:
A full interactive PoC is attached as
poc.html.Impact
Suggested Fix
Add an attribute allowlist in
createTree()andupdateElementAttributes():Then in
createTree():Alternative minimal fix — just block event handlers:
Environment
player/js/worker_wrapper.jscreateTree()(line 560),updateElementAttributes()(line 654)