sicua
Version:
A tool for analyzing project structure and dependencies
400 lines (399 loc) • 18 kB
JavaScript
"use strict";
/**
* Detector for unsafe HTML usage including dangerouslySetInnerHTML without sanitization
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnsafeHTMLDetector = void 0;
const typescript_1 = __importDefault(require("typescript"));
const BaseDetector_1 = require("./BaseDetector");
const ASTTraverser_1 = require("../utils/ASTTraverser");
const security_constants_1 = require("../constants/security.constants");
const general_constants_1 = require("../constants/general.constants");
class UnsafeHTMLDetector extends BaseDetector_1.BaseDetector {
constructor() {
super("UnsafeHTMLDetector", "unsafe-innerhtml", "critical", UnsafeHTMLDetector.HTML_PATTERNS);
}
async detect(scanResult) {
const vulnerabilities = [];
// Filter relevant files
// TODO: MOVE TO CONSTANTS
const relevantFiles = this.filterRelevantFiles(scanResult, [".ts", ".tsx", ".js", ".jsx"], ["node_modules", "dist", "build", ".git", "coverage"]);
for (const filePath of relevantFiles) {
const content = scanResult.fileContents.get(filePath);
if (!content)
continue;
// Check if file has sanitization imports
const hasSanitization = this.detectSanitizationLibraries(content);
// Apply pattern matching
const patternResults = this.applyPatternMatching(content, filePath);
const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateHTMLMatch(match, hasSanitization));
// Apply AST-based analysis
const sourceFile = scanResult.sourceFiles.get(filePath);
if (sourceFile) {
const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForUnsafeHTML(sf, fp, hasSanitization));
vulnerabilities.push(...astVulnerabilities);
}
// Adjust confidence based on file context and sanitization
const fileContext = this.getFileContext(filePath, content);
for (const vuln of patternVulnerabilities) {
// Lower confidence if sanitization is detected
if (hasSanitization.length > 0) {
vuln.confidence = "medium";
vuln.metadata = {
...vuln.metadata,
sanitizationLibraries: hasSanitization,
note: "Sanitization libraries detected - verify proper usage",
};
}
vuln.confidence = this.adjustConfidenceBasedOnContext(vuln, fileContext);
if (this.validateVulnerability(vuln)) {
vulnerabilities.push(vuln);
}
}
}
return vulnerabilities;
}
/**
* Detect sanitization libraries in the file
*/
detectSanitizationLibraries(content) {
const foundLibraries = [];
for (const lib of security_constants_1.SANITIZATION_LIBRARIES) {
if (content.includes(lib)) {
foundLibraries.push(lib);
}
}
return foundLibraries;
}
/**
* Validate if a pattern match represents unsafe HTML usage
*/
validateHTMLMatch(matchResult, sanitizationLibraries) {
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 sanitization is used in the same context
if (this.isSanitizedInContext(match.context || "", sanitizationLibraries)) {
return false;
}
return true;
}
/**
* AST-based analysis for unsafe HTML usage
*/
analyzeASTForUnsafeHTML(sourceFile, filePath, sanitizationLibraries) {
const vulnerabilities = [];
// Find JSX expressions with dangerouslySetInnerHTML
const jsxElements = ASTTraverser_1.ASTTraverser.findJSXElements(sourceFile);
for (const jsxElement of jsxElements) {
const dangerouslySetVuln = this.analyzeDangerouslySetInnerHTML(jsxElement, sourceFile, filePath, sanitizationLibraries);
if (dangerouslySetVuln) {
vulnerabilities.push(dangerouslySetVuln);
}
}
// Find property access expressions for innerHTML/outerHTML
const propertyAccess = ASTTraverser_1.ASTTraverser.findPropertyAccess(sourceFile);
for (const propAccess of propertyAccess) {
const htmlVuln = this.analyzeHTMLPropertyAccess(propAccess, sourceFile, filePath, sanitizationLibraries);
if (htmlVuln) {
vulnerabilities.push(htmlVuln);
}
}
// Find document.write calls
const documentWriteCalls = this.findDocumentWriteCalls(sourceFile);
for (const writeCall of documentWriteCalls) {
const writeVuln = this.analyzeDocumentWrite(writeCall, sourceFile, filePath);
if (writeVuln) {
vulnerabilities.push(writeVuln);
}
}
return vulnerabilities;
}
/**
* Analyze dangerouslySetInnerHTML usage in JSX
*/
analyzeDangerouslySetInnerHTML(jsxElement, sourceFile, filePath, sanitizationLibraries) {
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) &&
attr.name.text === "dangerouslySetInnerHTML") {
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(attr, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(jsxElement, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(attr, sourceFile);
// Check if the content appears to be sanitized
const isSanitized = this.isSanitizedInContext(context, sanitizationLibraries);
// Check if it's safe CSS generation
const isSafeCSS = this.isSafeCSSGeneration(context, code);
// If it's safe CSS generation, lower the severity and confidence
if (isSafeCSS) {
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),
}, "dangerouslySetInnerHTML used for CSS generation - verify content is properly constructed and safe", "medium", "medium", {
hasSanitization: false,
sanitizationLibraries,
jsxElementType: typescript_1.default.isJsxElement(jsxElement)
? "element"
: "self-closing",
isSafeCSS: true,
});
}
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),
}, isSanitized
? "dangerouslySetInnerHTML used with apparent sanitization - verify proper implementation"
: "dangerouslySetInnerHTML used without apparent sanitization - potential XSS vulnerability", "critical", isSanitized ? "medium" : "high", {
hasSanitization: isSanitized,
sanitizationLibraries,
jsxElementType: typescript_1.default.isJsxElement(jsxElement)
? "element"
: "self-closing",
});
}
}
return null;
}
/**
* Analyze innerHTML/outerHTML property access
*/
analyzeHTMLPropertyAccess(propAccess, sourceFile, filePath, sanitizationLibraries) {
if (!typescript_1.default.isIdentifier(propAccess.name))
return null;
const propertyName = propAccess.name.text;
if (propertyName !== "innerHTML" && propertyName !== "outerHTML")
return null;
// Check if this is an assignment (we're looking for assignments, not reads)
const parent = propAccess.parent;
if (!typescript_1.default.isBinaryExpression(parent) ||
parent.operatorToken.kind !== typescript_1.default.SyntaxKind.EqualsToken) {
return null;
}
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(propAccess, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(parent, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(parent, sourceFile);
// Check if it's just clearing innerHTML (safe operation)
if (this.isInnerHTMLClearing(code)) {
return null; // Skip clearing operations
}
const isSanitized = this.isSanitizedInContext(context, sanitizationLibraries);
return this.createVulnerability(filePath, {
line: location.line,
column: location.column,
endLine: location.line,
endColumn: location.column + code.length,
}, {
code,
surroundingContext: context,
functionName: this.extractFunctionFromAST(propAccess),
componentName: this.extractComponentName(filePath),
}, isSanitized
? `${propertyName} assignment with apparent sanitization - verify proper implementation`
: `Direct ${propertyName} assignment detected - potential XSS vulnerability`, "critical", isSanitized ? "medium" : "high", {
propertyName,
hasSanitization: isSanitized,
sanitizationLibraries,
});
}
/**
* Find document.write/document.writeln calls
*/
findDocumentWriteCalls(sourceFile) {
const callExpressions = ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression);
return callExpressions.filter((callExpr) => {
if (typescript_1.default.isPropertyAccessExpression(callExpr.expression)) {
const obj = callExpr.expression.expression;
const method = callExpr.expression.name;
return (typescript_1.default.isIdentifier(obj) &&
obj.text === "document" &&
typescript_1.default.isIdentifier(method) &&
(method.text === "write" || method.text === "writeln"));
}
return false;
});
}
/**
* Analyze document.write calls
*/
analyzeDocumentWrite(callExpr, sourceFile, filePath) {
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(callExpr, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(callExpr, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(callExpr, sourceFile);
const methodName = typescript_1.default.isPropertyAccessExpression(callExpr.expression) &&
typescript_1.default.isIdentifier(callExpr.expression.name)
? callExpr.expression.name.text
: "write";
return this.createVulnerability(filePath, {
line: location.line,
column: location.column,
endLine: location.line,
endColumn: location.column + code.length,
}, {
code,
surroundingContext: context,
functionName: this.extractFunctionFromAST(callExpr),
componentName: this.extractComponentName(filePath),
}, `document.${methodName}() usage detected - this can lead to XSS vulnerabilities`, "critical", "high", {
methodName,
argumentCount: callExpr.arguments.length,
});
}
/**
* Check if sanitization is used in the given context
*/
isSanitizedInContext(context, sanitizationLibraries) {
for (const lib of sanitizationLibraries) {
if (context.includes(lib) &&
(context.includes(".sanitize") || context.includes(".clean"))) {
return true;
}
}
return security_constants_1.SANITIZATION_PATTERNS.some((pattern) => pattern.test(context));
}
/**
* Check if dangerouslySetInnerHTML is used for safe CSS generation
*/
isSafeCSSGeneration(context, code) {
const lowerContext = context.toLowerCase();
const lowerCode = code.toLowerCase();
// Check if it's a style element with CSS generation
const isStyleElement = lowerContext.includes("<style") || lowerContext.includes("style");
// Check for CSS-specific patterns
const hasCSSContent = general_constants_1.CSS_PATTERNS.some((pattern) => lowerCode.includes(pattern) || lowerContext.includes(pattern));
// Check if it's generating style rules dynamically
const isDynamicCSS = lowerCode.includes("entries") ||
lowerCode.includes("map") ||
lowerCode.includes("join") ||
lowerCode.includes("theme");
return isStyleElement && hasCSSContent && isDynamicCSS;
}
/**
* Check if innerHTML is being cleared (safe operation)
*/
isInnerHTMLClearing(code) {
const trimmedCode = code.trim();
return (trimmedCode.endsWith('innerHTML = ""') ||
trimmedCode.endsWith("innerHTML = ''") ||
trimmedCode.endsWith("innerHTML = ``"));
}
/**
* 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.UnsafeHTMLDetector = UnsafeHTMLDetector;
UnsafeHTMLDetector.HTML_PATTERNS = [
{
id: "dangerously-set-inner-html",
name: "dangerouslySetInnerHTML usage",
description: "Usage of dangerouslySetInnerHTML detected - ensure content is properly sanitized",
pattern: {
type: "regex",
expression: /dangerouslySetInnerHTML\s*:/g,
},
vulnerabilityType: "unsafe-innerhtml",
severity: "critical",
confidence: "high",
fileTypes: [".tsx", ".jsx"],
enabled: true,
},
{
id: "innerHTML-assignment",
name: "innerHTML assignment",
description: "Direct innerHTML assignment detected - this can lead to XSS if content is not sanitized",
pattern: {
type: "regex",
expression: /\.innerHTML\s*=/g,
},
vulnerabilityType: "unsafe-innerhtml",
severity: "critical",
confidence: "medium",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
{
id: "outerhtml-assignment",
name: "outerHTML assignment",
description: "Direct outerHTML assignment detected - this can lead to XSS if content is not sanitized",
pattern: {
type: "regex",
expression: /\.outerHTML\s*=/g,
},
vulnerabilityType: "unsafe-innerhtml",
severity: "critical",
confidence: "medium",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
{
id: "document-write",
name: "document.write usage",
description: "document.write() usage detected - this can lead to XSS vulnerabilities",
pattern: {
type: "regex",
expression: /document\.write\s*\(/g,
},
vulnerabilityType: "unsafe-innerhtml",
severity: "critical",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
{
id: "document-writeln",
name: "document.writeln usage",
description: "document.writeln() usage detected - this can lead to XSS vulnerabilities",
pattern: {
type: "regex",
expression: /document\.writeln\s*\(/g,
},
vulnerabilityType: "unsafe-innerhtml",
severity: "critical",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
];