UNPKG

page-integrity-js

Version:

A library for monitoring and controlling DOM mutations and script execution, essential for PCI DSS compliance and security audits

413 lines (398 loc) 17.8 kB
var PageIntegrity = (function (exports) { 'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; // Malicious behavior patterns to check for const MALICIOUS_PATTERNS = { // Evasion techniques evasion: [ // Attempting to bypass CSP /document\.write\s*\(\s*['"]<iframe[^>]*src\s*=\s*['"]javascript:/i, /document\.location\s*=\s*['"]javascript:/i, // Trying to hide script execution /(?:setTimeout|setInterval)\s*\(\s*['"][^'"]*['"]/i, // Attempting to bypass same-origin policy /document\.domain\s*=\s*['"][^'"]*['"]/i, // Trying to disable security features /Object\.defineProperty\s*\(\s*window\s*,\s*['"]onerror['"]/i, ], // Covert execution patterns covertExecution: [ // Hidden iframe with malicious intent /document\.write\s*\(\s*['"]<iframe[^>]*style\s*=\s*['"]display\s*:\s*none[^>]*src\s*=\s*['"](?:javascript|data|vbscript):/i, // Stealthy script injection /document\.write\s*\(\s*['"]<script[^>]*src\s*=\s*['"](?:javascript|data|vbscript):/i, // Attempting to execute code in a hidden context /new\s+Worker\s*\(\s*['"]data:application\/javascript;base64/i, // Trying to execute code in a way that avoids detection /Function\s*\(\s*['"]return\s+eval\s*\(/i, // Direct eval usage /eval\s*\(\s*['"][^'"]*['"]\s*\)/i, // Function constructor usage /new\s+Function\s*\(\s*['"][^'"]*['"]\s*\)/i, ], // Security bypass attempts securityBypass: [ // Attempting to modify security headers /Object\.defineProperty\s*\(\s*document\s*,\s*['"]cookie['"]/i, // Trying to bypass XSS filters /String\.fromCharCode\s*\(\s*\d+\s*\)\s*\.\s*replace\s*\(\s*['"]\s*['"]\s*,\s*['"]\s*['"]/i, // Attempting to disable security features /Object\.defineProperty\s*\(\s*navigator\s*,\s*['"]userAgent['"]/i, // Trying to bypass same-origin policy /document\.domain\s*=\s*['"]\*['"]/i, // Modifying window properties /Object\.defineProperty\s*\(\s*window\s*,\s*['"]alert['"]/i, /delete\s+window\.alert/i, /window\.alert\s*=\s*function/i, ], // Malicious intent indicators maliciousIntent: [ // Attempting to steal sensitive data /document\.cookie\s*\+\s*['"](?:\s*&\s*|%26)?(?:key|token|auth|password|secret)=\s*\+\s*encodeURIComponent/i, // Trying to inject malicious code /document\.write\s*\(\s*['"]<script[^>]*>\s*eval\s*\(/i, // Attempting to modify security settings /Object\.defineProperty\s*\(\s*window\s*,\s*['"]localStorage['"]/i, // Trying to bypass security controls /document\.createElement\s*\(\s*['"]script['"]\s*\)\s*\.\s*setAttribute\s*\(\s*['"]crossorigin['"]/i, // Data exfiltration /fetch\s*\(\s*['"][^'"]*malicious[^'"]*['"]/i, /navigator\.sendBeacon\s*\(\s*['"][^'"]*malicious[^'"]*['"]/i, ] }; const DEFAULT_ANALYSIS_CONFIG = { minScore: 3, maxThreats: 2, checkSuspiciousStrings: true, weights: { evasion: 3, covertExecution: 3, securityBypass: 2, maliciousIntent: 2 }, scoringRules: { minSafeScore: 3, maxThreats: 2, suspiciousStringWeight: 1 } }; function analyzeScript(content, config = DEFAULT_ANALYSIS_CONFIG) { const threats = []; const details = []; let score = 0; // Check each category of patterns for (const [category, patterns] of Object.entries(MALICIOUS_PATTERNS)) { for (const pattern of patterns) { const matches = content.match(pattern); if (matches) { threats.push(category); details.push({ pattern: pattern.toString(), matches: matches }); // Weight different categories switch (category) { case 'evasion': score += 3; // Highest weight for evasion attempts break; case 'covertExecution': score += 3; // Highest weight for covert execution break; case 'securityBypass': score += 2; // Medium weight for security bypass attempts break; case 'maliciousIntent': score += 2; // Medium weight for malicious intent break; } } } } // Check for suspicious combinations if (threats.includes('evasion') && (threats.includes('covertExecution') || threats.includes('securityBypass'))) { score += 2; // Multiple evasion techniques indicate malicious intent } // Check for suspicious string patterns const suspiciousStrings = config.checkSuspiciousStrings ? detectSuspiciousStrings(content) : []; if (suspiciousStrings.length > 0) { threats.push('suspicious-strings'); score += suspiciousStrings.length; } return { threats, score, details, analysisDetails: { suspiciousStrings, categories: [...new Set(threats)] } }; } function detectSuspiciousStrings(content) { const suspicious = []; // Known malicious patterns const maliciousPatterns = [ /(?:bypass|evade|disable|override)\s*(?:security|protection|filter|policy)/i, /\.(?:php|asp|jsp|exe|dll|bat|cmd|sh|bash)(?:\?|$)/i, /(?:sql|nosql|command|shell|exec|system)\.(?:injection|attack)/i, /(?:hide|conceal|mask|obscure)\s*(?:execution|code|script|behavior)/i, ]; for (const pattern of maliciousPatterns) { const matches = content.match(pattern); if (matches) { suspicious.push(`suspicious-pattern:${pattern.toString()}`); } } return suspicious; } function createHash(text) { const encoder = new TextEncoder(); const data = encoder.encode(text); let hash = 0; for (let i = 0; i < data.length; i++) { hash = ((hash << 5) - hash) + data[i]; hash = hash & hash; } return hash.toString(16); // Convert to hexadecimal } function checkCachedResponse(cacheManager, url, content) { return __awaiter(this, void 0, void 0, function* () { const hash = createHash(content); const cached = yield cacheManager.getCachedResponse(hash); if (cached && cached.analysis) { const score = typeof cached.analysis.score === 'number' ? cached.analysis.score : 0; const threats = Array.isArray(cached.analysis.threats) ? cached.analysis.threats : []; const isMalicious = score >= 3 || threats.length >= 2; return { blocked: isMalicious, reason: cached.reason || 'Cached block', analysis: cached.analysis }; } return { blocked: false }; }); } class ScriptBlocker { constructor(cacheManager, config) { this.cacheManager = cacheManager; this.blockedScripts = new Map(); this.config = config; } shouldBlockScript(url, content) { return __awaiter(this, void 0, void 0, function* () { var _a; // Check if script is blacklisted const isBlacklisted = (_a = this.config.blacklistedHosts) === null || _a === void 0 ? void 0 : _a.some(host => url.includes(host)); if (isBlacklisted) { this.blockedScripts.set(url, { url, reason: 'Blacklisted script' }); return { blocked: true, reason: 'Blacklisted script' }; } // Check cache first const cached = yield checkCachedResponse(this.cacheManager, url, content); if (cached.blocked) { this.blockedScripts.set(url, { url, reason: cached.reason || 'Cached block', analysis: cached.analysis }); return cached; } // Analyze script for monitoring const analysis = analyzeScript(content); this.blockedScripts.set(url, { url, reason: 'Script analyzed', analysis }); return { blocked: false, analysis }; }); } isScriptBlocked(url) { return this.blockedScripts.has(url); } getBlockedScript(url) { return this.blockedScripts.get(url); } getAllBlockedScripts() { return Array.from(this.blockedScripts.values()); } clearBlockedScripts() { this.blockedScripts.clear(); } getBlockedScriptsCount() { return this.blockedScripts.size; } } const CACHE_NAME = 'response-cache'; const MAX_CACHE_SIZE = 2500; class CacheManager { constructor(cacheName = CACHE_NAME, maxSize = MAX_CACHE_SIZE) { this.cacheName = cacheName; this.maxSize = maxSize; } cacheResponse(hash, url, analysis) { return __awaiter(this, void 0, void 0, function* () { const cache = yield caches.open(this.cacheName); const responseData = analysis ? { url, analysis } : { url }; yield cache.put(hash, new Response(JSON.stringify(responseData))); // Implement LRU by limiting cache size const keys = yield cache.keys(); if (keys.length > this.maxSize) { yield cache.delete(keys[0]); } }); } getCachedResponse(hash) { return __awaiter(this, void 0, void 0, function* () { const cache = yield caches.open(this.cacheName); const response = yield cache.match(hash); if (response) { const text = yield response.text(); return JSON.parse(text); } return null; }); } clearCache() { return __awaiter(this, void 0, void 0, function* () { yield caches.delete(this.cacheName); }); } } /** * Page Integrity JS * A library for ensuring webpage content integrity by verifying that content updates * come from first-party JavaScript. * * @packageDocumentation */ function mergeConfig(defaults, config) { const mergedConfig = Object.assign(Object.assign({}, defaults), config); // Deep merge analysis config if provided if (config.analysisConfig) { mergedConfig.analysisConfig = Object.assign(Object.assign(Object.assign({}, DEFAULT_ANALYSIS_CONFIG), config.analysisConfig), { weights: Object.assign(Object.assign({}, DEFAULT_ANALYSIS_CONFIG.weights), config.analysisConfig.weights), scoringRules: Object.assign(Object.assign({}, DEFAULT_ANALYSIS_CONFIG.scoringRules), config.analysisConfig.scoringRules) }); } else { mergedConfig.analysisConfig = DEFAULT_ANALYSIS_CONFIG; } return mergedConfig; } function initScriptBlocker(config, cacheManager) { return new ScriptBlocker(cacheManager, config); } function exposeGlobally(cls, name) { if (typeof window !== 'undefined') { window[name] = cls; } } /** * Main class for monitoring and enforcing page integrity. * * Example usage: * ```js * const pi = new PageIntegrity({ * blacklistedHosts: ['evil.com'], * whitelistedHosts: ['trusted.com'], * onBlocked: (info) => { ... } * }); * ``` */ class PageIntegrity { /** * Create a new PageIntegrity instance. * @param config Configuration options for script and DOM mutation monitoring. */ constructor(config) { this.config = mergeConfig({ allowDynamicInline: true }, config); this.cacheManager = new CacheManager(); this.scriptBlocker = initScriptBlocker(this.config, this.cacheManager); exposeGlobally(PageIntegrity, 'PageIntegrity'); } /** * Update the configuration for script and DOM mutation monitoring. * @param newConfig Partial configuration to merge with the current config. */ updateConfig(newConfig) { this.config = mergeConfig(this.config, newConfig); this.scriptBlocker = initScriptBlocker(this.config, this.cacheManager); } handleScript(script, scriptInfo) { var _a, _b; // Check if script is blacklisted const isBlacklisted = (_a = this.config.blacklistedHosts) === null || _a === void 0 ? void 0 : _a.some(host => { const scriptUrl = script.src || ''; return scriptUrl.includes(host); }); if (isBlacklisted) { if (this.config.onBlocked) { this.config.onBlocked({ type: 'blacklisted', target: script, stackTrace: new Error().stack || '', context: { source: scriptInfo.source, origin: scriptInfo.origin } }); } return false; } // Perform analysis for monitoring purposes const content = script.textContent || ''; const analysis = analyzeScript(content, this.config.analysisConfig); // Report analysis results if score is below threshold if (analysis.score < (((_b = this.config.analysisConfig) === null || _b === void 0 ? void 0 : _b.minScore) || DEFAULT_ANALYSIS_CONFIG.minScore)) { if (this.config.onBlocked) { this.config.onBlocked({ type: 'low-score', target: script, stackTrace: new Error().stack || '', context: { source: scriptInfo.source, origin: scriptInfo.origin, score: analysis.score, analysisDetails: { staticScore: analysis.score, dynamicScore: 0, originScore: 0, hashScore: 0 } } }); } } return true; } } exports.PageIntegrity = PageIntegrity; exports.exposeGlobally = exposeGlobally; exports.initScriptBlocker = initScriptBlocker; exports.mergeConfig = mergeConfig; return exports; })({}); //# sourceMappingURL=page-integrity.js.map