-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground-monolithic-backup.js
More file actions
3260 lines (2829 loc) · 120 KB
/
background-monolithic-backup.js
File metadata and controls
3260 lines (2829 loc) · 120 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* HERA - OAuth/OIDC/SAML Security Testing Extension
* Code Monkey Cybersecurity - "Cybersecurity. With humans."
*
* 🛡️ ACTIVE DETECTION LAYERS (Currently Operational)
*
* ✅ OAuth/SAML Flow Security - CSRF, PKCE, state parameter validation
* ✅ Certificate Analysis - HTTPS/TLS integrity, domain matching
* ✅ DNS Intelligence - Homograph attacks, DGA detection, geolocation
* ✅ Session Tracking - Cross-domain correlation, ecosystem detection
* ✅ Secret Scanning - Hardcoded credentials, JWT vulnerabilities
* ✅ Dark Pattern Detection - UI manipulation, deceptive practices
* ✅ Privacy Violation Detection - GDPR compliance, consent validation
*
* ✅ PhishZip Compression Analysis - INTEGRATED (Phase 1 Complete)
* Status: Core functionality operational, requires baseline training
* Integration fixes completed (13th Review):
* - ✅ P0-THIRTEENTH-1: pako.js loaded dynamically in compression analyzer
* - ✅ P0-THIRTEENTH-2: Analyzer instantiated and called in ANALYSIS_COMPLETE
* - ✅ P0-THIRTEENTH-3: manifest.json web_accessible_resources includes pako.js
* - ⚠️ P1-THIRTEENTH-1: No baseline data yet (requires real auth page training)
* - ✅ P1-THIRTEENTH-2: Integrated into message pipeline with async wrapper
*
* Next steps: Train baselines on Microsoft/Google/GitHub/Okta auth pages
* Full roadmap: docs/PHISHZIP-INTEGRATION-SUMMARY.md (Phase 1-5, 8+ weeks)
*
* Philosophy: HONEST, evidence-based, human-centric security
* - Document what actually works (not marketing claims)
* - Show users real findings with explanations
* - Respect user agency - inform, don't patronize
*
* 📊 SECURITY REVIEW STATUS
* - Reviews 10-12: Fixed 18 critical issues (9 P0 + 9 P1/P2)
* - Review 13: Fixed PhishZip P0 issues (pako loading, baseline validation, async handler)
* - Review 14: Fixed 8 issues (3 P0 XSS, 2 P1 validation, 2 P2 DoS, 1 P3 dead code)
* - Review 15: Fixed 3 issues (1 P0 false positive, 1 P1 UX, 1 P2 modal)
* - Review 16: Fixed 10 issues from production error logs
* ✅ P0-SIXTEENTH-1: Message authorization bug (INJECT_RESPONSE_INTERCEPTOR, getBackendScan unauthorized)
* ✅ P0-SIXTEENTH-2: Storage quota exhaustion (circuit breaker, pre-write quota checks, startup cleanup)
* ✅ P0-SIXTEENTH-3: pako.js initialization race (initializeHera() startup coordinator)
* ✅ P1-SIXTEENTH-1: CSP injection failures (downgraded to debug logs - expected behavior)
* ✅ P2-SIXTEENTH-1: Redundant truncation logging (log once per instance)
* ✅ P2-SIXTEENTH-3: Broken encryption imports removed (secure-storage.js deprecated)
* ✅ P3-SIXTEENTH-1: Dead code in manifest.json (empty web_accessible_resources removed)
* ✅ P3-SIXTEENTH-2: Documented debounce timing rationale (100ms vs 1000ms tradeoffs)
* - Review 17 (Oct 9): Fixed 4 critical production errors
* ✅ P0-SEVENTEENTH-1: getBackendScan authorization fixed (was in wrong listener whitelist)
* ✅ P0-SEVENTEENTH-2: Backend scanning disabled (CSP blocks fetch to arbitrary domains)
* ✅ P0-SEVENTEENTH-3: Circuit breaker memory leak (rejects writes + clears cache when open)
* ✅ P1-SEVENTEENTH-1: pako.js init error swallowed (now explicitly sets ready=false)
*/
// Core analysis engines
import { HeraAuthProtocolDetector } from './hera-auth-detector.js';
import { HeraSecretScanner } from './hera-secret-scanner.js';
import { HeraMaliciousExtensionDetector } from './hera-extension-security.js';
import { HeraAuthSecurityAnalyzer } from './hera-auth-security-analyzer.js';
import { HeraPortAuthAnalyzer } from './hera-port-auth-analyzer.js';
import { EvidenceCollector } from './evidence-collector.js';
import { AlertManager } from './alert-manager.js';
// Modular architecture components
import { SecurityValidation } from './modules/security-validation.js';
import { storageManager } from './modules/storage-manager.js';
import { memoryManager } from './modules/memory-manager.js';
import { sessionTracker } from './modules/session-tracker.js';
import { ipCacheManager } from './modules/ip-cache.js';
// DNS and IP intelligence module (Phase 1 modularization)
import { resolveIPAddresses, getIPGeolocation, gatherDNSIntelligence, detectSuspiciousDomainPatterns } from './modules/dns-intelligence.js';
// Pure utility modules (Phase 1 modularization)
import { detectHomographAttack, detectDGAPattern, calculateStringSimilarity, levenshteinDistance } from './modules/string-utils.js';
import { parseCookieHeader, analyzeSetCookie, isSessionCookie, isAuthCookie } from './modules/cookie-utils.js';
import { analyzeJWT } from './modules/jwt-utils.js';
import { detectAuthType } from './modules/auth-utils.js';
import { analyzeRequestHeaders, analyzeResponseHeaders } from './modules/header-utils.js';
import { analyzeUrl, hasSensitiveParameters, detectSuspiciousUrlPatterns, isCrossOrigin, isExtensionRequest, isThirdPartyRequest, isSensitivePath } from './modules/url-utils.js';
import { performAlgNoneProbe, performRepeaterRequest, sanitizeProbeHeaders } from './modules/security-probes.js';
// P0-THIRTEENTH-1 FIX: Load pako.js compression library first
// Service workers can't use importScripts in modules, so we load via script tag approach
// pako will be available globally after this loads
self.pako = null; // Will be set by loading pako
// PHISHZIP INTEGRATION: Compression-based phishing detection (PhishZip methodology from CSIRO Data61)
// Adds Layer 5 to multi-layer defense: visual clone detection via HTML compression analysis
import { HeraCompressionAnalyzer } from './modules/hera-compression-analyzer.js';
// P0-THIRTEENTH-2 FIX: Instantiate compression analyzer globally
const compressionAnalyzer = new HeraCompressionAnalyzer();
let compressionAnalyzerReady = false;
// Auth flow analysis module (Tier 2 domain logic)
import {
analyzeAuthFlow,
analyzeOAuthConsent,
detectAuthProvider,
analyzeScopeRisks,
analyzeRedirectUri,
generateConsentWarnings,
analyzeAuthFailure
} from './modules/auth-flow-analyzer.js';
// ARCHITECTURE FIX P0-1: Detectors moved to content-script.js
// Removed imports - detectors run in content script where document/window exist
// background.js (service worker) cannot access DOM APIs
// NOTE: Deleted duplicate SecurityValidation code (was 107 lines, exact copy of module)
// Now using the modular version from ./modules/security-validation.js
// NOTE: Removed unused SecureStorage encryption system (was broken - session key lost on service worker restart)
// If encryption is needed in the future, use:
// 1. Password-based key derivation (PBKDF2) with user password
// 2. OR store key in chrome.storage.session (MV3) - but data still lost on browser restart
// 3. OR accept that sensitive data should NOT be stored locally at all
// P2-NINTH-1 FIX: Whitelist of allowed script injection files
const ALLOWED_SCRIPTS = new Set([
'response-interceptor.js',
'content-script.js'
]);
// P3-NINTH-1 & P3-NINTH-2 FIX: Production mode detection and safe logging
const isProduction = !chrome.runtime.getManifest().version.includes('dev');
// TODO P3-TENTH-3: Sanitized errors lack context for user bug reports
// Removing stack traces helps security but makes debugging production issues very hard
// Should add error codes and timestamps to help users report issues. See TENTH-REVIEW-FINDINGS.md:2280
function sanitizeError(error) {
if (!error) return 'Unknown error';
if (isProduction) {
// In production, hide stack traces and file paths
return {
message: error.message,
type: error.name
// Omit stack trace in production
};
} else {
// Full details in development
return {
message: error.message,
stack: error.stack,
name: error.name
};
}
}
function sanitizeUrl(url) {
if (!url) return '';
try {
const urlObj = new URL(url);
return urlObj.hostname; // Only log hostname, not full URL with paths/params
} catch (e) {
return 'invalid-url';
}
}
// --- Global State ---
// MIGRATED TO MODULES: authRequests and debugTargets now managed by memoryManager
// Wrap Maps with auto-sync proxies for automatic persistence
// P1-1 FIX: Cache wrapper functions to prevent memory leak
// P0-ARCH-3 FIX: Proxy with initialization check to prevent race conditions
const authRequestsWrapperCache = new Map();
const authRequests = new Proxy(memoryManager.authRequests, {
get(target, prop) {
// P0-ARCH-3 FIX: Warn if accessed before initialization
if (!memoryManager.initialized) {
console.warn(`Hera RACE: authRequests.${String(prop)} accessed before initialization - data may be incomplete`);
}
const value = target[prop];
if (typeof value === 'function') {
// P1-1: Return cached wrapper if it exists
if (!authRequestsWrapperCache.has(prop)) {
authRequestsWrapperCache.set(prop, function(...args) {
const result = value.apply(target, args);
// Auto-sync after mutating operations
if (prop === 'set' || prop === 'delete' || prop === 'clear') {
memoryManager.syncWrite();
}
return result;
});
}
return authRequestsWrapperCache.get(prop);
}
return value;
}
});
// P1-1 FIX: Cache wrapper functions to prevent memory leak
// P0-ARCH-3 FIX: Proxy with initialization check to prevent race conditions
const debugTargetsWrapperCache = new Map();
const debugTargets = new Proxy(memoryManager.debugTargets, {
get(target, prop) {
// P0-ARCH-3 FIX: Warn if accessed before initialization
if (!memoryManager.initialized) {
console.warn(`Hera RACE: debugTargets.${String(prop)} accessed before initialization - data may be incomplete`);
}
const value = target[prop];
if (typeof value === 'function') {
// P1-1: Return cached wrapper if it exists
if (!debugTargetsWrapperCache.has(prop)) {
debugTargetsWrapperCache.set(prop, function(...args) {
const result = value.apply(target, args);
// Auto-sync after mutating operations
if (prop === 'set' || prop === 'delete' || prop === 'clear') {
memoryManager.syncWrite();
}
return result;
});
}
return debugTargetsWrapperCache.get(prop);
}
return value;
}
});
const version = "1.3";
// Memory leak prevention: Delegated to memoryManager module
function cleanupStaleRequests() {
memoryManager.cleanupStaleRequests();
}
// Storage quota monitoring: Delegated to storageManager module
async function checkStorageQuota() {
await storageManager.checkStorageQuota();
}
// Initialize components FIRST (before alarm listeners need them)
const evidenceCollector = new EvidenceCollector(); // Evidence-based vulnerability verification
const alertManager = new AlertManager(); // Tiered, confidence-based alerting
// CRITICAL FIX P0: Master initialization to prevent race conditions
let heraReady = false;
let initializationPromise = null;
async function initializeHera() {
if (heraReady) return;
console.log('Hera: Starting initialization...');
const startTime = Date.now();
try {
// Initialize all persistent storage modules in parallel
await Promise.all([
memoryManager.initPromise,
sessionTracker.initPromise,
evidenceCollector.initPromise,
alertManager.initPromise,
ipCacheManager.initPromise
]);
// P0-THIRTEENTH-2 FIX: Initialize compression analyzer with pako.js
try {
await compressionAnalyzer.initialize();
compressionAnalyzerReady = true;
console.log('Hera: Compression analyzer initialized (PhishZip enabled)');
} catch (error) {
console.warn('Hera: Compression analyzer initialization failed - PhishZip disabled:', error);
compressionAnalyzerReady = false;
}
heraReady = true;
const duration = Date.now() - startTime;
console.log(`Hera: All modules initialized in ${duration}ms`);
// CRITICAL FIX P0-4: Initialize webRequest listeners AFTER all modules ready
await initializeWebRequestListeners();
} catch (error) {
console.error('Hera: Initialization failed:', error);
// Mark as ready anyway to prevent permanent blocking
heraReady = true;
}
}
// Start initialization immediately
initializationPromise = initializeHera();
// Use chrome.alarms API (persists across service worker restarts)
chrome.alarms.create('cleanupAuthRequests', { periodInMinutes: 2 });
chrome.alarms.create('checkStorageQuota', { periodInMinutes: 10 });
chrome.alarms.onAlarm.addListener(async (alarm) => {
await initializationPromise; // Wait for init before cleanup
if (alarm.name === 'cleanupAuthRequests') {
cleanupStaleRequests();
alertManager.cleanupAlertHistory(); // Also cleanup alert deduplication history
evidenceCollector.cleanup(); // CRITICAL FIX: Prevent evidence cache memory leak
sessionTracker.cleanupOldSessions(); // CRITICAL FIX: Use alarms instead of setInterval
} else if (alarm.name === 'checkStorageQuota') {
checkStorageQuota();
} else if (alarm.name.startsWith('heraProbeConsent_')) {
// P1-TENTH-3 FIX: Handle unique alarm names with UUIDs
// P0-ARCH-2 FIX: Auto-revoke probe consent when alarm fires
const { probeConsentManager } = await import('./modules/probe-consent.js');
await probeConsentManager.revokeConsent();
console.log('Hera: Probe consent auto-revoked (24h expiry)');
} else if (alarm.name === 'heraPrivacyConsentExpiry') {
// P0-ARCH-2 FIX: Auto-revoke privacy consent when alarm fires
const { privacyConsentManager } = await import('./modules/privacy-consent.js');
await privacyConsentManager.withdrawConsent();
console.log('Hera: Privacy consent auto-revoked (expiry)');
}
});
// Then initialize other components that depend on it
const heraAuthDetector = new HeraAuthProtocolDetector(evidenceCollector);
const heraSecretScanner = new HeraSecretScanner();
const heraExtensionDetector = new HeraMaliciousExtensionDetector();
const heraAuthSecurityAnalyzer = new HeraAuthSecurityAnalyzer();
const heraPortAuthAnalyzer = new HeraPortAuthAnalyzer();
// CRITICAL FIX: Define ALL helper functions BEFORE event listeners are registered
// Chrome can fire onInstalled immediately during module load, so functions must exist first
// P0-NINTH-1 FIX: Mutex for debugger operations to prevent race conditions
const debuggerOperationLocks = new Map(); // tabId -> Promise
async function attachDebugger(tabId) {
if (tabId <= 0) return;
// P0-NINTH-1 FIX: Acquire lock to prevent concurrent attach attempts
if (debuggerOperationLocks.has(tabId)) {
console.log(`Hera: Debugger operation already in progress for tab ${tabId}, skipping`);
return; // Another attach is in progress
}
// Create lock promise
let releaseLock;
const lockPromise = new Promise(resolve => { releaseLock = resolve; });
debuggerOperationLocks.set(tabId, lockPromise);
try {
// P0-NINTH-1 FIX: Double-check under lock
if (debugTargets.has(tabId)) {
console.log(`Hera: Debugger already attached to tab ${tabId}`);
return;
}
const result = await chrome.storage.local.get(['enableResponseCapture']);
const enabled = result.enableResponseCapture === true;
if (!enabled) {
return;
}
const debuggee = { tabId: tabId };
// P0-NINTH-1 FIX: Promisify attach for proper async/await
const attachSuccess = await new Promise((resolve) => {
chrome.debugger.attach(debuggee, version, () => {
if (chrome.runtime.lastError) {
const error = chrome.runtime.lastError.message;
console.warn(`Hera: Failed to attach debugger to tab ${tabId}: ${error}`);
resolve(false);
} else {
resolve(true);
}
});
});
if (!attachSuccess) {
return; // Attach failed
}
// P0-NINTH-1 FIX: Only set in map AFTER successful attach
debugTargets.set(tabId, debuggee);
// Enable Network domain
const networkEnabled = await new Promise((resolve) => {
chrome.debugger.sendCommand(debuggee, "Network.enable", {}, () => {
if (chrome.runtime.lastError) {
console.warn(`Failed to enable Network for tab ${tabId}`);
resolve(false);
} else {
resolve(true);
}
});
});
if (!networkEnabled) {
// Cleanup on failure
await new Promise((resolve) => {
chrome.debugger.detach(debuggee, () => {
debugTargets.delete(tabId);
resolve();
});
});
}
} catch (error) {
console.error('Hera: debugger attach failed:', error);
debugTargets.delete(tabId); // Ensure cleanup
} finally {
// P0-NINTH-1 FIX: Always release lock
debuggerOperationLocks.delete(tabId);
releaseLock();
}
}
async function initializeDebugger() {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
if (tab.id && tab.url && !tab.url.startsWith('chrome://')) {
attachDebugger(tab.id);
}
}
}
async function updateBadge() {
return storageManager.updateBadge();
}
function showAuthSecurityAlert(finding, url) {
try {
// Enhance finding with URL
const enrichedFinding = {
...finding,
url: url,
evidence: finding.evidence || {}
};
// Use AlertManager for tiered, confidence-based alerting
alertManager.processFinding(enrichedFinding);
} catch (error) {
console.error('Failed to show auth security alert:', error);
}
}
async function handleInterceptorInjection(sender, message) {
try {
const tabId = sender.tab?.id;
let url = sender.tab?.url;
if (!tabId) {
return { success: false, error: 'No tab ID available' };
}
// P1-TENTH-2 FIX: Get latest tab URL to prevent race condition
const tab = await chrome.tabs.get(tabId);
url = tab.url; // Use current URL, not cached sender.tab.url
// P1-TENTH-2 FIX: Enhanced URL validation
if (!url || url.startsWith('chrome://') || url.startsWith('about:') ||
url.startsWith('chrome-extension://') || url.startsWith('edge://') ||
url.startsWith('chrome-devtools://') || url.startsWith('view-source:')) {
console.log(`Hera: Skipping interceptor injection on restricted page: ${url}`);
return { success: false, error: 'Cannot inject on restricted pages' };
}
// P1-TENTH-2 FIX: Validate URL is HTTP/HTTPS only
if (!url.startsWith('http://') && !url.startsWith('https://')) {
console.log(`Hera: Skipping injection on non-HTTP(S) page: ${url}`);
return { success: false, error: 'Only HTTP(S) pages supported' };
}
// Check if chrome.scripting API is available
if (!chrome.scripting || !chrome.scripting.executeScript) {
console.error('Hera: chrome.scripting API not available - host permissions may not be granted');
return { success: false, error: 'Scripting permission not available' };
}
// Check if we have permission for this URL
const hasPermission = await chrome.permissions.contains({
origins: [new URL(url).origin + '/*']
});
if (!hasPermission) {
console.warn(`Hera: No permission for ${url} - host permissions not granted`);
return { success: false, error: 'Host permission not granted for this site' };
}
// P0-NINTH-3 FIX: Double-check permission right before injection to narrow race window
const hasPermissionNow = await chrome.permissions.contains({
origins: [new URL(url).origin + '/*']
});
if (!hasPermissionNow) {
console.warn('Hera: Permission revoked between check and injection');
return { success: false, error: 'Permission no longer available' };
}
// P2-NINTH-1 FIX: Validate script path against whitelist
const scriptFile = 'response-interceptor.js'; // Hardcoded for now
if (!ALLOWED_SCRIPTS.has(scriptFile)) {
console.error(`Hera: Attempted to inject non-whitelisted script: ${scriptFile}`);
return { success: false, error: 'Invalid script path' };
}
// P1-TENTH-2 FIX: THIRD check right before injection
const latestTab = await chrome.tabs.get(tabId);
if (latestTab.url !== url) {
console.warn(`Hera SECURITY: Tab URL changed during injection (TOCTOU attempt blocked)`);
console.warn(` Original: ${url}`);
console.warn(` Current: ${latestTab.url}`);
return { success: false, error: 'Tab URL changed during injection (security block)' };
}
// P0-NINTH-3 FIX: Wrap injection in try-catch to handle permission revocation
try {
const result = await chrome.scripting.executeScript({
target: { tabId: tabId },
world: 'ISOLATED',
files: [scriptFile]
});
// Check for Chrome runtime errors (permission revoked during injection)
if (chrome.runtime.lastError) {
console.error('Hera: Injection failed (permission revoked?):', chrome.runtime.lastError);
return { success: false, error: chrome.runtime.lastError.message };
}
console.log(`Hera: Response interceptor injected in isolated world for tab ${tabId}`);
return { success: true };
} catch (injectionError) {
// P0-NINTH-3 FIX: Catch errors from permission revocation mid-flight
if (injectionError.message?.includes('permission') ||
injectionError.message?.includes('Cannot access')) {
console.warn('Hera: Injection blocked - permission revoked mid-flight');
return { success: false, error: 'Permission revoked during injection' };
}
throw injectionError; // Re-throw unexpected errors
}
} catch (error) {
// P3-NINTH-1 FIX: Sanitize error messages to avoid leaking file paths
console.error('Hera: Failed to inject response interceptor:', sanitizeError(error));
return { success: false, error: error.message || 'Unknown error' };
}
}
// --- Event Listeners (registered AFTER all function definitions) ---
// CONSOLIDATED onInstalled listener (was duplicated 3x - lines 67, 422, 1455)
chrome.runtime.onInstalled.addListener(async (details) => {
console.log(`Hera ${details.reason}:`, details);
// On first install only
if (details.reason === 'install') {
try {
// SECURITY FIX: Clear any leftover data from previous installation
// Prevents privacy leak if extension was previously installed
await chrome.storage.local.clear();
console.log('Hera: Cleared previous installation data');
// 1. Set default configuration
await chrome.storage.local.set({
heraConfig: {
syncEndpoint: null,
riskThreshold: 50,
enableRealTimeAlerts: true,
autoExportEnabled: true,
autoExportThreshold: 950
}
});
console.log('Hera: Default configuration set');
// SECURITY FIX: Don't auto-request permissions on install (aggressive UX)
// Instead, show welcome screen and let user enable monitoring via popup
// This follows Chrome extension best practices for permission requests
console.log('Hera: Installation complete. Open popup to enable monitoring.');
} catch (error) {
console.error('Hera: Error during installation:', error);
}
}
// On install or update, initialize extension
initializeDebugger();
updateBadge();
});
// Add missing wrapper methods for HeraAuthProtocolDetector
heraAuthDetector.isAuthRequest = function(url, options) {
// Simple auth endpoint detection
const authPatterns = [
'/oauth', '/authorize', '/token', '/login', '/signin', '/auth',
'/api/auth', '/session', '/connect', '/saml', '/oidc', '/scim'
];
const urlLower = url.toLowerCase();
return authPatterns.some(pattern => urlLower.includes(pattern));
};
heraAuthDetector.analyze = function(url, method, headers, body) {
return this.analyzeRequest({
url: url,
method: method,
headers: headers,
body: body
});
};
// --- Storage Helper (MIGRATED TO storageManager module) ---
const heraStore = {
async storeAuthEvent(eventData) {
return storageManager.storeAuthEvent(eventData);
},
async storeSession(sessionData) {
return storageManager.storeSession(sessionData);
}
};
// --- Utility Functions ---
function decodeRequestBody(requestBody) {
if (!requestBody || !requestBody.raw) return null;
try {
const decoder = new TextDecoder('utf-8');
const decodedParts = requestBody.raw.map(part => {
if (part.bytes) {
const byteValues = Object.values(part.bytes);
return decoder.decode(new Uint8Array(byteValues));
}
return '';
});
return decodedParts.join('');
} catch (e) {
console.error('Hera: Failed to decode request body:', e);
return '[Hera: Failed to decode body]';
}
}
// CRITICAL FIX: Removed duplicate updateBadge and showAuthSecurityAlert (already defined at top of file)
// Show security alert for malicious extension detection (using AlertManager)
function showExtensionSecurityAlert(finding) {
try {
// Extension threats are always CRITICAL with high confidence
const enrichedFinding = {
...finding,
severity: 'CRITICAL',
url: 'chrome://extensions/',
evidence: {
verification: finding.details?.extensionId ? `chrome://extensions/?id=${finding.details.extensionId}` : null
}
};
// Use AlertManager for tiered alerting
alertManager.processFinding(enrichedFinding);
} catch (error) {
console.error('Failed to show extension security alert:', error);
}
}
// --- Main Logic ---
// Initialize webRequest listeners only if permission granted
async function initializeWebRequestListeners() {
const hasPermission = await chrome.permissions.contains({
permissions: ['webRequest'],
origins: ['https://*/*', 'http://localhost/*']
});
if (!hasPermission) {
console.warn('Hera: webRequest permission not granted - request monitoring disabled');
console.warn('Hera: Grant permission in extension settings to enable full functionality');
return false;
}
console.log('Hera: webRequest permissions granted, initializing listeners...');
// 1. Listen for requests
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
// CRITICAL FIX P0: Wait for initialization before processing
if (!heraReady) {
console.warn('Hera: Not ready, skipping request:', details.url);
return;
}
const isAuthRelated = heraAuthDetector.isAuthRequest(details.url, {});
if (isAuthRelated) {
// SECURITY FIX P2: Generate nonce for request/response matching
const requestNonce = crypto.randomUUID();
authRequests.set(details.requestId, {
id: details.requestId,
url: details.url,
method: details.method,
type: details.type,
tabId: details.tabId,
timestamp: new Date().toISOString(),
requestBody: decodeRequestBody(details.requestBody),
nonce: requestNonce, // For matching with intercepted response
// Placeholders for data from other listeners
requestHeaders: [],
responseHeaders: [],
statusCode: null,
responseBody: null,
metadata: {},
});
}
},
{ urls: ["<all_urls>"] },
["requestBody"]
);
// 2. Capture request headers
chrome.webRequest.onBeforeSendHeaders.addListener(
(details) => {
if (!heraReady) return; // Wait for init
const requestData = authRequests.get(details.requestId);
if (requestData) {
requestData.requestHeaders = details.requestHeaders;
// Perform analysis now that we have headers
const authAnalysis = heraAuthDetector.analyze(details.url, details.method, details.requestHeaders, requestData.requestBody);
requestData.authType = authAnalysis.protocol;
// Ensure metadata exists
if (!requestData.metadata) {
requestData.metadata = {};
}
requestData.metadata.authAnalysis = authAnalysis;
requestData.metadata.authAnalysis.riskCategory = heraAuthDetector.getRiskCategory(authAnalysis.riskScore);
// Update port analysis with headers
requestData.metadata.authTypeAnalysis = heraPortAuthAnalyzer.detectAuthType({
url: details.url,
method: details.method,
requestHeaders: details.requestHeaders,
requestBody: requestData.requestBody
});
// Check for default credentials
requestData.metadata.credentialAnalysis = heraPortAuthAnalyzer.checkDefaultCredentials({
url: details.url,
requestHeaders: details.requestHeaders,
requestBody: requestData.requestBody
});
}
},
{ urls: ["<all_urls>"] },
["requestHeaders"]
);
// 3. Capture response headers and status code
chrome.webRequest.onHeadersReceived.addListener(
(details) => {
if (!heraReady) return; // Wait for init
const requestData = authRequests.get(details.requestId);
if (requestData) {
requestData.statusCode = details.statusCode;
requestData.responseHeaders = details.responseHeaders;
}
},
{ urls: ["<all_urls>"] },
["responseHeaders"]
);
// --- Debugger and Final Save Logic ---
// CRITICAL FIX: Moved attachDebugger and initializeDebugger to top of file (before onInstalled)
chrome.tabs.onCreated.addListener((tab) => tab.id && attachDebugger(tab.id));
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.status === 'loading') {
attachDebugger(tabId);
}
});
chrome.tabs.onRemoved.addListener((tabId) => {
if (debugTargets.has(tabId)) {
const debuggee = debugTargets.get(tabId);
chrome.debugger.detach(debuggee, () => {
// Log error but don't block cleanup
if (chrome.runtime.lastError) {
console.log(`Debugger auto-detached for closed tab ${tabId}: ${chrome.runtime.lastError.message}`);
} else {
console.log(`Successfully detached debugger from closed tab ${tabId}`);
}
});
// P1-NINTH-1 FIX: Delete immediately, don't wait for callback
// The tab is already closed, so debugger is detached by Chrome anyway
debugTargets.delete(tabId);
}
});
// P1-NINTH-1 FIX: Periodic cleanup of stale debugger entries (defense in depth)
setInterval(async () => {
const allTabs = await chrome.tabs.query({});
const validTabIds = new Set(allTabs.map(tab => tab.id));
for (const [tabId, debuggee] of debugTargets.entries()) {
if (!validTabIds.has(tabId)) {
console.warn(`Hera: Removing stale debugger entry for closed tab ${tabId}`);
debugTargets.delete(tabId);
}
}
}, 60000); // Clean up every minute
// Listen for debugger events
chrome.debugger.onEvent.addListener((source, method, params) => {
// First, get the response details when they are received
if (method === "Network.responseReceived") {
const requestData = authRequests.get(params.requestId);
if (requestData) {
requestData.responseDetails = params.response; // Store for later
}
}
// When the request is finished, get the body and save everything
if (method === "Network.loadingFinished") {
const requestData = authRequests.get(params.requestId);
if (requestData && requestData.responseDetails) {
// P0-TENTH-1 FIX: Validate source tabId matches request tabId
if (source.tabId !== requestData.tabId) {
console.error('Hera SECURITY: debugger event tabId mismatch');
return;
}
// P0-TENTH-1 FIX: Validate request still exists in debugTargets
if (!debugTargets.has(source.tabId)) {
console.error('Hera SECURITY: debugger event from non-tracked tab');
return;
}
// P0-TENTH-1 FIX: Validate requestId format (Chrome uses UUID-like format)
if (!params.requestId || typeof params.requestId !== 'string') {
console.error('Hera SECURITY: invalid requestId format');
return;
}
const debuggee = { tabId: source.tabId };
chrome.debugger.sendCommand(
debuggee,
"Network.getResponseBody",
{ requestId: params.requestId },
(response) => {
if (!chrome.runtime.lastError && response) {
let body = response.body;
if (response.base64Encoded) {
try {
body = atob(response.body);
} catch (e) {
console.warn("Hera: Failed to decode base64 response body.", e);
body = "[Hera: Failed to decode base64 body]";
}
}
// P0-TENTH-1 FIX: Sanitize response body before storage
// Check for potentially malicious content that could execute in popup context
if (typeof body === 'string') {
if (/<script|onerror=|onclick=|onload=|javascript:/i.test(body)) {
console.warn('Hera SECURITY: Response contains potentially malicious content, sanitizing');
// Don't block entirely, but mark as suspicious
requestData.securityFlags = requestData.securityFlags || [];
requestData.securityFlags.push('SUSPICIOUS_CONTENT_IN_RESPONSE');
}
}
requestData.responseBody = body;
requestData.captureSource = 'debugger'; // Mark the source
// Ensure metadata structure exists
if (!requestData.metadata) {
requestData.metadata = {};
}
if (!requestData.metadata.authAnalysis) {
requestData.metadata.authAnalysis = {
issues: [],
riskScore: 0,
riskCategory: 'low'
};
}
// If the content is JavaScript, scan it for secrets
const contentType = requestData.responseDetails?.headers['content-type'] || '';
if (contentType.includes('javascript') || contentType.includes('application/x-javascript')) {
const secretFindings = heraSecretScanner.scan(body, requestData.url);
if (secretFindings.length > 0) {
if (!requestData.metadata.authAnalysis.issues) {
requestData.metadata.authAnalysis.issues = [];
}
requestData.metadata.authAnalysis.issues.push(...secretFindings);
}
}
// Analyze the response body for security issues
const responseBodyIssues = heraAuthDetector.analyzeResponseBody(body);
if (responseBodyIssues.length > 0) {
if (!requestData.metadata.authAnalysis.issues) {
requestData.metadata.authAnalysis.issues = [];
}
requestData.metadata.authAnalysis.issues.push(...responseBodyIssues);
// Recalculate risk score
requestData.metadata.authAnalysis.riskScore = heraAuthDetector.calculateRiskScore(requestData.metadata.authAnalysis.issues);
requestData.metadata.authAnalysis.riskCategory = heraAuthDetector.getRiskCategory(requestData.metadata.authAnalysis.riskScore);
}
}
// --- FINAL SAVE POINT ---
// Now that we have all data, save the complete request object.
chrome.storage.local.get({ heraSessions: [] }, (result) => {
let sessions = result.heraSessions;
// DOS prevention: Limit total sessions
const MAX_SESSIONS = 1000;
if (sessions.length >= MAX_SESSIONS) {
console.warn(`Session limit reached (${MAX_SESSIONS}), removing oldest`);
sessions.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
sessions = sessions.slice(0, MAX_SESSIONS - 1);
}
// DOS prevention: Limit individual request size
const MAX_REQUEST_SIZE = 100 * 1024; // 100KB per request
const requestSize = JSON.stringify(requestData).length;
if (requestSize > MAX_REQUEST_SIZE) {
console.warn(`Request too large (${requestSize} bytes), truncating response body`);
if (requestData.responseBody) {
requestData.responseBody = requestData.responseBody.substring(0, 10000) + '... [truncated]';
}
}
sessions.push(requestData);
chrome.storage.local.set({ heraSessions: sessions }, () => {
updateBadge();
authRequests.delete(params.requestId); // Clean up from memory
});
});
}
);
}
}
});
// --- Extension Lifecycle ---
// NOTE: onInstalled consolidated at line 67 (was duplicated here)
chrome.runtime.onStartup.addListener(() => {
console.log('Hera starting up...');
initializeDebugger();
updateBadge();
});
// CRITICAL FIX P0-NEW: onSuspend handler REMOVED
// Reason: Chrome MV3 onSuspend does NOT wait for async operations
// All _syncToStorage() methods are async (use await chrome.storage.*.set())
// Service worker terminates before writes complete → data loss
// Solution: Aggressive debouncing (reduced timeout from 100-200ms to 1000ms)
// See: https://developer.chrome.com/docs/extensions/mv3/service_workers/events/#suspend
// Original broken code (kept for reference):
// chrome.runtime.onSuspend.addListener(() => {
// memoryManager._syncToStorage(); // Returns Promise, not awaited!
// // Service worker dies here → chrome.storage.*.set() abandoned
// });
// CRITICAL FIX: Handle devtools port connections (P1)
// Devtools panel connects via chrome.runtime.connect() and sends messages via port.postMessage()
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'devtools-page') {
console.log('Hera: DevTools panel connected');
// Handle messages from devtools panel
port.onMessage.addListener(async (message) => {
console.log('Hera: DevTools message received:', message.type);
if (message.type === 'INIT_DEVTOOLS') {
// Send all existing requests to devtools panel
chrome.storage.local.get({ heraSessions: [] }, (result) => {
result.heraSessions.forEach(session => {
port.postMessage({
type: 'AUTH_REQUEST',
data: session
});
});
});
} else if (message.type === 'SET_RECORDING_STATE') {
// Store recording state in session storage
await chrome.storage.session.set({ heraRecording: message.isRecording });
console.log(`Hera: Recording ${message.isRecording ? 'enabled' : 'paused'}`);
} else if (message.type === 'CLEAR_REQUESTS') {
// CRITICAL FIX P1: Route through storageManager
await storageManager.clearAllSessions();
await memoryManager.clearAuthRequests();
console.log('Hera: All requests cleared');
}
});
// Handle disconnect
port.onDisconnect.addListener(() => {
console.log('Hera: DevTools panel disconnected');
});
}
});
// Consolidated message listener (removed duplicate listener at line 4014)
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Sender validation: Reject external messages (security)
if (!sender.id || sender.id !== chrome.runtime.id) {