UNPKG

chrome-extension-test-framework

Version:

Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies

442 lines (362 loc) 20.3 kB
/** * SecurityTestSuite - セキュリティ関連の検証テストスイート */ const TestSuite = require('../lib/TestSuite'); const fs = require('fs'); const path = require('path'); const SecurityAnalyzer = require('../lib/SecurityAnalyzer'); const StorageAnalyzer = require('../lib/StorageAnalyzer'); const ContextAwareDetector = require('../lib/ContextAwareDetector'); class SecurityTestSuite extends TestSuite { constructor(config) { super({ name: 'Security Validation', description: 'Chrome拡張機能のセキュリティ要件を検証', config: config }); this.config = config; this.setupTests(); } setupTests() { // CSP(Content Security Policy)検証 this.test('Content Security Policy validation', async (config) => { const manifestPath = path.join(config.extensionPath, 'manifest.json'); const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); if (manifest.content_security_policy) { const cspString = JSON.stringify(manifest.content_security_policy); // unsafe-evalの使用チェック if (cspString.includes('unsafe-eval')) { throw new Error('CSP contains unsafe-eval directive'); } // unsafe-inlineの使用チェック if (cspString.includes('unsafe-inline')) { console.warn(' ⚠️ CSP contains unsafe-inline directive'); } // httpsの強制 if (cspString.includes('http:') && !cspString.includes('http://localhost')) { throw new Error('CSP should enforce HTTPS for external resources'); } } }); // 外部スクリプトの検証 this.test('No external script loading', async (config) => { const htmlFiles = await this.findFiles(config.extensionPath, '.html'); for (const htmlFile of htmlFiles) { const content = fs.readFileSync(htmlFile, 'utf8'); // 外部スクリプトタグのチェック const externalScriptRegex = /<script[^>]+src=["']https?:\/\/(?!localhost)/gi; const matches = content.match(externalScriptRegex); if (matches) { throw new Error(`External scripts found in ${path.basename(htmlFile)}`); } } }); // インラインスクリプトの検証 this.test('No inline scripts in HTML', async (config) => { const htmlFiles = await this.findFiles(config.extensionPath, '.html'); for (const htmlFile of htmlFiles) { const content = fs.readFileSync(htmlFile, 'utf8'); // インラインスクリプトのチェック const inlineScriptRegex = /<script[^>]*>[\s\S]*?<\/script>/gi; const scriptTags = content.match(inlineScriptRegex) || []; const inlineScripts = scriptTags.filter(tag => !tag.includes('src=')); if (inlineScripts.length > 0) { console.warn(` ⚠️ Inline scripts found in ${path.basename(htmlFile)}`); } // onclickなどのインラインイベントハンドラ const inlineHandlerRegex = /\son\w+\s*=/gi; if (inlineHandlerRegex.test(content)) { throw new Error(`Inline event handlers found in ${path.basename(htmlFile)}`); } } }); // eval使用の検証 this.test('No eval() usage', async (config) => { const jsFiles = await this.findFiles(config.extensionPath, '.js'); for (const jsFile of jsFiles) { const content = fs.readFileSync(jsFile, 'utf8'); // eval()の使用チェック const evalRegex = /\beval\s*\(/g; if (evalRegex.test(content)) { throw new Error(`eval() usage found in ${path.basename(jsFile)}`); } // Function()コンストラクタの使用チェック const functionRegex = /new\s+Function\s*\(/g; if (functionRegex.test(content)) { throw new Error(`Function() constructor found in ${path.basename(jsFile)}`); } } }); // innerHTMLの安全な使用(コンテキストを考慮) this.test('Safe innerHTML usage', async (config) => { const jsFiles = await this.findFiles(config.extensionPath, '.js'); const detector = new ContextAwareDetector(); const allIssues = []; for (const jsFile of jsFiles) { const content = fs.readFileSync(jsFile, 'utf8'); const fileName = path.basename(jsFile); // innerHTMLの検出 const innerHTMLIssues = detector.detectUnsafeInnerHTML(content, jsFile); if (innerHTMLIssues.length > 0) { innerHTMLIssues.forEach(issue => { if (issue.severity === 'high') { allIssues.push(`${fileName}:${issue.line} - ${issue.message}`); } else { console.warn(` ⚠️ ${fileName}:${issue.line} - ${issue.message}`); if (issue.suggestion) { console.warn(` 💡 ${issue.suggestion}`); } } }); } } if (allIssues.length > 0) { throw new Error(`High-risk innerHTML usage detected:\n ${allIssues.join('\n ')}`); } }); // HTTPSの使用確認 this.test('HTTPS enforcement', async (config) => { const files = await this.findFiles(config.extensionPath, ['.js', '.html', '.json']); for (const file of files) { // 拡張機能のディレクトリ内のファイルのみを対象にする if (!file.startsWith(config.extensionPath)) { continue; } const content = fs.readFileSync(file, 'utf8'); // HTTPのURLを検出(localhostを除く) const httpRegex = /http:\/\/(?!localhost|127\.0\.0\.1)/gi; const matches = content.match(httpRegex); if (matches) { // 相対パスで表示 const relativePath = path.relative(config.extensionPath, file); throw new Error(`Insecure HTTP URLs found in ${relativePath}`); } } }); // パーミッションの最小権限原則 this.test('Least privilege permissions', async (config) => { const manifestPath = path.join(config.extensionPath, 'manifest.json'); const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); // 過度に広範なパーミッション const broadPermissions = [ '<all_urls>', 'http://*/*', 'https://*/*', '*://*/*' ]; const permissions = [ ...(manifest.permissions || []), ...(manifest.host_permissions || []) ]; const foundBroad = permissions.filter(p => broadPermissions.includes(p)); if (foundBroad.length > 0) { console.warn(` ⚠️ Overly broad permissions: ${foundBroad.join(', ')}`); } // 危険なAPIパーミッション const dangerousAPIs = [ 'debugger', 'management', 'proxy', 'webRequest', 'webRequestBlocking' ]; const foundDangerous = permissions.filter(p => dangerousAPIs.includes(p)); if (foundDangerous.length > 0) { console.warn(` ⚠️ Powerful API permissions: ${foundDangerous.join(', ')}`); } }); // ストレージの安全な使用 this.test('Secure storage usage', async (config) => { const jsFiles = await this.findFiles(config.extensionPath, '.js'); for (const jsFile of jsFiles) { const content = fs.readFileSync(jsFile, 'utf8'); // localStorage使用の警告(chrome.storage推奨) if (/localStorage\./g.test(content)) { console.warn(` ⚠️ localStorage usage in ${path.basename(jsFile)} - consider using chrome.storage API`); } // 機密情報の可能性があるキーワード const sensitiveKeywords = [ /password/i, /secret/i, /token/i, /api[_-]?key/i, /private[_-]?key/i ]; sensitiveKeywords.forEach(keyword => { if (keyword.test(content)) { console.warn(` ⚠️ Potential sensitive data handling in ${path.basename(jsFile)}`); } }); } }); // XSSの潜在的リスク this.test('XSS vulnerability check', async (config) => { const jsFiles = await this.findFiles(config.extensionPath, '.js'); for (const jsFile of jsFiles) { const content = fs.readFileSync(jsFile, 'utf8'); // documentWriteの使用 if (/document\.write/g.test(content)) { throw new Error(`document.write usage found in ${path.basename(jsFile)}`); } // insertAdjacentHTMLの使用 if (/\.insertAdjacentHTML/g.test(content)) { console.warn(` ⚠️ insertAdjacentHTML usage in ${path.basename(jsFile)} - ensure proper sanitization`); } } }); // メッセージパッシングのセキュリティ this.test('Message passing security', async (config) => { const jsFiles = await this.findFiles(config.extensionPath, '.js'); for (const jsFile of jsFiles) { const content = fs.readFileSync(jsFile, 'utf8'); // メッセージリスナーの検証 if (/chrome\.runtime\.onMessage(External)?\.addListener/g.test(content)) { // 送信元の検証が行われているか if (!/sender\.(id|url|origin)/g.test(content)) { console.warn(` ⚠️ Message listener in ${path.basename(jsFile)} should verify sender`); } } // postMessageの使用 if (/window\.postMessage/g.test(content)) { console.warn(` ⚠️ window.postMessage usage in ${path.basename(jsFile)} - ensure origin validation`); } } }); // 高度なセキュリティ分析 this.test('Advanced security analysis', async (config) => { const analyzer = new SecurityAnalyzer(config); const results = await analyzer.analyze(config.extensionPath, this.excludeManager); const report = analyzer.generateReport(); // クリティカルな問題がある場合はエラー if (report.summary.critical > 0) { const criticalIssues = report.issuesBySeverity.critical .map(issue => `${issue.file}:${issue.line} - ${issue.message}`) .join('\n '); throw new Error(`Critical security issues found:\n ${criticalIssues}`); } // 高リスクの問題がある場合は警告 if (report.summary.high > 0) { console.warn(` ⚠️ ${report.summary.high} high-risk security issues found`); report.issuesBySeverity.high.forEach(issue => { console.warn(` - ${issue.file}:${issue.line} - ${issue.type}`); }); } // 中リスクの問題 if (report.summary.medium > 0) { console.warn(` ⚠️ ${report.summary.medium} medium-risk security issues found`); } console.log(` 📊 Security scan complete: ${report.summary.scannedFiles} files analyzed`); }); // APIキーとシークレットの検出 this.test('No hardcoded secrets', async (config) => { const analyzer = new SecurityAnalyzer(); const results = await analyzer.analyze(config.extensionPath); const secretIssues = results.issues.filter(issue => issue.type.includes('API Key') || issue.type.includes('Private Key') || issue.type.includes('Secret') || issue.type.includes('Password') || issue.type.includes('Token') ); if (secretIssues.length > 0) { const secrets = secretIssues .map(issue => `${issue.file}:${issue.line} - ${issue.type}`) .join('\n '); throw new Error(`Hardcoded secrets detected:\n ${secrets}`); } }); // 安全なストレージの使用 this.test('Secure data storage', async (config) => { const jsFiles = await this.findFiles(config.extensionPath, '.js'); const issues = []; for (const jsFile of jsFiles) { const content = fs.readFileSync(jsFile, 'utf8'); const fileName = path.basename(jsFile); // コンテキストを考慮したストレージ分析 const detector = new ContextAwareDetector(); // localStorageの使用を検出 const localStorageIssues = detector.detectLocalStorageUsage(content, jsFile); // localStorage関連の問題をフィルタリング const storageIssues = localStorageIssues.filter(issue => issue.type === 'localStorage' && (issue.severity === 'high' || issue.severity === 'medium') ); storageIssues.forEach(issue => { if (issue.severity === 'high') { issues.push(`${fileName}:${issue.line} - ${issue.message}`); } else if (issue.severity === 'medium' || issue.severity === 'low') { // 低・中レベルの問題は警告として表示 if (!this.config.quiet) { console.warn(` ⚠️ ${fileName}:${issue.line} - ${issue.message}`); } } }); // 暗号化チェックは維持(ただし誤検知を減らす) const hasStorage = /localStorage|sessionStorage/g.test(content); const hasEncryption = /encrypt|crypto|cipher|hash/gi.test(content); const hasSensitivePattern = /password|token|key|secret|credential/gi.test(content); if (hasStorage && hasSensitivePattern && !hasEncryption) { // ストレージと機密データパターンの両方が存在し、暗号化がない場合のみ警告 // コンテキストベースの検出により、コメントや文字列リテラル内の検出は既に除外されている console.warn(` ⚠️ ${fileName} may store sensitive data without encryption`); } } if (issues.length > 0) { throw new Error(`Insecure storage detected:\n ${issues.join('\n ')}`); } }); // Chrome Storage APIの使用パターン検証 this.test('Chrome storage API usage patterns', async (config) => { const analyzer = new StorageAnalyzer(); const results = await analyzer.analyze(config.extensionPath); // 結果の表示 if (results.summary.deprecatedStorageUsage > 0) { console.warn(` ⚠️ Deprecated storage APIs detected: ${results.summary.deprecatedStorageUsage} occurrences`); // localStorage使用の詳細 if (results.usage.localStorage.length > 0) { console.warn(` 📦 localStorage usage in ${results.usage.localStorage.length} files`); results.usage.localStorage.forEach(item => { console.warn(` - ${item.file}: ${item.occurrences} occurrences`); }); } // sessionStorage使用の詳細 if (results.usage.sessionStorage.length > 0) { console.warn(` 📦 sessionStorage usage in ${results.usage.sessionStorage.length} files`); results.usage.sessionStorage.forEach(item => { console.warn(` - ${item.file}: ${item.occurrences} occurrences`); }); } } // chrome.storage使用状況 if (results.summary.chromeStorageUsage > 0) { console.log(` ✅ chrome.storage API usage: ${results.summary.chromeStorageUsage} calls`); } // 重大な問題がある場合はエラー const criticalIssues = results.issues.filter(issue => issue.severity === 'error'); if (criticalIssues.length > 0) { throw new Error(`Critical storage issues found:\n ${criticalIssues.map(i => i.message).join('\n ')}`); } // 推奨事項の表示 if (results.summary.recommendations.length > 0) { results.summary.recommendations.forEach(rec => { if (rec.priority === 'high') { console.warn(` 💡 ${rec.message}`); } }); } }); } /** * ファイルを検索 */ async findFiles(dir, extensions) { // getAllFilesメソッドを使用してExcludeManagerを適用 const allFiles = await this.getAllFiles(); const exts = Array.isArray(extensions) ? extensions : [extensions]; // 拡張子でフィルタリング return allFiles .map(relativePath => path.join(this.config.extensionPath, relativePath)) .filter(fullPath => exts.some(ext => fullPath.endsWith(ext))); } } module.exports = SecurityTestSuite;