UNPKG

@sun-asterisk/sunlint

Version:

☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

407 lines (338 loc) 12.8 kB
/** * AST-based analyzer for S005 - No Origin Header Authentication * Detects usage of Origin header for authentication/access control through AST analysis */ const babel = require('@babel/parser'); const traverse = require('@babel/traverse').default; class S005ASTAnalyzer { constructor() { this.ruleId = 'S005'; this.ruleName = 'No Origin Header Authentication'; this.description = 'Do not use Origin header for authentication or access control'; } async analyze(files, language, options = {}) { const violations = []; for (const filePath of files) { try { const content = require('fs').readFileSync(filePath, 'utf8'); const fileViolations = await this.analyzeFile(content, filePath, options); violations.push(...fileViolations); } catch (error) { if (options.verbose) { console.warn(`⚠️ S005 AST analysis failed for ${filePath}: ${error.message}`); } } } return violations; } async analyzeFile(content, filePath, options = {}) { const violations = []; try { // Parse with TypeScript/JavaScript support const ast = babel.parse(content, { sourceType: 'module', allowImportExportEverywhere: true, allowReturnOutsideFunction: true, plugins: [ 'typescript', 'jsx', 'objectRestSpread', 'functionBind', 'exportDefaultFrom', 'decorators-legacy', 'classProperties', 'asyncGenerators', 'dynamicImport' ] }); // Traverse AST to find Origin header usage in authentication contexts traverse(ast, { MemberExpression: (path) => { this.checkOriginHeaderAccess(path, violations, filePath); }, CallExpression: (path) => { this.checkOriginHeaderMethods(path, violations, filePath); }, IfStatement: (path) => { this.checkConditionalOriginAuth(path, violations, filePath); }, AssignmentExpression: (path) => { this.checkOriginAssignment(path, violations, filePath); } }); } catch (parseError) { if (options.verbose) { console.warn(`⚠️ S005 parse failed for ${filePath}: ${parseError.message}`); } // Fall back to regex analysis if AST parsing fails return this.analyzeWithRegex(content, filePath, options); } return violations; } checkOriginHeaderAccess(path, violations, filePath) { const node = path.node; // Check for req.headers.origin, headers.origin, req.headers['origin'] if (this.isOriginHeaderAccess(node)) { // Check if this is in an authentication context if (this.isInAuthenticationContext(path)) { violations.push({ ruleId: this.ruleId, severity: 'error', message: 'Origin header should not be used for authentication. Origin can be spoofed and is not secure for access control.', line: node.loc ? node.loc.start.line : 1, column: node.loc ? node.loc.start.column + 1 : 1, filePath: filePath, type: 'origin_header_access' }); } } } checkOriginHeaderMethods(path, violations, filePath) { const node = path.node; // Check for req.get('origin'), req.header('origin'), getHeader('origin') if (this.isOriginHeaderMethod(node)) { if (this.isInAuthenticationContext(path)) { violations.push({ ruleId: this.ruleId, severity: 'error', message: 'Origin header retrieval methods should not be used for authentication purposes.', line: node.loc ? node.loc.start.line : 1, column: node.loc ? node.loc.start.column + 1 : 1, filePath: filePath, type: 'origin_header_method' }); } } // Check for CORS configuration with origin-based auth if (this.isCORSOriginAuth(node)) { violations.push({ ruleId: this.ruleId, severity: 'warning', message: 'CORS origin configuration should not replace proper authentication mechanisms.', line: node.loc ? node.loc.start.line : 1, column: node.loc ? node.loc.start.column + 1 : 1, filePath: filePath, type: 'cors_origin_auth' }); } } checkConditionalOriginAuth(path, violations, filePath) { const node = path.node; // Check if condition involves origin header and authentication if (this.hasOriginInCondition(node.test) && this.hasAuthInBlock(node.consequent)) { violations.push({ ruleId: this.ruleId, severity: 'error', message: 'Conditional authentication based on Origin header is insecure. Use proper authentication tokens.', line: node.loc ? node.loc.start.line : 1, column: node.loc ? node.loc.start.column + 1 : 1, filePath: filePath, type: 'conditional_origin_auth' }); } } checkOriginAssignment(path, violations, filePath) { const node = path.node; // Check for assignments involving origin header in auth context if (this.isOriginRelatedAssignment(node) && this.isInAuthenticationContext(path)) { violations.push({ ruleId: this.ruleId, severity: 'warning', message: 'Origin header values should not be assigned for authentication purposes.', line: node.loc ? node.loc.start.line : 1, column: node.loc ? node.loc.start.column + 1 : 1, filePath: filePath, type: 'origin_assignment_auth' }); } } isOriginHeaderAccess(node) { // req.headers.origin if (node.type === 'MemberExpression' && node.object && node.object.type === 'MemberExpression' && node.object.property && node.object.property.name === 'headers' && node.property && (node.property.name === 'origin' || (node.property.type === 'StringLiteral' && node.property.value === 'origin'))) { return true; } // headers.origin or headers['origin'] if (node.type === 'MemberExpression' && node.object && node.object.name === 'headers' && node.property && (node.property.name === 'origin' || (node.property.type === 'StringLiteral' && node.property.value === 'origin'))) { return true; } return false; } isOriginHeaderMethod(node) { if (node.type !== 'CallExpression' || !node.callee) return false; const callee = node.callee; // req.get('origin'), req.header('origin') if (callee.type === 'MemberExpression' && callee.property && (callee.property.name === 'get' || callee.property.name === 'header') && node.arguments && node.arguments.length > 0 && node.arguments[0].type === 'StringLiteral' && node.arguments[0].value.toLowerCase() === 'origin') { return true; } // getHeader('origin') if (callee.type === 'Identifier' && callee.name === 'getHeader' && node.arguments && node.arguments.length > 0 && node.arguments[0].type === 'StringLiteral' && node.arguments[0].value.toLowerCase() === 'origin') { return true; } return false; } isCORSOriginAuth(node) { if (node.type !== 'CallExpression' || !node.callee) return false; // Check for CORS configuration calls const callee = node.callee; if (callee.type === 'Identifier' && callee.name === 'cors' || (callee.type === 'MemberExpression' && callee.property && callee.property.name === 'cors')) { // Check if arguments contain auth-related configuration if (node.arguments && node.arguments.length > 0) { const config = node.arguments[0]; if (config.type === 'ObjectExpression') { return config.properties.some(prop => this.isPropertyWithAuthKeyword(prop) && this.hasOriginReference(prop) ); } } } return false; } hasOriginInCondition(testNode) { if (!testNode) return false; // Recursively check for origin references in condition if (testNode.type === 'MemberExpression') { return this.isOriginHeaderAccess(testNode); } if (testNode.type === 'CallExpression') { return this.isOriginHeaderMethod(testNode); } if (testNode.type === 'BinaryExpression') { return this.hasOriginInCondition(testNode.left) || this.hasOriginInCondition(testNode.right); } if (testNode.type === 'LogicalExpression') { return this.hasOriginInCondition(testNode.left) || this.hasOriginInCondition(testNode.right); } return false; } hasAuthInBlock(blockNode) { if (!blockNode) return false; const authKeywords = ['auth', 'login', 'token', 'session', 'user', 'permission', 'access']; // Simple check for auth-related identifiers in the block let hasAuth = false; try { traverse(blockNode, { Identifier: (path) => { if (path.node && path.node.name && authKeywords.some(keyword => path.node.name.toLowerCase().includes(keyword))) { hasAuth = true; path.stop(); } }, StringLiteral: (path) => { if (path.node && path.node.value && authKeywords.some(keyword => path.node.value.toLowerCase().includes(keyword))) { hasAuth = true; path.stop(); } } }, this); } catch (error) { // Ignore traverse errors, return false return false; } return hasAuth; } isInAuthenticationContext(path) { // Check parent nodes for authentication context let currentPath = path; let depth = 0; const maxDepth = 10; while (currentPath && depth < maxDepth) { const node = currentPath.node; // Check function names if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { if (this.hasAuthInName(node.id?.name)) { return true; } } // Check variable declarations if (node.type === 'VariableDeclarator' && this.hasAuthInName(node.id?.name)) { return true; } // Check object property names if (node.type === 'ObjectProperty' && this.hasAuthInName(node.key?.name || node.key?.value)) { return true; } currentPath = currentPath.parent; depth++; } return false; } hasAuthInName(name) { if (!name) return false; const authKeywords = [ 'auth', 'login', 'logout', 'authenticate', 'authorize', 'permission', 'access', 'token', 'session', 'user', 'verify', 'validate', 'check', 'guard', 'protect', 'middleware', 'passport', 'jwt', 'bearer' ]; const lowerName = name.toLowerCase(); return authKeywords.some(keyword => lowerName.includes(keyword)); } isOriginRelatedAssignment(node) { if (node.type !== 'AssignmentExpression') return false; // Check if right side involves origin header return this.hasOriginReference(node.right); } hasOriginReference(node) { if (!node) return false; if (node.type === 'MemberExpression') { return this.isOriginHeaderAccess(node); } if (node.type === 'CallExpression') { return this.isOriginHeaderMethod(node); } if (node.type === 'Identifier' && node.name.toLowerCase().includes('origin')) { return true; } if (node.type === 'StringLiteral' && node.value.toLowerCase().includes('origin')) { return true; } return false; } isPropertyWithAuthKeyword(prop) { if (!prop || prop.type !== 'ObjectProperty') return false; const key = prop.key?.name || prop.key?.value; if (!key) return false; return this.hasAuthInName(key); } // Fallback regex analysis analyzeWithRegex(content, filePath, options = {}) { const violations = []; const lines = content.split('\n'); lines.forEach((line, index) => { const lineNumber = index + 1; // Basic regex check for origin header in auth context const originAuthPattern = /(?:req\.headers\.origin|req\.get\s*\(\s*['"`]origin['"`]\s*\)).*(?:auth|login|token|permission)/i; if (originAuthPattern.test(line)) { violations.push({ ruleId: this.ruleId, severity: 'error', message: 'Origin header should not be used for authentication (detected via regex fallback).', line: lineNumber, column: line.search(/origin/i) + 1, filePath: filePath, type: 'origin_auth_regex' }); } }); return violations; } } module.exports = S005ASTAnalyzer;