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
JavaScript
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