sicua
Version:
A tool for analyzing project structure and dependencies
579 lines (578 loc) • 21.6 kB
JavaScript
;
/**
* Detector for server-only imports in client-side code
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServerOnlyImportsDetector = void 0;
const typescript_1 = __importDefault(require("typescript"));
const BaseDetector_1 = require("./BaseDetector");
const ASTTraverser_1 = require("../utils/ASTTraverser");
class ServerOnlyImportsDetector extends BaseDetector_1.BaseDetector {
constructor() {
super("ServerOnlyImportsDetector", "server-only-import", "high", ServerOnlyImportsDetector.SERVER_IMPORT_PATTERNS);
}
async detect(scanResult) {
const vulnerabilities = [];
// Filter for client-side files only
const clientFiles = this.filterClientSideFiles(scanResult);
for (const filePath of clientFiles) {
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.analyzeASTForServerImports(sf, fp));
vulnerabilities.push(...astVulnerabilities);
}
// Apply pattern matching as backup
const patternResults = this.applyPatternMatching(content, filePath);
const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateServerImportMatch(match, filePath));
// Process pattern vulnerabilities
for (const vuln of patternVulnerabilities) {
if (this.validateVulnerability(vuln)) {
vulnerabilities.push(vuln);
}
}
}
return vulnerabilities;
}
/**
* Filter files to only include client-side files
*/
filterClientSideFiles(scanResult) {
return scanResult.filePaths.filter((filePath) => {
const normalizedPath = filePath.replace(/\\/g, "/");
// Exclude server-side file locations
const serverPaths = [
"/api/",
"/pages/api/",
"/app/api/",
"/server/",
"/lib/server/",
"/utils/server/",
"middleware.ts",
"middleware.js",
"/prisma/",
"/database/",
"/db/",
"next.config",
".env",
];
if (serverPaths.some((serverPath) => normalizedPath.includes(serverPath))) {
return false;
}
// Include client-side file locations
const clientPaths = [
"/components/",
"/pages/",
"/app/",
"/src/",
"/hooks/",
"/utils/",
"/lib/",
];
// Only process supported file extensions
const supportedExtensions = [".ts", ".tsx", ".js", ".jsx"];
if (!supportedExtensions.some((ext) => filePath.endsWith(ext))) {
return false;
}
// Include if it's in a client path or not specifically server
return (clientPaths.some((clientPath) => normalizedPath.includes(clientPath)) ||
!serverPaths.some((serverPath) => normalizedPath.includes(serverPath)));
});
}
/**
* Validate if a server import pattern match is actually problematic
*/
validateServerImportMatch(matchResult, filePath) {
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 a type-only import (TypeScript)
if (match.context && match.context.includes("import type")) {
return false;
}
return true;
}
/**
* AST-based analysis for server-only imports
*/
analyzeASTForServerImports(sourceFile, filePath) {
const vulnerabilities = [];
// Find import declarations
const importDeclarations = this.findImportDeclarations(sourceFile);
for (const importDecl of importDeclarations) {
const importVuln = this.analyzeImportDeclaration(importDecl, sourceFile, filePath);
if (importVuln) {
vulnerabilities.push(importVuln);
}
}
// Find require calls
const requireCalls = this.findRequireCalls(sourceFile);
for (const requireCall of requireCalls) {
const requireVuln = this.analyzeRequireCall(requireCall, sourceFile, filePath);
if (requireVuln) {
vulnerabilities.push(requireVuln);
}
}
// Find dynamic imports
const dynamicImports = this.findDynamicImports(sourceFile);
for (const dynamicImport of dynamicImports) {
const dynamicVuln = this.analyzeDynamicImport(dynamicImport, sourceFile, filePath);
if (dynamicVuln) {
vulnerabilities.push(dynamicVuln);
}
}
return vulnerabilities;
}
/**
* Find import declarations
*/
findImportDeclarations(sourceFile) {
return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.ImportDeclaration);
}
/**
* Find require() calls
*/
findRequireCalls(sourceFile) {
return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression, (node) => {
return (typescript_1.default.isIdentifier(node.expression) && node.expression.text === "require");
});
}
/**
* Find dynamic import() calls
*/
findDynamicImports(sourceFile) {
return ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression, (node) => {
return node.expression.kind === typescript_1.default.SyntaxKind.ImportKeyword;
});
}
/**
* Analyze import declaration for server-only modules
*/
analyzeImportDeclaration(importDecl, sourceFile, filePath) {
if (!typescript_1.default.isStringLiteral(importDecl.moduleSpecifier)) {
return null;
}
const moduleName = importDecl.moduleSpecifier.text;
// Skip type-only imports
if (importDecl.importClause?.isTypeOnly) {
return null;
}
// Skip TypeScript AST type imports in type definition files - ADD THIS CHECK
if (this.isTypeDefinitionFile(filePath) && moduleName === "typescript") {
return null;
}
const serverImportAnalysis = this.analyzeModuleForServerOnly(moduleName);
if (!serverImportAnalysis)
return null;
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(importDecl, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(importDecl, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(importDecl, 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(importDecl),
componentName: this.extractComponentName(filePath),
}, serverImportAnalysis.description, serverImportAnalysis.severity, serverImportAnalysis.confidence, {
moduleName,
importType: "import-declaration",
category: serverImportAnalysis.category,
recommendations: serverImportAnalysis.recommendations,
detectionMethod: "import-declaration-analysis",
});
}
/**
* Analyze require call for server-only modules
*/
analyzeRequireCall(requireCall, sourceFile, filePath) {
if (requireCall.arguments.length === 0)
return null;
const firstArg = requireCall.arguments[0];
if (!typescript_1.default.isStringLiteral(firstArg))
return null;
const moduleName = firstArg.text;
const serverImportAnalysis = this.analyzeModuleForServerOnly(moduleName);
if (!serverImportAnalysis)
return null;
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(requireCall, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(requireCall, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(requireCall, 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(requireCall),
componentName: this.extractComponentName(filePath),
}, serverImportAnalysis.description, serverImportAnalysis.severity, serverImportAnalysis.confidence, {
moduleName,
importType: "require-call",
category: serverImportAnalysis.category,
recommendations: serverImportAnalysis.recommendations,
detectionMethod: "require-call-analysis",
});
}
/**
* Analyze dynamic import for server-only modules
*/
analyzeDynamicImport(dynamicImport, sourceFile, filePath) {
if (dynamicImport.arguments.length === 0)
return null;
const firstArg = dynamicImport.arguments[0];
if (!typescript_1.default.isStringLiteral(firstArg))
return null;
const moduleName = firstArg.text;
const serverImportAnalysis = this.analyzeModuleForServerOnly(moduleName);
if (!serverImportAnalysis)
return null;
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(dynamicImport, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(dynamicImport, sourceFile);
const code = ASTTraverser_1.ASTTraverser.getNodeText(dynamicImport, 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(dynamicImport),
componentName: this.extractComponentName(filePath),
}, serverImportAnalysis.description, serverImportAnalysis.severity, serverImportAnalysis.confidence, {
moduleName,
importType: "dynamic-import",
category: serverImportAnalysis.category,
recommendations: serverImportAnalysis.recommendations,
detectionMethod: "dynamic-import-analysis",
});
}
/**
* Analyze module name to determine if it's server-only
*/
analyzeModuleForServerOnly(moduleName) {
// Check Node.js built-ins
if (ServerOnlyImportsDetector.NODEJS_BUILTINS.includes(moduleName)) {
return {
description: `Node.js built-in module '${moduleName}' imported in client code - this will fail at runtime`,
severity: "high",
confidence: "high",
category: "nodejs-builtin",
recommendations: [
"Remove this import from client-side code",
"Move functionality to server-side API route",
"Use browser-compatible alternatives if available",
"Consider using a polyfill or web API equivalent",
],
};
}
// Check server-only libraries
const isServerOnlyLib = ServerOnlyImportsDetector.SERVER_ONLY_LIBRARIES.some((lib) => moduleName === lib || moduleName.startsWith(`${lib}/`));
if (isServerOnlyLib) {
const isCritical = this.isCriticalServerLibrary(moduleName);
return {
description: `Server-only library '${moduleName}' imported in client code - ${isCritical
? "may expose sensitive functionality"
: "will fail at runtime"}`,
severity: isCritical ? "critical" : "high",
confidence: "high",
category: isCritical
? "sensitive-server-library"
: "server-only-library",
recommendations: [
"Move this functionality to server-side code",
"Create API endpoint for client-server communication",
"Use client-compatible alternatives if needed",
...(isCritical
? ["Ensure no credentials or sensitive data are exposed"]
: []),
],
};
}
// Check potentially server-only libraries
const isPotentiallyServerOnly = ServerOnlyImportsDetector.POTENTIALLY_SERVER_ONLY.some((lib) => moduleName === lib || moduleName.startsWith(`${lib}/`));
if (isPotentiallyServerOnly) {
return {
description: `Potentially server-only library '${moduleName}' imported in client code - verify this is intended`,
severity: "medium",
confidence: "medium",
category: "potentially-server-only",
recommendations: [
"Verify this library works in browser environment",
"Consider using browser-specific alternatives",
"Ensure no server-only features are being used",
"Test thoroughly in browser environment",
],
};
}
return null;
}
/**
* Check if a library contains critical/sensitive functionality
*/
isCriticalServerLibrary(moduleName) {
const criticalLibraries = [
"bcrypt",
"bcryptjs",
"argon2",
"scrypt",
"pbkdf2",
"jsonwebtoken",
"passport",
"express-session",
"mysql",
"postgres",
"mongodb",
"prisma",
"sequelize",
"stripe",
"paypal-rest-sdk",
"braintree",
"nodemailer",
"sendgrid",
"twilio",
"aws-sdk",
"@aws-sdk",
];
return criticalLibraries.some((lib) => moduleName === lib || moduleName.startsWith(`${lib}/`));
}
isTypeDefinitionFile(filePath) {
const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/");
return (
// Type definition files
normalizedPath.includes(".types.") ||
normalizedPath.endsWith("types.ts") ||
normalizedPath.endsWith("types.tsx") ||
normalizedPath.endsWith(".d.ts") ||
// Type directories
normalizedPath.includes("/types/") ||
normalizedPath.includes("/type/") ||
normalizedPath.includes("/@types/") ||
// Interface files
normalizedPath.includes(".interface.") ||
normalizedPath.includes("/interfaces/") ||
// Schema/model files (often contain type imports)
normalizedPath.includes(".schema.") ||
normalizedPath.includes("/schemas/") ||
normalizedPath.includes("/models/") ||
// Configuration type files
normalizedPath.includes("config.types.") ||
normalizedPath.includes("constants.types."));
}
/**
* 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.ServerOnlyImportsDetector = ServerOnlyImportsDetector;
ServerOnlyImportsDetector.SERVER_IMPORT_PATTERNS = [
{
id: "nodejs-builtin-import",
name: "Node.js built-in module import",
description: "Node.js built-in module imported in client-side code - this will fail at runtime",
pattern: {
type: "regex",
expression: /import.*['"](?:fs|path|os|crypto|buffer|stream|http|https|net|url|util|child_process|cluster|dgram|dns|events|readline|repl|tls|vm|zlib|worker_threads)['"]|require\(['"](?:fs|path|os|crypto|buffer|stream|http|https|net|url|util|child_process|cluster|dgram|dns|events|readline|repl|tls|vm|zlib|worker_threads)['"]\)/g,
},
vulnerabilityType: "server-only-import",
severity: "high",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
{
id: "database-import-client",
name: "Database library import in client code",
description: "Database library imported in client-side code - credentials may be exposed",
pattern: {
type: "regex",
expression: /import.*['"](?:mysql|postgres|pg|mongodb|mongoose|prisma|sequelize|typeorm|knex)['"]|require\(['"](?:mysql|postgres|pg|mongodb|mongoose|prisma|sequelize|typeorm|knex)['"]\)/g,
},
vulnerabilityType: "server-only-import",
severity: "critical",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx"],
enabled: true,
},
];
// Node.js built-in modules that should never be used in client code
ServerOnlyImportsDetector.NODEJS_BUILTINS = [
"fs",
"path",
"os",
"crypto",
"buffer",
"stream",
"http",
"https",
"net",
"url",
"util",
"child_process",
"cluster",
"dgram",
"dns",
"events",
"readline",
"repl",
"tls",
"vm",
"zlib",
"worker_threads",
"perf_hooks",
"inspector",
"async_hooks",
"trace_events",
"v8",
"domain",
"punycode",
"querystring",
"string_decoder",
"timers",
];
// Server-side libraries that expose sensitive functionality
ServerOnlyImportsDetector.SERVER_ONLY_LIBRARIES = [
// Databases
"mysql",
"mysql2",
"postgres",
"pg",
"mongodb",
"mongoose",
"prisma",
"sequelize",
"typeorm",
"knex",
"sqlite",
"sqlite3",
"redis",
"ioredis",
"cassandra-driver",
"neo4j-driver",
// Authentication server libraries
"bcrypt",
"bcryptjs",
"argon2",
"scrypt",
"pbkdf2",
"jsonwebtoken",
"passport",
"passport-local",
"passport-jwt",
"express-session",
// Server frameworks/middleware
"express",
"koa",
"fastify",
"hapi",
"restify",
"connect",
"cors",
"helmet",
"morgan",
"compression",
"cookie-parser",
// File/System operations
"multer",
"formidable",
"archiver",
"tar",
"yauzl",
"yazl",
"chokidar",
"nodemon",
"pm2",
"forever",
// Process/System utilities
"shelljs",
"execa",
"cross-spawn",
"which",
"find-up",
"graceful-fs",
"rimraf",
"mkdirp",
"glob",
"minimatch",
// Cryptography (server-side)
"node-forge",
"crypto-js",
"tweetnacl",
"sodium-native",
// Email/SMS
"nodemailer",
"sendgrid",
"mailgun",
"twilio",
// Payment processing
"stripe",
"paypal-rest-sdk",
"braintree",
// AWS/Cloud services (server SDKs)
"aws-sdk",
"@aws-sdk",
"google-cloud",
"azure",
// Development/Build tools
"webpack",
"rollup",
"vite",
"esbuild",
"babel",
"typescript",
"eslint",
"prettier",
"jest",
"mocha",
"chai",
"sinon",
];
// Libraries that might be server-only depending on usage
ServerOnlyImportsDetector.POTENTIALLY_SERVER_ONLY = [
"axios", // Can be client or server, but check context
"node-fetch", // Server-only fetch polyfill
"undici", // Server-side HTTP client
"got", // Server-side HTTP client
"superagent", // Can be both, but often server
"request", // Deprecated, but server-only
"dotenv", // Environment variables (server-only)
"config", // Configuration management
"winston",
"bunyan",
"pino", // Server-side logging
];