ubon
Version:
Security scanner for AI-generated apps (Cursor, Lovable, Windsurf, v0). Catches hardcoded secrets, prompt injection, hallucinated imports, Server Actions / Edge runtime mistakes, and the vibe-coded vulnerabilities traditional linters miss.
86 lines • 4.73 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const rule = {
meta: {
id: 'TAILWIND001',
category: 'security',
severity: 'medium',
message: 'Dynamic className with unvalidated input - may allow CSS injection',
fix: 'Validate className input against a whitelist of allowed Tailwind classes',
helpUri: 'https://tailwindcss.com/docs/content-configuration#dynamic-class-names',
impact: 'CSS injection can be used for UI spoofing, clickjacking, or exfiltrating data through CSS selectors.'
},
impl: {
detect: (content, file, lines) => {
const results = [];
if (!/(jsx|tsx|svelte|astro)$/.test(file))
return results;
lines.forEach((line, index) => {
// Pattern 1: className={variable} without validation
const classNameVarPattern = /className\s*=\s*\{\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\s*\}/g;
let match;
while ((match = classNameVarPattern.exec(line)) !== null) {
const varName = match[1];
// Skip if it's a literal string or known safe patterns
if (varName === 'className' || varName === 'classes')
continue;
if (/^['"`]/.test(varName))
continue; // Already a string literal
// Check if there's validation nearby (within 5 lines before)
const startCheck = Math.max(0, index - 5);
const contextLines = lines.slice(startCheck, index + 1);
const hasValidation = contextLines.some(l => /whitelist|allowed|includes\(|switch\s*\(/i.test(l) ||
/^(const|let|var)\s+\w+\s*=\s*\[['"`]/.test(l) || // Array of allowed values
/clsx\(|classnames\(|cn\(/i.test(l) // Using class utility with conditions
);
// Check for template literal injection
const hasTemplateLiteral = /className\s*=\s*\{\s*`[^`]*\$\{[^}]+\}[^`]*`\s*\}/i.test(line);
if (!hasValidation || hasTemplateLiteral) {
results.push({
line: index + 1,
match: match[0],
confidence: hasTemplateLiteral ? 0.85 : 0.75
});
}
}
// Pattern 2: className with template literal and variables
const templateLiteralPattern = /className\s*=\s*\{`[^`]*\$\{([^}]+)\}[^`]*`\}/g;
while ((match = templateLiteralPattern.exec(line)) !== null) {
const varContent = match[1];
// Check for validation nearby (within 5 lines before)
const startCheck = Math.max(0, index - 5);
const contextLines = lines.slice(startCheck, index + 1);
const hasValidation = contextLines.some(l => /whitelist|allowed|includes\(|switch\s*\(/i.test(l) ||
/^(const|let|var)\s+\w+\s*=\s*\[['"`]/.test(l) || // Array of allowed values
/clsx\(|classnames\(|cn\(/i.test(l) // Using class utility with conditions
);
// Flag if no validation or if variable looks like user input
const looksLikeUserInput = /(props|user|data|input|query|params|state)/i.test(varContent);
if (!hasValidation || looksLikeUserInput) {
results.push({
line: index + 1,
match: match[0],
confidence: looksLikeUserInput ? 0.85 : 0.75
});
}
}
// Pattern 3: String concatenation in className
const concatPattern = /className\s*=\s*\{[^}]*\+[^}]*\}/g;
while ((match = concatPattern.exec(line)) !== null) {
// Check if it's concatenating with variables (not just static strings)
if (/\+\s*[a-zA-Z_$][a-zA-Z0-9_$]*|\b[a-zA-Z_$][a-zA-Z0-9_$]*\s*\+/.test(match[0])) {
results.push({
line: index + 1,
match: match[0],
confidence: 0.78
});
}
}
});
return results;
},
fileTypes: ['jsx', 'tsx', 'svelte', 'astro']
}
};
exports.default = rule;
//# sourceMappingURL=TAILWIND001.js.map