fix: validate plugin names in tailwind config generator to prevent code injection (#275)

The _format_plugins() method interpolated plugin names directly into
require() statements without sanitization. A plugin name containing
a single quote could break out of require() and inject arbitrary
JavaScript that executes when Node.js loads the generated config.

Add a strict regex allowlist matching valid npm package name patterns
(optional @scope, alphanumeric/hyphen/underscore, optional subpath).
Reject any plugin name that doesn't match before generating output.

Closes #246
This commit is contained in:
Artemii Fridriksen 2026-06-22 13:34:41 -04:00 committed by GitHub
parent 9a863a5275
commit 9bb646b336
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -8,10 +8,16 @@ Supports colors, fonts, spacing, breakpoints, and plugin recommendations.
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
# Valid npm package name pattern: optional @scope/, then package name with
# optional subpath. Only allows alphanumeric, hyphens, dots, underscores,
# and forward slashes — no quotes, parens, or semicolons.
_VALID_PLUGIN_NAME = re.compile(r'^(@[a-zA-Z0-9_-]+/)?[a-zA-Z0-9_-]+(/[a-zA-Z0-9_.-]+)*$')
class TailwindConfigGenerator:
"""Generate Tailwind CSS configuration files."""
@ -230,13 +236,24 @@ module.exports = {{
"""
def _format_plugins(self) -> str:
"""Format plugins array for config."""
"""Format plugins array for config.
Validates each plugin name against a strict allowlist pattern
to prevent code injection via crafted require() statements
(see: CWE-94).
"""
if not self.config["plugins"]:
return ""
plugin_requires = [
f"require('{plugin}')" for plugin in self.config["plugins"]
]
plugin_requires = []
for plugin in self.config["plugins"]:
if not _VALID_PLUGIN_NAME.match(plugin):
raise ValueError(
f"Invalid plugin name: {plugin!r}. "
"Plugin names must be valid npm package names "
"(e.g. '@tailwindcss/typography')."
)
plugin_requires.append(f"require('{plugin}')")
return ", ".join(plugin_requires)
def _indent_json(self, json_str: str, level: int) -> str: