sicua
Version:
A tool for analyzing project structure and dependencies
457 lines (456 loc) • 19.7 kB
JavaScript
"use strict";
/**
* Detector for client-side storage of sensitive data
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClientStorageDetector = void 0;
const typescript_1 = __importDefault(require("typescript"));
const BaseDetector_1 = require("./BaseDetector");
const ASTTraverser_1 = require("../utils/ASTTraverser");
const storage_constants_1 = require("../constants/storage.constants");
const sensitiveData_constants_1 = require("../constants/sensitiveData.constants");
class ClientStorageDetector extends BaseDetector_1.BaseDetector {
constructor() {
super("ClientStorageDetector", "client-storage-sensitive", "medium", ClientStorageDetector.STORAGE_PATTERNS);
}
async detect(scanResult) {
const vulnerabilities = [];
// Filter relevant files (focus on client-side files)
// TODO: MOVE TO CONSTANTS
const relevantFiles = this.filterRelevantFiles(scanResult, [".ts", ".tsx", ".js", ".jsx"], [
"node_modules",
"dist",
"build",
".git",
"coverage",
"__tests__",
".test.",
".spec.",
]);
for (const filePath of relevantFiles) {
const content = scanResult.fileContents.get(filePath);
if (!content)
continue;
// Only analyze client-side files
const fileContext = this.getFileContext(filePath, content);
if (!fileContext.isClientSide ||
fileContext.fileType === "api-route" ||
fileContext.fileType === "middleware") {
continue;
}
// Check for storage libraries
const storageLibraries = this.detectStorageLibraries(content);
// Apply pattern matching
const patternResults = this.applyPatternMatching(content, filePath);
const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateStorageMatch(match));
// Apply AST-based analysis
const sourceFile = scanResult.sourceFiles.get(filePath);
if (sourceFile) {
const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForClientStorage(sf, fp, storageLibraries));
vulnerabilities.push(...astVulnerabilities);
}
// Process pattern vulnerabilities
for (const vuln of patternVulnerabilities) {
// Add library information if detected
if (storageLibraries.length > 0) {
vuln.metadata = {
...vuln.metadata,
storageLibraries,
note: "Storage libraries detected - verify their security configuration",
};
}
vuln.confidence = this.adjustConfidenceBasedOnContext(vuln, fileContext);
if (this.validateVulnerability(vuln)) {
vulnerabilities.push(vuln);
}
}
}
return vulnerabilities;
}
/**
* Detect storage libraries used in the file
*/
detectStorageLibraries(content) {
const foundLibraries = [];
for (const lib of storage_constants_1.STORAGE_LIBRARIES) {
if (content.includes(lib)) {
foundLibraries.push(lib);
}
}
return foundLibraries;
}
/**
* Validate if a storage pattern match is problematic
*/
validateStorageMatch(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 client storage usage
*/
analyzeASTForClientStorage(sourceFile, filePath, storageLibraries) {
const vulnerabilities = [];
// Find storage API calls
const storageApiCalls = this.findStorageApiCalls(sourceFile);
for (const apiCall of storageApiCalls) {
const apiVuln = this.analyzeStorageApiCall(apiCall, sourceFile, filePath);
if (apiVuln) {
vulnerabilities.push(apiVuln);
}
}
// Find storage library calls
const storageLibraryCalls = this.findStorageLibraryCalls(sourceFile, storageLibraries);
for (const libCall of storageLibraryCalls) {
const libVuln = this.analyzeStorageLibraryCall(libCall, sourceFile, filePath);
if (libVuln) {
vulnerabilities.push(libVuln);
}
}
return vulnerabilities;
}
/**
* Find storage API calls (localStorage, sessionStorage, etc.)
*/
findStorageApiCalls(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;
if (typescript_1.default.isIdentifier(obj) && typescript_1.default.isIdentifier(method)) {
return (storage_constants_1.STORAGE_APIS.includes(obj.text) &&
(method.text === "setItem" ||
method.text === "getItem" ||
method.text === "removeItem" ||
method.text === "clear"));
}
}
return false;
});
}
/**
* Find storage library calls
*/
findStorageLibraryCalls(sourceFile, storageLibraries) {
if (storageLibraries.length === 0)
return [];
return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression, (node) => {
// Look for library method calls
if (typescript_1.default.isPropertyAccessExpression(node.expression)) {
const obj = node.expression.expression;
const method = node.expression.name;
if (typescript_1.default.isIdentifier(obj) && typescript_1.default.isIdentifier(method)) {
const objName = obj.text.toLowerCase();
return (storageLibraries.some((lib) => objName.includes(lib.replace("-", "")) ||
objName.includes("storage") ||
objName.includes("db")) &&
(method.text === "set" ||
method.text === "get" ||
method.text === "put" ||
method.text === "add"));
}
}
return false;
});
}
/**
* Analyze storage API call
*/
analyzeStorageApiCall(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);
// Get storage type and method
const storageInfo = this.getStorageInfo(callExpr);
if (!storageInfo)
return null;
// Analyze the key and value for sensitive data
const sensitivityAnalysis = this.analyzeStorageSensitivity(callExpr);
if (!sensitivityAnalysis ||
sensitivityAnalysis.sensitivityLevel === "none") {
return null;
}
// Check if this appears to be UI state storage
const keyArg = callExpr.arguments[0];
if (keyArg && typescript_1.default.isStringLiteral(keyArg)) {
if (this.isUIStateStorage(keyArg.text, context)) {
return null; // Skip UI state storage
}
}
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),
}, `${storageInfo.storageType}.${storageInfo.method}() used with potentially sensitive data: ${sensitivityAnalysis.sensitiveKeys.join(", ")}`, "medium", sensitivityAnalysis.confidence, {
storageType: storageInfo.storageType,
method: storageInfo.method,
sensitiveKeys: sensitivityAnalysis.sensitiveKeys,
sensitivityLevel: sensitivityAnalysis.sensitivityLevel,
recommendations: sensitivityAnalysis.recommendations,
detectionMethod: "storage-api-analysis",
});
}
/**
* Analyze storage library call
*/
analyzeStorageLibraryCall(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);
// Analyze for sensitive data patterns
const sensitivityAnalysis = this.analyzeStorageSensitivity(callExpr);
if (!sensitivityAnalysis ||
sensitivityAnalysis.sensitivityLevel === "none") {
return null;
}
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),
}, `Storage library used with potentially sensitive data: ${sensitivityAnalysis.sensitiveKeys.join(", ")}`, "medium", sensitivityAnalysis.confidence, {
libraryCall: true,
sensitiveKeys: sensitivityAnalysis.sensitiveKeys,
sensitivityLevel: sensitivityAnalysis.sensitivityLevel,
recommendations: sensitivityAnalysis.recommendations,
detectionMethod: "storage-library-analysis",
});
}
/**
* Get storage type and method information
*/
getStorageInfo(callExpr) {
if (typescript_1.default.isPropertyAccessExpression(callExpr.expression)) {
const obj = callExpr.expression.expression;
const method = callExpr.expression.name;
if (typescript_1.default.isIdentifier(obj) && typescript_1.default.isIdentifier(method)) {
return {
storageType: obj.text,
method: method.text,
};
}
}
return null;
}
/**
* Analyze storage call for sensitive data
*/
analyzeStorageSensitivity(callExpr) {
const analysis = {
sensitiveKeys: [],
sensitivityLevel: "none",
confidence: "low",
recommendations: [],
};
// Analyze arguments for sensitive data
for (let i = 0; i < callExpr.arguments.length; i++) {
const arg = callExpr.arguments[i];
const sensitiveData = this.extractSensitiveDataFromArgument(arg);
if (sensitiveData.length > 0) {
analysis.sensitiveKeys.push(...sensitiveData);
}
}
if (analysis.sensitiveKeys.length === 0) {
return null;
}
// Determine sensitivity level and confidence
analysis.sensitivityLevel = this.determineSensitivityLevel(analysis.sensitiveKeys);
analysis.confidence = this.determineConfidenceLevel(analysis.sensitiveKeys);
analysis.recommendations = this.generateStorageRecommendations(analysis.sensitiveKeys);
return analysis;
}
/**
* Extract sensitive data indicators from function argument
*/
extractSensitiveDataFromArgument(arg) {
const sensitiveData = [];
if (typescript_1.default.isStringLiteral(arg)) {
const text = arg.text.toLowerCase();
// Check if it's explicitly non-sensitive or UI state
if (sensitiveData_constants_1.NON_SENSITIVE_TERMS.some((term) => text.includes(term))) {
return []; // Return empty array for non-sensitive terms
}
if (storage_constants_1.UI_STATE_TERMS.some((term) => text.includes(term))) {
return []; // Return empty for UI state
}
const foundSensitive = sensitiveData_constants_1.SENSITIVE_DATA_KEYWORDS.filter((keyword) => text.includes(keyword));
sensitiveData.push(...foundSensitive);
}
else if (typescript_1.default.isIdentifier(arg)) {
const name = arg.text.toLowerCase();
if (sensitiveData_constants_1.NON_SENSITIVE_TERMS.some((term) => name.includes(term))) {
return [];
}
if (storage_constants_1.UI_STATE_TERMS.some((term) => name.includes(term))) {
return [];
}
const foundSensitive = sensitiveData_constants_1.SENSITIVE_DATA_KEYWORDS.filter((keyword) => name.includes(keyword));
sensitiveData.push(...foundSensitive);
}
else if (typescript_1.default.isPropertyAccessExpression(arg)) {
const propertyName = typescript_1.default.isIdentifier(arg.name)
? arg.name.text.toLowerCase()
: "";
if (sensitiveData_constants_1.NON_SENSITIVE_TERMS.some((term) => propertyName.includes(term))) {
return [];
}
const foundSensitive = sensitiveData_constants_1.SENSITIVE_DATA_KEYWORDS.filter((keyword) => propertyName.includes(keyword));
sensitiveData.push(...foundSensitive);
}
return sensitiveData;
}
/**
* Check if storage usage is for UI state rather than sensitive data
*/
isUIStateStorage(key, context) {
const lowerKey = key.toLowerCase();
const lowerContext = context.toLowerCase();
// Check if key or context suggests UI state
const isUIKey = storage_constants_1.UI_STATE_PATTERNS.some((pattern) => lowerKey.includes(pattern) || lowerContext.includes(pattern));
// Additional context checks for common UI patterns
const hasUIContext = lowerContext.includes("component") ||
lowerContext.includes("hook") ||
lowerContext.includes("use") ||
lowerContext.includes("state") ||
lowerContext.includes("local");
return isUIKey || hasUIContext;
}
/**
* Determine sensitivity level based on found keywords
*/
determineSensitivityLevel(sensitiveKeys) {
if (sensitiveKeys.some((key) => sensitiveData_constants_1.HIGH_SENSITIVITY_KEYWORDS.includes(key))) {
return "high";
}
else if (sensitiveKeys.some((key) => sensitiveData_constants_1.MEDIUM_SENSITIVITY_KEYWORDS.includes(key))) {
return "medium";
}
else if (sensitiveKeys.length > 0) {
return "low";
}
return "none";
}
/**
* Determine confidence level based on context
*/
determineConfidenceLevel(sensitiveKeys) {
if (sensitiveKeys.some((key) => sensitiveData_constants_1.CLIENT_EXPLICIT_SENSITIVE.includes(key))) {
return "high";
}
else if (sensitiveKeys.length > 1) {
return "medium";
}
return "low";
}
/**
* Generate storage security recommendations
*/
generateStorageRecommendations(sensitiveKeys) {
const recommendations = [];
if (sensitiveKeys.includes("password") ||
sensitiveKeys.includes("secret")) {
recommendations.push("Never store passwords or secrets in client-side storage");
recommendations.push("Use secure server-side session management instead");
}
if (sensitiveKeys.includes("token") || sensitiveKeys.includes("jwt")) {
recommendations.push("Consider using httpOnly cookies for token storage");
recommendations.push("Implement token rotation and expiration");
}
if (sensitiveKeys.length > 0) {
recommendations.push("Encrypt sensitive data before storing");
recommendations.push("Use sessionStorage instead of localStorage for temporary data");
recommendations.push("Implement data cleanup on logout");
}
return recommendations;
}
/**
* 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.ClientStorageDetector = ClientStorageDetector;
ClientStorageDetector.STORAGE_PATTERNS = [
{
id: "localstorage-sensitive",
name: "localStorage with sensitive data",
description: "localStorage usage with potentially sensitive data detected",
pattern: {
type: "regex",
expression: /localStorage\.(setItem|getItem)\s*\([^)]*(?:password|token|secret|key|auth|jwt|session)[^)]*/gi,
},
vulnerabilityType: "client-storage-sensitive",
severity: "medium",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
{
id: "sessionstorage-sensitive",
name: "sessionStorage with sensitive data",
description: "sessionStorage usage with potentially sensitive data detected",
pattern: {
type: "regex",
expression: /sessionStorage\.(setItem|getItem)\s*\([^)]*(?:password|token|secret|key|auth|jwt|session)[^)]*/gi,
},
vulnerabilityType: "client-storage-sensitive",
severity: "medium",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
{
id: "indexeddb-sensitive",
name: "IndexedDB with sensitive data",
description: "IndexedDB usage with potentially sensitive data detected",
pattern: {
type: "regex",
expression: /indexedDB\.|IDBDatabase/gi,
},
vulnerabilityType: "client-storage-sensitive",
severity: "medium",
confidence: "low",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
];