DEV Community

Cover image for Why Wazuh Missed React2Shell
Dolan
Dolan

Posted on • Edited on

Why Wazuh Missed React2Shell

Introduction

The security community was recently shaken by React2Shell, a critical unauthenticated Remote Code Execution (RCE) vulnerability. It was initially tracked under two CVEs:

  • CVE-2025-55182 โ€“ React

  • CVE-2025-66478 โ€“ Next.js

With a CVSS score of 10.0, this vulnerability impacts Next.js 15/16 (App Router) and any framework relying on React 19โ€™s RSC implementation.

Wazuh published a great blog explaining how to detect this vulnerability:

๐Ÿ‘‰ https://wazuh.com/blog/detecting-next-js-cve-2025-66478-rce-vulnerability-with-wazuh/

However, after doing deeper research, I found a critical limitation that applies not only to this CVE, but to most modern application stacks.

The Detection Gap: Why SIEMs Are Blind to Local Projects
Standard vulnerability detectors (including Wazuhโ€™s default modules) excel at finding packages installed via system managers like apt or npm install -g. But modern development happens elsewhere:

Node.js: Most projects install dependencies locally in the project folder.
Python: Virtual environments (venv, uv) isolate packages from the system.
Docker: Packages are buried inside container layers (/var/lib/docker/...).

If your SIEM isn't looking at these local paths, you are flying blind.

So if a vulnerable version of Next.js or React exists inside a project directory, Wazuh will miss it.

I explained this problem earlier here:

๐Ÿ‘‰ https://dev.to/0xdolan/why-you-shouldnt-rely-solely-on-detectors-373g

Wazuh provides several features that help, but none fully solve this problem.

IT Hygiene / Vulnerability Detection

๐Ÿ‘‰ https://documentation.wazuh.com/current/getting-started/use-cases/it-hygiene.html#it-hygiene

๐Ÿ‘‰ https://documentation.wazuh.com/current/user-manual/capabilities/vulnerability-detection/index.html

  • Detects globally installed packages only

Misses:

  • Local Node.js projects
  • Python venvs
  • Docker containers

File Integrity Monitoring (FIM)

๐Ÿ‘‰ https://documentation.wazuh.com/current/user-manual/capabilities/file-integrity/index.html

  • Detects file changes

Does NOT extract:

  • Package names
  • Versions
  • Vulnerability status

Command Monitoring

๐Ÿ‘‰ https://documentation.wazuh.com/current/user-manual/capabilities/command-monitoring/how-it-works.html

  • Can run scripts from the Wazuh manager
  • High risk
  • If manager is compromised โ†’ full agent compromise
  • Not recommended for frequent scans

So I needed a safer and more accurate approach.

The Solution: A Custom Inventory Workflow

We canโ€™t rely on static scanners. Instead, we need a proactive script that:

  1. Crawls the filesystem for package.json files.
  2. Extracts version data for specific high-risk libraries (React/Next.js).
  3. Logs these to a JSON file.
  4. Feeds that file into Wazuh for real-time alerting.

Step 1: The Inventory Script

We use a Python script designed to be lightweight. Crucially, it does not exclude /var/lib/docker, allowing us to find vulnerable libraries inside container images.

Save as /usr/local/bin/scan_react2shell.py

#!/usr/bin/env python3

import json
import os
import sys
import time

# --- CONFIGURATION ---
LOG_FILE = "/var/log/react2shell_scan.json"
SEARCH_PATHS = ["/"]

# Excludes (Docker paths are NOT excluded so we can scan them)
EXCLUDE_DIRS = {"/proc", "/sys", "/dev", "/run", "/tmp", "/snap", "/boot"}


def scan_system():
    print(f"[*] Starting Inventory Scan on: {SEARCH_PATHS}")

    # Initialize Log File
    if not os.path.exists(LOG_FILE):
        try:
            with open(LOG_FILE, "w") as f:
                pass
            print(f"[*] Created new log file at {LOG_FILE}")
        except IOError as e:
            print(f"[!] Error creating log file: {e}")
            return

    count_inspected = 0

    for root_dir in SEARCH_PATHS:
        for dirpath, dirnames, filenames in os.walk(root_dir, followlinks=True):
            # Skip excluded directories
            if any(exclude in dirpath for exclude in EXCLUDE_DIRS):
                dirnames[:] = []
                continue

            if "package.json" in filenames:
                parent_dir = os.path.basename(dirpath)
                grandparent_dir = os.path.basename(os.path.dirname(dirpath))

                pkg_name = None

                if grandparent_dir == "node_modules":
                    if parent_dir == "react":
                        pkg_name = "react"
                    elif parent_dir == "next":
                        pkg_name = "next"

                if pkg_name:
                    full_path = os.path.join(dirpath, "package.json")
                    try:
                        with open(
                            full_path, "r", encoding="utf-8", errors="ignore"
                        ) as f:
                            data = json.load(f)
                            ver = data.get("version", "0.0.0")

                            print(f"[+] Found {pkg_name} v{ver} at {full_path}")
                            count_inspected += 1

                            # --- DOCKER TAGGING LOGIC ---
                            display_path = full_path
                            if (
                                "/var/lib/docker" in full_path
                                or "/var/lib/containerd" in full_path
                            ):
                                display_path = f"[DOCKER] {full_path}"

                            log_entry = {
                                "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
                                "scan_type": "react_inventory",
                                "package": pkg_name,
                                "version": ver,
                                "path": display_path,
                            }

                            with open(LOG_FILE, "a") as lf:
                                lf.write(json.dumps(log_entry) + "\n")

                    except Exception as e:
                        pass

    print(f"[*] Scan complete. Total Packages Logged: {count_inspected}")


if __name__ == "__main__":
    scan_system()

Enter fullscreen mode Exit fullscreen mode

Note: This script scans for package.json and specifically flags packages like react and next while identifying if they are inside a Docker path.

Sample Output:

{"timestamp":"2025-12-11T13:15:07","scan_type":"react_inventory","package":"react","version":"19.2.1","path":"[DOCKER]/var/lib/docker/.../node_modules/react/package.json"}
{"timestamp":"2025-12-11T13:15:08","scan_type":"react_inventory","package":"next","version":"15.4.5","path":"/home/user/app/node_modules/next/package.json"}

Enter fullscreen mode Exit fullscreen mode

Step 2: Wazuh Integration

Once the script generates /var/log/react2shell_scan.json, we need Wazuh to read it.

1. Configure the Agent:

Add this to the Wazuh agent or via centralized configuration (ossec.conf):

<localfile>
  <log_format>json</log_format>
  <location>/var/log/react2shell_scan.json</location>
</localfile>
Enter fullscreen mode Exit fullscreen mode

Pushing the Configuration Centrally

To avoid manually editing the ossec.conf file on every server, we utilize Wazuhโ€™s Centralized Configuration feature. By modifying the agent.conf file on the Manager, we can instruct all agents to start "watching" our custom inventory log.

On the Wazuh Manager:

Edit the shared configuration file for the default group:
vim /var/ossec/etc/shared/default/agent.conf

Add the following block inside the tags:

<agent_config>
  <!-- 
    This tells every agent to monitor the output of our inventory script.
    Wazuh will collect these JSON logs and send them to the manager for analysis.
  -->
  <localfile>
    <log_format>json</log_format>
    <location>/var/log/react2shell_scan.json</location>
  </localfile>
</agent_config>
Enter fullscreen mode Exit fullscreen mode

How it works:

  1. Synchronization: Once you save this file on the Manager, it automatically calculates a new MD5 checksum.
  2. Propagation: The next time your agents check in (heartbeat), they will detect the configuration change, download the updated agent.conf file, and restart their log collector engine.
  3. Visibility: From this point forward, any time your Python script (run via cron) updates /var/log/react2shell_scan.json, the data is instantly streamed to the Wazuh Manager to be checked against your custom React2Shell rules.

2. Create the Detection Rules:

In your Wazuh Manager (/var/ossec/etc/rules/local_rules.xml), add rules to flag specific vulnerable versions based on the Vercel and React security bulletins.

<group name="react_inventory,cve_2025_55182,cve_2025_66478,vulnerability,">
  <!-- INVENTORY: Log EVERY React/Next package found-->
  <rule id="100500" level="3"> 
    <decoded_as>json</decoded_as> 
    <field name="scan_type">react_inventory</field> 
    <description>Inventory: Found $(package) version $(version) at $(path).</description> 
  </rule>

<!-- Detect Vulnerable React.js -->
<rule id="100501" level="15">
  <if_sid>100500</if_sid>
  <field name="package">react</field>
  <field name="version" type="pcre2">
    ^19\.0\.0$|^19\.1\.[0-1]$|^19\.2\.0$
  </field>
  <info type="cve">CVE-2025-55182</info> 
  <info type="link">https://nvd.nist.gov/vuln/detail/CVE-2025-55182</info>
  <description>Vulnerable React2Shell detected: React $(version) at $(path)</description>
<mitre><id>T1190</id></mitre>
</rule>


  <!-- Detect Vulnerable Next.js -->
  <rule id="100502" level="15"> 
    <if_sid>100500</if_sid> 
    <field name="package">next</field>  
    <field name="version" type="pcre2">^15\.(0\.[0-4]|1\.[0-8]|2\.[0-5]|3\.[0-5]|4\.[0-7]|5\.[0-6])$|^16\.0\.[0-6]$</field>
  <info type="cve">CVE-2025-66478</info> 
  <info type="link">https://nvd.nist.gov/vuln/detail/CVE-2025-66478</info>
    <description>Vulnerable React2Shell detected: Next.js $(version) at $(path)</description> 
    <mitre><id>T1190</id></mitre> 
  </rule>
</group>
Enter fullscreen mode Exit fullscreen mode

Step 3: Deployment via Ansible

How do you get this onto 100 servers? Don't use Wazuh's "Command Monitoring" for script execution. Giving a SIEM manager the right to run arbitrary scripts on all agents is a major security risk if the manager is compromised.

Instead, use Ansible. Create a playbook to:

  1. Copy the Python script.
  2. Set up a Cron job (e.g., daily at 2 AM).
  3. Ensure the log file permissions are correct.

Notifications / Messenger Integration

To receive real-time alerts in your favorite messenger app, you can use these community integrations:

Wazuh-Telegram-Slack-Teams

Conclusion

By shifting from "System Inventory" to "Application Inventory," we caught a CVSS 10.0 vulnerability that otherwise would have stayed hidden in a local node_modules folder.

Top comments (0)