sicua
Version:
A tool for analyzing project structure and dependencies
511 lines (510 loc) • 21.4 kB
JavaScript
"use strict";
/**
* Detector for React-specific security anti-patterns
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReactAntiPatternDetector = void 0;
const typescript_1 = __importDefault(require("typescript"));
const BaseDetector_1 = require("./BaseDetector");
const ASTTraverser_1 = require("../utils/ASTTraverser");
class ReactAntiPatternDetector extends BaseDetector_1.BaseDetector {
constructor() {
super("ReactAntiPatternDetector", "react-antipattern", "critical", ReactAntiPatternDetector.REACT_ANTIPATTERN_PATTERNS);
}
async detect(scanResult) {
const vulnerabilities = [];
// Filter for React/JSX files only
const reactFiles = this.filterReactFiles(scanResult);
for (const filePath of reactFiles) {
const content = scanResult.fileContents.get(filePath);
if (!content)
continue;
// Apply AST-based analysis for comprehensive detection
const sourceFile = scanResult.sourceFiles.get(filePath);
if (sourceFile) {
const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForReactAntiPatterns(sf, fp));
vulnerabilities.push(...astVulnerabilities);
}
// Apply pattern matching as backup
const patternResults = this.applyPatternMatching(content, filePath);
const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateReactAntiPatternMatch(match));
// Process pattern vulnerabilities
for (const vuln of patternVulnerabilities) {
if (this.validateVulnerability(vuln)) {
vulnerabilities.push(vuln);
}
}
}
return vulnerabilities;
}
/**
* Filter files to only include React/JSX files
*/
filterReactFiles(scanResult) {
return scanResult.filePaths.filter((filePath) => {
// Only process React/JSX files
if (![".tsx", ".jsx", ".ts", ".js"].some((ext) => filePath.endsWith(ext))) {
return false;
}
// Check if file contains React/JSX content
const content = scanResult.fileContents.get(filePath);
if (!content)
return false;
return this.isReactFile(content);
});
}
/**
* Check if file contains React/JSX content
*/
isReactFile(content) {
const reactIndicators = [
/import\s+.*React.*from\s+['"]react['"]/,
/import\s+.*\{[^}]*useState[^}]*\}.*from\s+['"]react['"]/,
/import\s+.*\{[^}]*useEffect[^}]*\}.*from\s+['"]react['"]/,
/export\s+default\s+function\s+\w+\s*\([^)]*\)\s*\{[\s\S]*return\s*\(/,
/<[A-Z][a-zA-Z0-9]*[\s>]/, // JSX component
/<\/[A-Z][a-zA-Z0-9]*>/, // JSX closing tag
/React\./,
/\.jsx?$|\.tsx?$/,
];
return reactIndicators.some((indicator) => indicator.test(content));
}
/**
* Validate if a React anti-pattern match is actually problematic
*/
validateReactAntiPatternMatch(matchResult) {
const match = matchResult.matches[0];
if (!match)
return false;
// Check if it's in a comment
if (this.isInComment(match.context || "", match.match)) {
return false;
}
// Check if it's in test code
if (this.isInTestContext(match.context || "")) {
return false;
}
return true;
}
/**
* AST-based analysis for React anti-patterns
*/
analyzeASTForReactAntiPatterns(sourceFile, filePath) {
const vulnerabilities = [];
// Find React.createElement calls
const createElementCalls = this.findReactCreateElementCalls(sourceFile);
for (const createElementCall of createElementCalls) {
const createElementVuln = this.analyzeCreateElementCall(createElementCall, sourceFile, filePath);
if (createElementVuln) {
vulnerabilities.push(createElementVuln);
}
}
// Find JSX elements
const jsxElements = ASTTraverser_1.ASTTraverser.findJSXElements(sourceFile);
for (const jsxElement of jsxElements) {
const jsxVuln = this.analyzeJSXElement(jsxElement, sourceFile, filePath);
if (jsxVuln) {
vulnerabilities.push(jsxVuln);
}
}
// Find ref usage with dangerous operations
const refUsages = this.findDangerousRefUsage(sourceFile);
for (const refUsage of refUsages) {
const refVuln = this.analyzeRefUsage(refUsage, sourceFile, filePath);
if (refVuln) {
vulnerabilities.push(refVuln);
}
}
return vulnerabilities;
}
/**
* Find React.createElement calls
*/
findReactCreateElementCalls(sourceFile) {
return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression, (node) => {
if (typescript_1.default.isPropertyAccessExpression(node.expression)) {
const obj = node.expression.expression;
const method = node.expression.name;
return (typescript_1.default.isIdentifier(obj) &&
obj.text === "React" &&
typescript_1.default.isIdentifier(method) &&
method.text === "createElement");
}
return false;
});
}
/**
* Find dangerous ref usage
*/
findDangerousRefUsage(sourceFile) {
return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.PropertyAccessExpression, (node) => {
// Look for ref.current.dangerousOperation
if (typescript_1.default.isPropertyAccessExpression(node.expression) &&
typescript_1.default.isIdentifier(node.name) &&
ReactAntiPatternDetector.DANGEROUS_REF_OPERATIONS.includes(node.name.text)) {
const refAccess = node.expression;
if (typescript_1.default.isPropertyAccessExpression(refAccess) &&
typescript_1.default.isIdentifier(refAccess.name) &&
refAccess.name.text === "current") {
return true;
}
}
return false;
});
}
/**
* Analyze React.createElement call for dangerous patterns
*/
analyzeCreateElementCall(createElementCall, sourceFile, filePath) {
if (createElementCall.arguments.length === 0)
return null;
const firstArg = createElementCall.arguments[0];
let elementType = null;
if (typescript_1.default.isStringLiteral(firstArg)) {
elementType = firstArg.text;
}
else if (typescript_1.default.isIdentifier(firstArg)) {
elementType = firstArg.text;
}
if (!elementType)
return null;
// Check if creating dangerous HTML elements
if (ReactAntiPatternDetector.DANGEROUS_HTML_ELEMENTS.includes(elementType.toLowerCase())) {
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(createElementCall, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(createElementCall, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(createElementCall, sourceFile);
// Check if there are props or children that could be user-controlled
const hasUserContent = this.hasUserControlledContent(createElementCall);
return this.createVulnerability(filePath, {
line: location.line,
column: location.column,
endLine: location.line,
endColumn: location.column + code.length,
}, {
code,
surroundingContext: context,
functionName: this.extractFunctionFromAST(createElementCall),
componentName: this.extractComponentName(filePath),
}, `React.createElement used to create '${elementType}' element - ${hasUserContent ? "with user-controlled content, " : ""}potential XSS vulnerability`, elementType === "script" ? "critical" : "high", hasUserContent ? "high" : "medium", {
elementType,
hasUserContent,
createMethod: "React.createElement",
recommendations: [
`Avoid creating '${elementType}' elements dynamically`,
"Use safer alternatives for dynamic content",
"Sanitize any user input before rendering",
"Consider using dangerouslySetInnerHTML with proper sanitization if needed",
],
detectionMethod: "react-create-element-analysis",
});
}
return null;
}
/**
* Analyze JSX element for dangerous patterns
*/
analyzeJSXElement(jsxElement, sourceFile, filePath) {
const tagName = this.getJSXTagName(jsxElement);
if (!tagName)
return null;
// Check dangerous HTML elements
if (ReactAntiPatternDetector.DANGEROUS_HTML_ELEMENTS.includes(tagName.toLowerCase())) {
const hasUserContent = this.jsxHasUserControlledContent(jsxElement);
// Special handling for script tags
if (tagName.toLowerCase() === "script") {
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(jsxElement, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(jsxElement, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(jsxElement, sourceFile);
return this.createVulnerability(filePath, {
line: location.line,
column: location.column,
endLine: location.line,
endColumn: location.column + code.length,
}, {
code,
surroundingContext: context,
functionName: this.extractFunctionFromAST(jsxElement),
componentName: this.extractComponentName(filePath),
}, `JSX script element detected - ${hasUserContent ? "contains user-controlled content, " : ""}potential XSS vulnerability`, "critical", "high", {
elementType: tagName,
hasUserContent,
createMethod: "JSX",
recommendations: [
"Remove script tags from JSX",
"Load scripts through proper React mechanisms",
"Use useEffect for script loading if needed",
"Never include user content in script tags",
],
detectionMethod: "jsx-element-analysis",
});
}
// Check for dangerous props
const dangerousProps = this.findDangerousJSXProps(jsxElement);
if (dangerousProps.length > 0) {
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(jsxElement, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(jsxElement, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(jsxElement, sourceFile);
return this.createVulnerability(filePath, {
line: location.line,
column: location.column,
endLine: location.line,
endColumn: location.column + code.length,
}, {
code,
surroundingContext: context,
functionName: this.extractFunctionFromAST(jsxElement),
componentName: this.extractComponentName(filePath),
}, `JSX ${tagName} element with potentially dangerous props: ${dangerousProps.join(", ")}`, "high", "medium", {
elementType: tagName,
dangerousProps,
createMethod: "JSX",
recommendations: [
"Validate and sanitize prop values",
"Avoid user-controlled content in event handlers",
"Use safe alternatives for dynamic URLs",
],
detectionMethod: "jsx-props-analysis",
});
}
}
return null;
}
/**
* Analyze ref usage for dangerous operations
*/
analyzeRefUsage(refUsage, sourceFile, filePath) {
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(refUsage, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(refUsage, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(refUsage, sourceFile);
const operation = typescript_1.default.isIdentifier(refUsage.name)
? refUsage.name.text
: "unknown";
return this.createVulnerability(filePath, {
line: location.line,
column: location.column,
endLine: location.line,
endColumn: location.column + code.length,
}, {
code,
surroundingContext: context,
functionName: this.extractFunctionFromAST(refUsage),
componentName: this.extractComponentName(filePath),
}, `Dangerous ref operation '${operation}' detected - potential XSS vulnerability`, "high", "high", {
operation,
recommendations: [
"Avoid direct DOM manipulation through refs",
"Use React state and props for content updates",
"Sanitize any content before setting innerHTML",
"Consider using dangerouslySetInnerHTML with proper sanitization",
],
detectionMethod: "ref-usage-analysis",
});
}
/**
* Get JSX tag name
*/
getJSXTagName(jsxElement) {
if (typescript_1.default.isJsxElement(jsxElement)) {
const tagName = jsxElement.openingElement.tagName;
if (typescript_1.default.isIdentifier(tagName)) {
return tagName.text;
}
}
else if (typescript_1.default.isJsxSelfClosingElement(jsxElement)) {
const tagName = jsxElement.tagName;
if (typescript_1.default.isIdentifier(tagName)) {
return tagName.text;
}
}
return null;
}
/**
* Check if React.createElement has user-controlled content
*/
hasUserControlledContent(createElementCall) {
// Check props (second argument)
if (createElementCall.arguments.length > 1) {
const propsArg = createElementCall.arguments[1];
if (this.containsUserVariables(propsArg)) {
return true;
}
}
// Check children (third+ arguments)
for (let i = 2; i < createElementCall.arguments.length; i++) {
const child = createElementCall.arguments[i];
if (this.containsUserVariables(child)) {
return true;
}
}
return false;
}
/**
* Check if JSX element has user-controlled content
*/
jsxHasUserControlledContent(jsxElement) {
// Check attributes
const attributes = typescript_1.default.isJsxElement(jsxElement)
? jsxElement.openingElement.attributes.properties
: jsxElement.attributes.properties;
for (const attr of attributes) {
if (typescript_1.default.isJsxAttribute(attr) && attr.initializer) {
if (typescript_1.default.isJsxExpression(attr.initializer) &&
attr.initializer.expression) {
if (this.containsUserVariables(attr.initializer.expression)) {
return true;
}
}
}
}
// Check children for JSX elements
if (typescript_1.default.isJsxElement(jsxElement)) {
for (const child of jsxElement.children) {
if (typescript_1.default.isJsxExpression(child) && child.expression) {
if (this.containsUserVariables(child.expression)) {
return true;
}
}
}
}
return false;
}
/**
* Find dangerous JSX props
*/
findDangerousJSXProps(jsxElement) {
const dangerousProps = [];
const attributes = typescript_1.default.isJsxElement(jsxElement)
? jsxElement.openingElement.attributes.properties
: jsxElement.attributes.properties;
for (const attr of attributes) {
if (typescript_1.default.isJsxAttribute(attr) && typescript_1.default.isIdentifier(attr.name)) {
const propName = attr.name.text;
if (ReactAntiPatternDetector.DANGEROUS_PROPS.includes(propName)) {
// Check if the prop value contains user variables
if (attr.initializer &&
typescript_1.default.isJsxExpression(attr.initializer) &&
attr.initializer.expression) {
if (this.containsUserVariables(attr.initializer.expression)) {
dangerousProps.push(propName);
}
}
}
}
}
return dangerousProps;
}
/**
* Check if expression contains user-controlled variables
*/
containsUserVariables(expr) {
// Simple heuristic - look for variables that suggest user input
const userInputIndicators = [
"props",
"userInput",
"query",
"params",
"body",
"request",
"form",
"input",
"data",
"content",
"message",
"text",
];
const exprText = expr.getText().toLowerCase();
return userInputIndicators.some((indicator) => exprText.includes(indicator));
}
/**
* Extract function name from AST node context
*/
extractFunctionFromAST(node) {
let current = node.parent;
while (current) {
if (typescript_1.default.isFunctionDeclaration(current) && current.name) {
return current.name.text;
}
if (typescript_1.default.isMethodDeclaration(current) && typescript_1.default.isIdentifier(current.name)) {
return current.name.text;
}
if (typescript_1.default.isVariableDeclaration(current) &&
typescript_1.default.isIdentifier(current.name) &&
current.initializer &&
(typescript_1.default.isFunctionExpression(current.initializer) ||
typescript_1.default.isArrowFunction(current.initializer))) {
return current.name.text;
}
current = current.parent;
}
return undefined;
}
}
exports.ReactAntiPatternDetector = ReactAntiPatternDetector;
ReactAntiPatternDetector.REACT_ANTIPATTERN_PATTERNS = [
{
id: "react-create-element-script",
name: "React.createElement with script tag",
description: "React.createElement used to create script elements - potential XSS vulnerability",
pattern: {
type: "regex",
expression: /React\.createElement\s*\(\s*['"`]script['"`]/g,
},
vulnerabilityType: "react-antipattern",
severity: "critical",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
{
id: "jsx-script-with-variable",
name: "JSX script tag with variable content",
description: "JSX script element contains variable content - potential XSS vulnerability",
pattern: {
type: "regex",
expression: /<script[^>]*>\s*\{[^}]*\}/g,
},
vulnerabilityType: "react-antipattern",
severity: "critical",
confidence: "high",
fileTypes: [".tsx", ".jsx"],
enabled: true,
},
];
// Dangerous HTML elements that should not contain user content
ReactAntiPatternDetector.DANGEROUS_HTML_ELEMENTS = [
"script",
"iframe",
"object",
"embed",
"link",
"meta",
"style",
];
// React props that can execute JavaScript
ReactAntiPatternDetector.DANGEROUS_PROPS = [
"onClick",
"onLoad",
"onError",
"onFocus",
"onBlur",
"onChange",
"onSubmit",
"onMouseOver",
"onMouseOut",
"onKeyDown",
"onKeyUp",
"href",
"src",
"action",
"formAction",
];
// Ref-related anti-patterns
ReactAntiPatternDetector.DANGEROUS_REF_OPERATIONS = [
"innerHTML",
"outerHTML",
"insertAdjacentHTML",
"execCommand",
];