UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

511 lines (510 loc) 21.4 kB
"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", ];