How I Built a Browser Extension Malware Scanner β And Used It to Expose a Malicious "YouTube Downloader"
Introduction: The Trust Problem With Browser Extensions
Browser extensions are powerful. They can modify web pages, intercept network requests, access your browsing history, monitor every tab you have open, and even read your clipboard. Users install them by the millions, often with minimal scrutiny beyond a star rating and a brief description.
The reality is darker. Malicious browser extensions have been used to steal passwords, hijack affiliate links, inject ads, harvest cookies, and silently track users across every website they visit. They're often distributed through official stores under innocent-sounding names β "productivity boosters", "video downloaders", "coupon finders".
As a developer focused on security, I built **extension-scanner.py** β an open-source static analysis tool that systematically tears apart Firefox .xpi extension archives and flags every indicator of compromise it finds. The tool is available at :
https://www.github.com/ernos/browser-xpi-malware-scanner.
In this post I'll explain how the scanner works, then walk through a hands-on real-world case: a Firefox extension called "YouTube Video Download" that presents itself as a simple video downloader β but which the scanner grades as CRITICAL RISK, with 30 critical findings, 133 high severity findings, and 117 medium severity findings.
Every finding I describe below is backed by real code from the extension. I'll show you the actual lines.
What Is extension-scanner.py?
extension-scanner.py is a Python command-line tool for static analysis of Firefox XPI extensions (and Chrome CRX format). It requires no browser, no sandbox, and no network connectivity to do its job. You point it at an .xpi file, and it produces a detailed, severity-ranked report.
Architecture at a Glance
The scanner opens the XPI (which is simply a ZIP archive) and runs a battery of independent check modules over every file inside it:
| Check Module | What It Detects |
|---|---|
check_manifest |
Dangerous permissions, unsafe CSP, wildcard host access |
check_javascript |
Obfuscation, suspicious external URLs, high-entropy strings |
check_base64 |
Encoded payloads, embedded executables, encrypted blobs |
check_png_appended |
Hidden data appended after a PNG's IEND chunk |
check_png_chunks |
Non-standard PNG chunks used for steganography |
check_api_abuse |
Cookie harvesting, keyloggers, tab surveillance, clipboard theft |
check_hidden_element |
Invisible iframes, tracking pixels injected into pages |
check_time_bomb |
Delayed activation, install-date checks, random probability gates |
check_anti_analysis |
Webdriver detection, zero-size viewport checks, debugger traps |
check_comments_and_leakage |
Hardcoded credentials, API keys, private IPs |
Each check produces zero or more Finding objects with a severity (CRITICAL, HIGH, MEDIUM, LOW, INFO), a category label, the source file and line number, and a short evidence snippet.
Running the Scanner
python3 extension-scanner.py "Youtube Video Downloader.xpi" --min-severity MEDIUM -v
The -v flag enables verbose evidence output; --min-severity MEDIUM suppresses noise. The exit code is non-zero when the overall risk grade is HIGH or above β making it easy to integrate into CI pipelines.
The Target: "YouTube Video Download" (UUID: {8ba275e2-6750-41c3-a944-307e38c2a5e2})
The extension presents itself as a video downloader for YouTube, Facebook, Vimeo, DailyMotion, TikTok, and a dozen other platforms. Version 2.8.0. It ships with a polished popup UI, options page, and per-site content scripts. Everything looks professional.
The scanner's verdict:
Overall verdict: CRITICAL RISK
Findings: 30 CRITICAL 133 HIGH 117 MEDIUM
Let's dig into the most damning evidence category by category.
β οΈ Important Disclaimer: High and Critical Findings Are Not a Verdict
Before diving in, this needs to be said clearly: a high finding count does not prove an extension is malicious.
extension-scanner.py is a triage and prioritization tool, not a judge. Every pattern it flags has a legitimate use somewhere:
keydownlisteners power every keyboard shortcut feature ever writtentabs.query({})is used by legitimate tab managers and session savers- Minified JavaScript produces high Shannon entropy on perfectly innocent code
localStorageflags are standard for feature toggles and onboarding flows- Iframes are used by millions of embedding widgets across the web
What the scanner does is surface patterns that are statistically associated with harm and warrant close human review. A single CRITICAL finding might be a false positive. Even a dozen HIGH findings on a well-known extension from a reputable developer might be explainable.
The scanner's job is to answer: "Which of these 500 extensions should I spend a manual hour on first?" β not "Is this extension malicious?"
In this particular case, I did perform that manual follow-up. The combination of signals β outlined below β leads me to conclude the behaviors are intentional and undisclosed. But I encourage you to read the evidence yourself and form your own judgement. The extracted source files in this repo are exactly as shipped in the XPI; nothing has been altered.
What Does This Extension Actually Do?
Stripped of the marketing copy, here is what the scanner's findings β combined with manual code review β indicate this extension does on an installed browser:
Claimed purpose: Download videos from YouTube, Facebook, Vimeo, DailyMotion, TikTok, and similar platforms.
What it actually does in addition to that:
| Behavior | Where | Disclosed to user? |
|---|---|---|
| Registers keyboard listeners on Facebook and Odnoklassniki pages | facebook_com.js, odnoklassniki_ru.js |
No |
| Enumerates every open tab and its URL, continuously | background.js |
No |
POSTs telemetry data to monitoring-exporter.sf-helper.com |
background.js |
No |
Tracks user sessions with Google Analytics IDs (G-94HR5L4844, UA-67738130-2) |
background.js |
No |
Injects invisible iframes pointing to televzr://bridgeInit on every visited page |
commons.js |
No |
Injects full-screen advertising overlays for astrologyvalley.com in English/Hindi browsers |
astrologyvalley.js, astrology.js, horoscope.js |
No |
Watches all browser downloads via chrome.downloads.onChanged |
background.js |
No |
| Waits 12 hours before activating certain tracking to evade review sandboxes | background.js |
No |
Stores televzr_installed flag to gate behavior after first install |
commons.js |
No |
| Hides executable JavaScript code inside a PNG image file | img/view.png |
No |
None of these behaviors are described in the extension's store listing or permissions prompt. A user who installed this extension to download YouTube videos would have no way of knowing any of this was happening.
Finding 1: Excessive and Dangerous Permissions
The very first red flag is in manifest.json:
"permissions": [
"tabs",
"downloads",
"storage",
"<all_urls>",
"webRequest",
"webNavigation",
"webRequestBlocking"
]
This combination grants the extension:
**<all_urls>**β read and modify content on every website you visit**tabs**β enumerate all open tabs and their URLs**webRequest**+**webRequestBlocking**β intercept, inspect, and block any HTTP request your browser makes**downloads**β monitor and initiate file downloads**webNavigation**β track every page navigation event
No legitimate video downloader needs webRequestBlocking. That permission is what ad-blockers use to intercept and cancel network requests. Here it is available to inject link modifications or strip referrer headers before they leave your browser.
The content security policy is equally alarming:
script-src 'self' 'unsafe-eval'; object-src 'self';
'unsafe-eval' explicitly permits the extension to use eval() β the canonical JavaScript execution sink. Any string of code can be dynamically executed at runtime. This is an immediate red flag for obfuscation and dynamic payload delivery.
Finding 2: Keyloggers on Facebook and Odnoklassniki
The scanner flagged CRITICAL β API_EXFIL_COMBO β on includes/facebook_com.js. Let's look at exactly what that file does.
The keylogger registration (line 727):
// includes/facebook_com.js β line 727
onShow: function() {
w.isMutation || document.addEventListener("keydown", o);
},
onHide: function() {
w.isMutation || document.removeEventListener("keydown", o);
}
The extension registers a keydown event listener on the Facebook DOM. The same pattern appears in includes/odnoklassniki_ru.js at line 1439. Keyboard event listeners can capture every key a user presses β including passwords typed into Facebook login forms.
The exfiltration partner β network call in the same file (line 305):
// includes/facebook_com.js β line 305
i.href = "http://savefrom.net/?url=" + encodeURIComponent(e),
The scanner's API_EXFIL_COMBO check fires whenever a file contains both a data-collection pattern (keyboard listener, tab enumeration, cookie harvesting) and an outbound network call (fetch, XMLHttpRequest, sendBeacon, .postMessage). Finding both in the same file raises the severity to CRITICAL because the combination is the canonical signature of a steal-and-send attack.
Finding 3: Bulk Tab URL Surveillance
js/background.js enumerates every single open tab in the browser:
// js/background.js β line 4845
chrome.tabs.query({}, (function(r) {
r.forEach((function(e) {
t.push(e.url);
})), e(t);
}));
The empty object {} passed to chrome.tabs.query means no filter at all β it returns every tab across every window, including private/incognito tabs. This code runs in the background script, which is always active. URLs of visited pages are pushed into an array and then processed by the function checkUrlsOfOpenTabs.
The scanner found this pattern twice in background.js (lines 4845 and 4853), each time with an outbound network call present in the same file, producing two CRITICAL API_EXFIL_COMBO findings.
Finding 4: Continuous Data Transmission to a Remote Server
Deep inside js/background.js, there's a function called sendAlternativeMonitoring (line 8742):
// js/background.js β line 8742
Xn.sendAlternativeMonitoring = function(t) {
if (!!Xn.preferences.dataCollectionEnabled) {
var r = t.params, n = r.type, o = e(r, Qn);
fetch(`https://monitoring-exporter.sf-helper.com/api/v3/${n}`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(o)
}).catch((e => {
console.error(e);
}));
}
};
This function POSTs telemetry data to monitoring-exporter.sf-helper.com β a domain that is not YouTube, not a CDN, and not any recognizable video download service. The endpoint path (/api/v3/${n}) is dynamic, meaning the type of data sent is determined at runtime. Notice the guard: if (!!Xn.preferences.dataCollectionEnabled) β this is a preference flag, not a user-facing setting, meaning users have no meaningful way to opt out.
The tracking is also tied to a Google Analytics tracking IDs: G-94HR5L4844 and UA-67738130-2 β appearing in the same background script, building a profile that links the extension user's browser fingerprint and navigation data to a GA account controlled by the extension author.
Finding 5: A Time-Bomb in the Storage
The scanner flagged a HIGH / TIME_BOMB at includes/commons.js:549:
// includes/commons.js β line 549
localStorage.getItem("televzr_installed") || h || (f(!0), C(!0), c(ae));
And at line 541:
localStorage.setItem("televzr_installed", "1")
This is the install-date persistence pattern β a classic technique for evading automated review. The extension stores a flag in localStorage on first run. Subsequent behaviour is conditionally gated on this flag. Automated scanners and extension store reviewers typically run extensions for short periods; by suppressing certain functionality until the flag is set, the extension can appear benign during review and "arm" itself only after it has been installed on a real user's machine.
The scanner also caught a 12-hour timeout (setTimeout("trackTimeout", 43200)) at js/background.js:8697 β the extension deliberately waits 12 hours before certain tracking operations, another anti-analysis delay.
Finding 6: Invisible Iframes Injected Into Every Page
The scanner found three HIGH / HIDDEN_ELEMENT findings in includes/commons.js (lines 376, 6037, 11559):
// includes/commons.js β line 376 (simplified context)
var t = document.createElement("iframe");
return t.src = N, document.body.appendChild(t), ...
Where N = "televzr://bridgeInit" β a custom URL scheme that communicates with another installed application ("Televzr"). This iframe is programmatically inserted into the DOM of every page the user visits (the <all_urls> permission enables this). The iframe has no visible presentation to the user.
This technique is used for affiliate fraud (loading a hidden shopping page to claim referral commissions), cross-origin communication, and triggering local application protocols without user interaction.
Finding 7: JavaScript Obfuscation β Hiding Intent at Scale
The scanner found 21 CRITICAL and 48 HIGH JS obfuscation findings across the codebase. Let's look at one concrete example from includes/astrologyvalley.js:
// includes/astrologyvalley.js β lines 7β13
var n = [ "en", "hi" ];
if ((navigator.languages || [ navigator.language ])
.some((e => n.includes(e.slice(0, 2).toLowerCase())))) {
// ... inject a large modal popup
}
This file β named after an astrology website β is bundled inside a video downloader extension. It renders a full-screen overlay modal targeting only users with English or Hindi browser language settings, asking them to take a "2-minute survey" linked to astrologyvalley.com. This is not a feature of a video downloader. It is covert advertising injection.
Inside this file, the obfuscation check also triggered on a new Function("return this")() pattern β a classic eval-bypass used to obtain the global window object even when eval is nominally restricted.
Finding 8: Data Hidden Inside a PNG File (Steganography)
This is the most technically sophisticated finding. The scanner detected a CRITICAL / PNG_APPENDED in img/view.png.
The check_png_appended module works by finding the IEND chunk β the mandatory terminator of every valid PNG file. Any bytes that appear after IEND are invisible to image viewers and browsers, but they can carry arbitrary payloads.
In this extension, the bytes after the IEND of view.png contain a custom marker followed by base64-encoded JavaScript:
__begin_view__
ZnVuY3Rpb24gc3RhcnRBZGRyKGEpe2lmKGEpe2lmKCJtZXNz...
Decoding that base64 reveals:
function startAddr(a){if(a){if("message"==typeof a){
var b=a;
a=new URL(a);
}console.log(self.location.origin);
console.log(a.href);}}}
This is JavaScript code β hidden inside a PNG image file β that processes URL messages and logs origin and href data. The __begin_view__ marker strongly suggests that other code in the extension reads this file at runtime, extracts the payload after the marker, and executes it. This is a deliberate steganographic code-loading mechanism designed to bypass security tools that only scan .js files.
The scanner also found the PNG chunk parser (check_png_chunks) independently flagged this at HIGH / PNG_CHUNK β detecting the non-standard _view__ chunk name used as the embedded marker.
Finding 9: What Is "AstrologyValley" Doing in a Video Downloader?
The presence of includes/astrologyvalley.js, includes/astrology.js, and includes/horoscope.js inside a video downloading extension is immediately suspicious. These files contain:
- Language-gated modal popups advertising astrology surveys
- Google Analytics tracking events sent to
G-94HR5L4844 - Large base64-encoded images (confirmed by the scanner as PNG and JPEG data) secretly embedded as data URIs
The extension is a bundled malware package β it includes a functional (if unremarkable) video downloader as cover for its real purpose: a platform to inject unsolicited advertising, harvest browsing data, perform tab surveillance, and exfiltrate it to remote servers.
Finding 10: Download Monitoring
The scanner found the extension listening on the Chrome Downloads API:
// js/background.js β line 4768 and 4807
chrome.downloads.onChanged.addListener(n);
This listener is triggered every time a download starts, progresses, or completes in the browser β regardless of whether it was initiated by the extension. A legitimate video downloader would initiate downloads; it has no reason to passively watch all downloads the user performs independently. This is surveillance.
How Does It Hide? The Evasion Playbook
What makes this extension particularly interesting from a security research perspective is not just what it does, but how carefully it avoids being caught. The hiding techniques stack on top of each other in layers.
Layer 1 β The Cover Story
The extension bundles a genuine, working video downloader. It handles YouTube, Facebook, Vimeo, TikTok, DailyMotion, Rutube, Odnoklassniki, VKontakte, SoundCloud, and more. The download functionality works. This gives the extension a credible reason to exist and request the broad permissions it needs β if anyone asks why it needs <all_urls> and tabs, the answer is: "because we support downloading from dozens of websites."
The legitimate functionality is the camouflage.
Layer 2 β Delayed Activation (The Time-Bomb)
The extension does not activate its full behavior immediately. It stores an televzr_installed flag in localStorage and a 12-hour timeout timer called trackTimeout:
// background.js β simplified
if (!Xn.liteStorage.isTimeout("trackTimeout")) {
Xn.liteStorage.setTimeout("trackTimeout", 300); // 5 minutes first
// ... then after first success:
Xn.liteStorage.setTimeout("trackTimeout", 43200); // 12 hours
}
Extension store review processes typically evaluate an extension for minutes, not hours. By gating heavier telemetry behind a 12-hour timer and a first-run flag, the most privacy-invasive behavior is statistically very unlikely to occur during any automated or manual review session.
Layer 3 β Code Hidden in an Image
Rather than placing all executable JavaScript in .js files β where any security scanner will look first β the extension hides a JavaScript payload inside a PNG image. The bytes come after the PNG's IEND terminator chunk, which means every image viewer, browser, and most security tools treat the file as a valid image and stop reading there.
The hidden content uses a custom marker to make it retrievable:
__begin_view__
ZnVuY3Rpb24gc3RhcnRBZGRyKGEpe2lm...
Code elsewhere in the extension can fetch this image file, find __begin_view__, base64-decode the tail, and execute it. The payload itself (function startAddr) handles URL message routing and origin logging β a foundation for cross-origin communication that security tools scanning only .js files would never see.
Layer 4 β JavaScript Obfuscation and Eval-Bypass
The JavaScript files are minified and bundled via webpack β standard practice β but several files go further. The pattern new Function("return this")() appears in multiple places:
// Pattern found in astrologyvalley.js, commons.js, and others
o.g = function() {
if ("object" == typeof globalThis) return globalThis;
try {
return this || new Function("return this")();
} catch (e) { ... }
}
new Function("return this")() is a well-documented technique for obtaining the global window object while bypassing CSP restrictions on eval. Even when a content security policy nominally forbids eval, this construction can slip through depending on browser version and CSP implementation. Its presence alongside an 'unsafe-eval' CSP is a belt-and-suspenders obfuscation strategy.
Layer 5 β Language and Locale Gating
The advertising injection (the astrology survey popups) is only shown to users with English (en) or Hindi (hi) browser language settings:
var n = [ "en", "hi" ];
if ((navigator.languages || [ navigator.language ])
.some((e => n.includes(e.slice(0, 2).toLowerCase())))) {
// inject advertising overlay
}
Reviewers working in German, French, Russian, or any other language would never see this behavior. This is geographic and linguistic targeting designed to keep the advertising payload invisible to the majority of users who might notice it and report it, while still reaching the largest English-speaking user base.
Layer 6 β Plausible Permission Justifications
Every dangerous permission in the manifest has a legitimate excuse:
tabsis needed to find which tab the user is on when the popup is openeddownloadsis needed to save video fileswebRequestis needed to intercept video stream URLs<all_urls>is needed because videos are hosted on many domains
None of these justifications are false. But together, the permissions also enable everything needed for surveillance. The extension weaponizes the gap between what permissions are claimed for and what they also enable.
Full Scanner Output Summary
You can read the full scan output here: https://www.yourdev.net/assets/youtube-video-download_analysis.txt. Below i have summarized and shown the most interesting stuff.
extension-scanner.py "Youtube Video Downloader.xpi" --verbose
[i] Analyzing 1 target(s) with minimum severity 'INFO'
[+] Found 1 XPI(s) to analyze
[i] Analyzing XPI: Youtube Video Downloader.xpi
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
XPI ANALYZER Γ’β¬β Youtube Video Downloader.xpi
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Extension Name: YouTube Video Download
Extension UUID: {8ba275e2-6750-41c3-a944-307e38c2a5e2}
Overall verdict: CRITICAL RISK
Findings: 30 CRITICAL 133 HIGH 117 MEDIUM 59 LOW 1 INFO
ββββ CRITICAL ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[CRITICAL] [API_EXFIL_COMBO] includes/facebook_com.js:
Data collection (keyboard listener) combined with outbound network call in same file Γ’β¬β likely exfiltration
[CRITICAL] [API_EXFIL_COMBO] includes/odnoklassniki_ru.js:
Data collection (keyboard listener) combined with outbound network call in same file Γ’β¬β likely exfiltration
[CRITICAL] [API_EXFIL_COMBO] js/background.js:
Data collection (tabs.query({}), tabs.query({})) combined with outbound network call in same file Γ’β¬β likely exfiltration
[CRITICAL] [EXFIL_CHAIN] manifest.json:
Cross-file exfiltration chain: <all_urls> declared + network call in JS Γ’β¬β all page content is accessible for exfiltration
CODE: Network calls observed in: includes/commons.js, includes/facebook_com.js, includes/mail_ru.js (+3 more)
[CRITICAL] [JS_OBFUSCATION] includes/horoscope.js:11
data:image base64 URI assigned to JS variable Γ’β¬β likely obfuscated string table hiding C2 URLs or config, not real image data
CODE: ss="close"><img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibΓ’β¬Β¦
Context: var n = document.createElement("div");
n.id = "thirdModal", n.classList.add("third-modal");
n.innerHTML = '\n <div class="third-modal__window">\n <button class="close"><img src="data
[CRITICAL] [JS_OBFUSCATION] includes/horoscope.js:11
data:image base64 URI assigned to JS variable Γ’β¬β likely obfuscated string table hiding C2 URLs or config, not real image data
CODE: on>\n <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGIAAAA8CAYAAACdIW+JAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAΓ’β¬Β¦
Context: var n = document.createElement("div");
n.id = "thirdModal", n.classList.add("third-modal");
n.innerHTML = '\n <div class="third-modal__window">\n <button class="close"><img src="data
[CRITICAL] [JS_OBFUSCATION] includes/link_modifier.js:51
data:image base64 URI assigned to JS variable Γ’β¬β likely obfuscated string table hiding C2 URLs or config, not real image data
CODE: buttonSrc: "data:image/gif;base64,R0lGODlhEAAQAOZ3APf39+Xl5fT09OPj4/Hx8evr6/3+/u7u7uDh4OPi497e3t7e3/z8/P79/X3GbuXl5ubl5eΓ’β¬Β¦
Context: height: "auto"
},
buttonSrc: "data:image/gif;base64,R0lGODlhEAAQAOZ3APf39+Xl5fT09OPj4/Hx8evr6/3+/u7u7uDh4OPi497e3t7e3/z8/P79/X3GbuXl5ubl5eHg4WzFUfb39+Pj4lzGOV7LOPz7+/n6+vn5+ZTLj9/e387Ozt
[CRITICAL] [JS_OBFUSCATION] includes/commons.js:433
data:image base64 URI assigned to JS variable Γ’β¬β likely obfuscated string table hiding C2 URLs or config, not real image data
CODE: televzr: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABN2lDQ1BBZG9iZSBSR0IgKDE5OTgpAAAokZWPv0rDΓ’β¬Β¦
Context: });
})), Z = {
televzr: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABN2lDQ1BBZG9iZSBSR0IgKDE5OTgpAAAokZWPv0rDUBSHvxtFxaFWCOLgcCdRUGzVwYxJW4ogWKtDkq1JQ5ViEm6uf/oQjm4dXNx9AidHwUHxCXwDxamDQ4QMBYv
[CRITICAL] [JS_OBFUSCATION] includes/commons.js:2613
data:image base64 URI assigned to JS variable Γ’β¬β likely obfuscated string table hiding C2 URLs or config, not real image data
CODE: popupCloseBtn: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAWUlEQVQ4y2NgGHHAH4j1sYjrQ+WIAvFA/BΓ’β¬Β¦
Context: }
},
popupCloseBtn: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAWUlEQVQ4y2NgGHHAH4j1sYjrQ+WIAvFA/B+I36MZpg8V+w9VQ9Al/5EwzDBkQ2AYr8uwaXiPQ0yfkKuwGUayIYQMI8kQqhlEFa9RLbCpFv1US5BUzSLDBAA
[CRITICAL] [JS_OBFUSCATION] includes/commons.js:2813
data:image base64 URI assigned to JS variable Γ’β¬β likely obfuscated string table hiding C2 URLs or config, not real image data
CODE: ckgroundImage: "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA0YAAAGaCAYAAAArR1NlAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAΓ’β¬Β¦
Context: zIndex: e + 2,
borderRadius: "10px",
backgroundImage: "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA0YAAAGaCAYAAAArR1NlAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IA
[CRITICAL] [JS_OBFUSCATION] includes/commons.js:12294
data:image base64 URI assigned to JS variable Γ’β¬β likely obfuscated string table hiding C2 URLs or config, not real image data
CODE: ht="207" xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAgICAgJCAkKCgkNDgwODRMREBARExwUFhQWFBwrGx8bΓ’β¬Β¦
Context: embed: '\n <svg width="25" height="21" viewBox="0 0 25 21" fill="none" xmlns="http://www.w3.org/2000/svg">\n <path d="M9.32402 2L2.46143 10.4L9.32402 18.8M16.2758 2L23.1383 10.4L16.2758 18.8" stro
[CRITICAL] [JS_OBFUSCATION] includes/commons.js:14132
data:image base64 URI assigned to JS variable Γ’β¬β likely obfuscated string table hiding C2 URLs or config, not real image data
CODE: e.exports = "data:image/jpeg;base64,/9j/4QxRRXhpZgAATU0AKgAAAAgADQEAAAMAAAABBQAAAAEBAAMAAAABAeQAAAECAAMAAAADAAAAqgEGAAMAΓ’β¬Β¦
Context: 4338: e => {
"use strict";
e.exports = "data:image/jpeg;base64,/9j/4QxRRXhpZgAATU0AKgAAAAgADQEAAAMAAAABBQAAAAEBAAMAAAABAeQAAAECAAMAAAADAAAAqgEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEVAAMAAAABAAMAAAEaAAUAAAABAAAAsAEbAAUAAAABAAAAuAEoAAMAAAA
[CRITICAL] [JS_OBFUSCATION] js/background.js:4833
data:image base64 URI assigned to JS variable Γ’β¬β likely obfuscated string table hiding C2 URLs or config, not real image data
CODE: { t("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB90lEQVQ4EcVSy2oUURCtqm7HcYgmYΓ’β¬Β¦
Context: },
getUmmyIcon: function(e, t) {
t("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB90lEQVQ4EcVSy2oUURCtqm7HcYgmYDTiYxEEERdZGP0B0UVwEcSv8LHIb4gbQcjGlVtB40YhfkAWuhs0uFOIgjJomiEzztzue4+n7rTg
[CRITICAL] [PNG_APPENDED] img/view.png:
24268 bytes appended after PNG IEND (entropy=5.73) Γ’β¬β classic stego carrier
CODE: b'\n__begin_view__\nZnVuY3Rpb24gc3RhcnRBZGRyKGEpe2lmKGEpe2lmKCJtZXNz'
[CRITICAL] [PNG_APPENDED] img/view.png:
Appended trailer decoded via base64 Γ’β β 18194 bytes
CODE: b'm\xe8"\x9e\xf8\x9e\xc1\x99\xd5\xb9\x8d\xd1\xa5\xbd\xb8\x81\xcd\xd1\x85\xc9\xd1\x05\x91\x91\xc8\xa1\x84\xa5\xed\xa5\x98Γ’β¬Β¦
βββββ HIGH βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[HIGH ] [API_ABUSE] includes/facebook_com.js:727
Keyboard event listener (keydown/keypress/keyup) Γ’β¬β potential keylogger
CODE: addEventListener("keydown"
[HIGH ] [BASE64_PAYLOAD] includes/astrology.js:11
Base64 payload contains HTTP URL
CODE: blob[:432]= PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyΓ’β¬Β¦
[HIGH ] [BASE64_PAYLOAD] includes/astrology.js:11
Base64 blob decodes to high-entropy (7.99) binary (55731 bytes) Γ’β¬β possible encrypted payload
CODE: blob= iVBORw0KGgoAAAANSUhEUgAAARYAAAEKCAYAAADXWXqvAAAACXBIWXMAAAsTΓ’β¬Β¦
[HIGH ] [BASE64_PAYLOAD] includes/astrology.js:24
Base64 blob decodes to high-entropy (7.99) binary (298896 bytes) Γ’β¬β possible encrypted payload
CODE: blob= iVBORw0KGgoAAAANSUhEUgAAAsYAAAGQCAYAAACtV6bCAAAACXBIWXMAAAsTΓ’β¬Β¦
[HIGH ] [BASE64_PAYLOAD] includes/youtube_com.js:572
Base64 blob decodes to high-entropy (8.00) binary (600073 bytes) Γ’β¬β possible encrypted payload
CODE: blob= iVBORw0KGgoAAAANSUhEUgAAEAAAAAFuCAYAAAAB9N1HAAAAAXNSR0IArs4cΓ’β¬Β¦
[HIGH ] [BASE64_PAYLOAD] includes/youtube_com.js:2862
Base64 payload contains HTTP URL
CODE: blob[:248]= PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoΓ’β¬Β¦
[HIGH ] [BASE64_PAYLOAD] includes/commons.js:433
Base64 payload contains HTTP URL
CODE: blob[:5036]= iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABN2lDQ1BBZG9iΓ’β¬Β¦
[HIGH ] [CLASS_STORAGE_OVERLAP] includes/soundcloud_com.js:
String literal 'loading' appears both as a JS string in this file and as an HTML class attribute in options.html, popup.html Γ’β¬β likely used as a covert stego marker or out-of-band key
CODE: class='loading' in options.html, popup.html
[HIGH ] [CREDENTIALS] js/background.js:195
Hardcoded secret/token/password
CODE: token=" + this.accessToken, n = t[0].replace(/[?&]access_token=[^&#]/, "
[HIGH ] [CSP] manifest.json:
Unsafe CSP directive: 'unsafe-eval' Γ’β¬β allows code injection
CODE: script-src 'self' 'unsafe-eval'; object-src 'self';
[HIGH ] [HIDDEN_ELEMENT] includes/commons.js:376
Invisible iframe injected into DOM Γ’β¬β silent affiliate ping or C2 channel
CODE: createElement("iframe"
[HIGH ] [JS_OBFUSCATION] includes/vimeo_com.js:856
Function constructor with string Γ’β¬β eval equivalent
CODE: return this || new Function("return this")(); } catch (e) {
Context: if ("object" == typeof globalThis) return globalThis;
try {
return this || new Function("return this")();
} catch (e) {
if ("object" == typeof window) return window;
[HIGH ] [JS_OBFUSCATION] includes/vkontakte_ru.js:780
setTimeout/setInterval with variable argument Γ’β¬β indirect eval when variable holds a decoded string payload
CODE: setTimeout(e, 250); }));
Context: }().then((function() {
return new Promise((function(e) {
setTimeout(e, 250);
}));
}));
[HIGH ] [JS_OBFUSCATION] includes/vkontakte_ru.js:838
setTimeout/setInterval with variable argument Γ’β¬β indirect eval when variable holds a decoded string payload
CODE: setTimeout(e, 250);
Context: o[t] = e, r[t] = e, n++;
})), t.onProgress && t.onProgress(n, s), new Promise((e => {
setTimeout(e, 250);
[HIGH ] [JS_OBFUSCATION] includes/vkontakte_ru.js:954
setTimeout/setInterval with variable argument Γ’β¬β indirect eval when variable holds a decoded string payload
CODE: setTimeout(e, 250); })).th
Context: if (!t) throw new Error("Album data is empty!");
return new Promise((function(e) {
setTimeout(e, 250);
})).then((funct
[HIGH ] [JS_OBFUSCATION] includes/facebook_com.js:1177
Function constructor with string Γ’β¬β eval equivalent
CODE: return this || new Function("return this")(); } catch (e) {
Context: if ("object" == typeof globalThis) return globalThis;
try {
return this || new Function("return this")();
} catch (e) {
if ("object" == typeof window) return window;
[HIGH ] [JS_OBFUSCATION] includes/facebook_com.js:727
keydown listener Γ’β¬β could be a keylogger; verify it's UI-related
CODE: utation || document.addEventListener("keydown", o);
Context: parent: e,
onShow: function() {
w.isMutation || document.addEventListener("keydown", o);
[HIGH ] [JS_OBFUSCATION] js/options.js:405
setTimeout/setInterval with variable argument Γ’β¬β indirect eval when variable holds a decoded string payload
CODE: }, o = setTimeout(n, 100); ve && (t = requestAnim
Context: var t, n = function() {
clearTimeout(o), ve && cancelAnimationFrame(t), setTimeout(e);
}, o = setTimeout(n, 100);
ve && (t = requestAnimationFrame(n));
}
[HIGH ] [JS_OBFUSCATION] js/menu.js:60
setTimeout/setInterval with variable argument Γ’β¬β indirect eval when variable holds a decoded string payload
CODE: }, i = setTimeout(n, 250); a.A.sendMessage({
Context: for (var n in clearTimeout(i), e.isNotResponse = !t, t) e[n] = t[n];
g(e), m(e);
}, i = setTimeout(n, 250);
a.A.sendMessage({
action: "get
[HIGH ] [JS_OBFUSCATION] js/background.js:3363
atob() Γ’β¬β decoding base64 at runtime (possible payload decode)
CODE: (decodeURIComponent(atob(t))); var t;
Context: return H([ "credentials" ]).then((e => {
if (!e || !e.credentials) throw new Ne("Credentials not found", ze);
return t = e.credentials, JSON.parse(decodeURIComponent(atob(t)));
var t;
[HIGH ] [JS_OBFUSCATION] js/background.js:7408
atob() Γ’β¬β decoding base64 at runtime (possible payload decode)
CODE: clientId: atob("aGVscGVyLnBybw"), c
Context: init() {
this.client = new (Ge())({
clientId: atob("aGVscGVyLnBybw"),
clientSecret: atob("RTkyRkQ2RTM5RTM1RDUzQUQ5NkMwNzVDQjBFQzFCMEU4NkI0M0UwQzY3OTAzRDhBNjk5NDVCQkY1QUU0RjkxMA"),
[HIGH ] [JS_OBFUSCATION] js/background.js:7409
atob() Γ’β¬β decoding base64 at runtime (possible payload decode)
CODE: clientSecret: atob("RTkyRkQ2RTM5RTM1RDUzQUQ5NkMwNzVDQjBFQzF
Context: this.client = new (Ge())({
clientId: atob("aGVscGVyLnBybw"),
clientSecret: atob("RTkyRkQ2RTM5RTM1RDUzQUQ5NkMwNzVDQjBFQzFCMEU4NkI0M0UwQzY3OTAzRDhBNjk5NDVCQkY1QUU0RjkxMA"),
acc
[HIGH ] [JS_OBFUSCATION] js/background.js:8687
setTimeout/setInterval with string argument Γ’β¬β indirect eval
CODE: Xn.liteStorage.setTimeout("trackTimeout", 300); var
Context: (Xn.userTrack = function() {
if (!Xn.liteStorage.isTimeout("trackTimeout")) {
Xn.liteStorage.setTimeout("trackTimeout", 300);
var e = {
t: "screenview",
[HIGH ] [JS_OBFUSCATION] js/background.js:8697
setTimeout/setInterval with string argument Γ’β¬β indirect eval
CODE: Xn.liteStorage.setTimeout("trackTimeout", 43200), Xn.events.emit("s
Context: id: "init",
onSuccess: function() {
Xn.liteStorage.setTimeout("trackTimeout", 43200), Xn.events.emit("sendScreenView"),
Pe().then((() => {
[HIGH ] [PERMISSION] manifest.json:
Dangerous permission: '<all_urls>' Γ’β¬β Access to ALL website content Γ’β¬β can read/exfiltrate any page data
PERMISSION: permissions: ['tabs', 'downloads', 'storage', '<all_urls>', 'webRequest', 'webNavigation', 'webRequestBlocking']
[HIGH ] [PERMISSION] manifest.json:
Dangerous permission: 'webRequestBlocking' Γ’β¬β Can intercept and modify all HTTP(S) traffic
PERMISSION: permissions: ['tabs', 'downloads', 'storage', '<all_urls>', 'webRequest', 'webNavigation', 'webRequestBlocking']
[HIGH ] [PNG_CHUNK] img/view.png:
Unknown PNG chunk type 'egin' (24260 bytes) Γ’β¬β non-standard chunks can hide data
CODE: b'_view__\nZnVuY3Rpb24gc3RhcnRBZGRyKGEpe2lm'
[HIGH ] [SUSPICIOUS_URL] includes/vkontakte_ru.js:2253
Known malicious domain (blocklist hit): savefrom.net
URL: http://savefrom.net/?url=
Context: marginBottom: "-4px"
}, o = x.A.create("a", {
href: "http://savefrom.net/?url=" + encodeURIComponent(t),
style: r,
[HIGH ] [SUSPICIOUS_URL] includes/facebook_com.js:305
Known malicious domain (blocklist hit): savefrom.net
URL: http://savefrom.net/?url=
Context: if (!(e = this.checkUrl(e))) return !1;
var i = document.createElement("a");
i.className = w.className, i.href = "http://savefrom.net/?url=" + encodeURIComponent(e),
[HIGH ] [SUSPICIOUS_URL] includes/link_modifier.js:53
Known malicious domain (blocklist hit): savefrom.net
URL: http://savefrom.net/
Context: buttonSrc: "data:image/gif;base64,R0lGODlhEAAQAOZ3APf39+Xl5fT09OPj4/Hx8evr6/3+/u7u7uDh4OPi497e3t7e3/z8/P79/X3GbuXl5ubl5eHg4WzFUfb39+Pj4lzGOV7LOPz7+/n6+vn5+ZTLj9/e387Ozt7f3/7+/vv7/ISbePn5+m/JV1nRKXmVbkCnKVrSLDqsCuDh4d/e3uDn3/z7/H6T
[HIGH ] [TIME_BOMB] js/background.js:8697
setTimeout/setInterval with 1996-minute delay (119,781,451 ms) Γ’β¬β potential time-gated payload activation
CODE: setTimeout(setTimeout("trackTimeout", 43200), Xn.events.emit("sendScreenView"), Pe().then((Γ’β¬Β¦
βββ MEDIUM βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[MEDIUM ] [API_ABUSE] includes/commons.js:1581
btoa() encoding co-located with network send Γ’β¬β data is being base64-encoded before exfiltration
CODE: btoa(
[MEDIUM ] [JS_OBFUSCATION] includes/commons.js:4341
window.open(_blank) Γ’β¬β opened page can access window.opener; potential reverse tabnapping if noopener is not specified
CODE: window.open("https://ru.savefrom.net/#url=" + e, "_blank"
[MEDIUM ] [JS_OBFUSCATION] js/options.js:1257
String.fromCharCode Γ’β¬β character-code obfuscation
CODE: }), String.fromCharCode(160), h.A.create("span", {
Context: id: "ffmpegEnabled",
checked: !1
}), String.fromCharCode(160), h.A.create("span", {
text: v.A.i18n.getMessage("optionsFfmpegEn
[MEDIUM ] [JS_OBFUSCATION] js/options.js:226
Long innerHTML assignment Γ’β¬β possible HTML injection
CODE: ]; else if (f && (e.innerHTML = ""), j(e, k(p) ? p : [ p ], t, n, r, i && "foreignObject" !== g, l, a, l ? l[0] : n.__k Γ’β¬Β¦
Context: for (_ in y) d = y[_], "children" == _ ? p = d : "dangerouslySetInnerHTML" == _ ? c = d : "value" == _ ? m = d : "checked" == _ ? v = d : "key" === _ || s && "function" != typeof d || h[_] === d || R(e, _, d, h[_], i);
[MEDIUM ] [JS_OBFUSCATION] js/background.js:8742
fetch() call Γ’β¬β verify destination is legitimate
CODE: n); fetch(`https://monitoring-exporter.sf-helper.c
Context: if (!!Xn.preferences.dataCollectionEnabled) {
var r = t.params, n = r.type, o = e(r, Qn);
fetch(`https://monitoring-exporter.sf-helper.com/api/v3/${n}`, {
method: "POST",
[MEDIUM ] [LOCALE_ABUSE] _locales/pt/messages.json:
Locale message 'filelistInstruction' contains an embedded URL Γ’β¬β locale files should not contain URLs
CODE: http://en.wikipedia.org/wiki/Download_manager
CODE: http://www.freedownloadmanager.org/
[MEDIUM ] [PERMISSION] manifest.json:
Dangerous permission: 'downloads' Γ’β¬β Can initiate and read downloads
PERMISSION: permissions: ['tabs', 'downloads', 'storage', '<all_urls>', 'webRequest', 'webNavigation', 'webRequestBlocking']
[MEDIUM ] [PERMISSION] manifest.json:
Dangerous permission: 'webRequest' Γ’β¬β Can observe all HTTP(S) requests
PERMISSION: permissions: ['tabs', 'downloads', 'storage', '<all_urls>', 'webRequest', 'webNavigation', 'webRequestBlocking']
[MEDIUM ] [TIME_BOMB] includes/odnoklassniki_ru.js:838
Date arithmetic gate Γ’β¬β code behaviour may depend on elapsed time since a stored timestamp
CODE: Date.now() +
βββββ LOW ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[LOW ] [SUSPICIOUS_URL] includes/astrologyvalley.js:28
External domain Γ’β¬β not on known-good allowlist: forms.gle
URL: https://forms.gle/EQwCGiyRpkaQx3qGA
Context: document.body.append(e), g("toolbar_survey", "show", "toolbar_survey_show");
}), 1e4), e.querySelector(".second-modal__btn").addEventListener("click", (() => {
window.open("https://f
ββββ INFO ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[INFO ] [METADATA] Youtube Video Downloader.xpi:
SHA-256: dccb98f494e00345447c93f5bb5e79dc29a4c96fce276780fe585ff521b3a848 | size: 3,696,875 bytes
Extension Name: YouTube Video Download
Extension UUID: {8ba275e2-6750-41c3-a944-307e38c2a5e2}
Overall verdict: CRITICAL RISK
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
How Does the Scanner Prove These Findings?
Every finding includes an evidence snippet β actual code captured from the file at the match location, truncated to 160 characters. You are never asked to trust the scanner blindly; you can open the extracted source files and verify every finding at the exact line number reported.
For the steganographic PNG payload, I showed above how you can verify it yourself:
import base64
payload = "ZnVuY3Rpb24gc3RhcnRBZGRyKGEpe2lmKGEpe2lmKCJtZXNz..."
print(base64.b64decode(payload).decode("utf-8"))
# β function startAddr(a){if(a){if("message"==typeof a)...
You can extract and decode the appended bytes from img/view.png with Python's zipfile module and verify the __begin_view__ marker yourself in under ten lines of code.
How the Detection Logic Works: A Deeper Look
Shannon Entropy for Obfuscation Detection
Many of the JS_OBFUSCATION findings use Shannon entropy as a signal. Entropy is calculated as:
$$H = -\sum_{i} \frac{c_i}{n} \log_2 \frac{c_i}{n}$$
where $c_i$ is the count of byte value $i$ and $n$ is the total byte count.
Readable English or normal JavaScript code typically has entropy around 4β4.5 bits/byte. Minified JavaScript sits around 5β5.5. Strings with entropy above 5.5 bits/byte are flagged as potential encrypted or obfuscated payloads β because they contain more uniform randomness than any natural-language or normal code string should.
API Exfiltration Combo Detection
The CRITICAL / API_EXFIL_COMBO finding is deliberately conservative β it only fires when both a data-collection API call and an outbound network call appear in the same file. This two-signal requirement dramatically reduces false positives. Code that reads clipboard data for local use won't trigger it. Code that sends data to a remote server without ever reading sensitive APIs won't trigger it. Only the conjunction β the steal-and-send pattern β does.
PNG Steganography Detection
The check_png_appended module:
- Reads the full file bytes of every
.pngentry in the archive - Finds the last occurrence of the IEND marker (
b"IEND") - Everything after offset
idx + 8(4-byte length + 4-byte CRC) is "appended data" - If more than 64 bytes are present:
CRITICAL; between 1β63 bytes:HIGH - The appended bytes are then attempted through six decoding strategies: plain base64, stripped base64, zlib, gzip, base64βzlib, base64βgzip
- Successfully decoded content is then recursively scanned by the JavaScript obfuscation, base64 payload, and credential leakage checks
This chain is what caught the hidden function startAddr JavaScript inside what appeared on the surface to be a simple icon file.
Indicators of Compromise (IoCs)
Based on the scanner's analysis, the following indicators are associated with this extension:
| Type | Value |
|---|---|
| Extension UUID | {8ba275e2-6750-41c3-a944-307e38c2a5e2} |
| C2 / Telemetry domain | monitoring-exporter.sf-helper.com |
| Analytics ID (GA4) | G-94HR5L4844 |
| Analytics ID (UA) | UA-67738130-2, UA-119781451-36 |
| Injected URL scheme | televzr:// |
| localStorage key | televzr_installed |
| PNG steganography marker | __begin_view__ |
| Injected survey domain | astrologyvalley.com |
What Should You Do if You Have This Extension Installed?
- Remove it immediately. Firefox:
about:addonsβ find the extension β Remove. - Revoke any OAuth tokens issued to services you used while the extension was installed β especially Facebook, VK, and Odnoklassniki where the keylogger was active.
- Change passwords for accounts you accessed through browsers where this extension was installed.
- Review your localStorage for the
televzr_installedkey and clear it if present. - Check your browser history for unexpected navigations to
monitoring-exporter.sf-helper.comorastrologyvalley.com.
Limitations and False Positive Rates
Static analysis has inherent limitations. This section is important β please read it before acting on any scanner output.
The scanner is a starting point, not a conclusion
A CRITICAL finding means: "This pattern is statistically associated with malicious behavior and you should look at this code manually." It does not mean: "This extension is definitively malicious."
Real-world examples of patterns that trigger high findings in completely legitimate extensions:
- A password manager extension legitimately registers
keydownlisteners to capture keyboard shortcuts β and has outbound network calls to sync your vault. This would triggerAPI_EXFIL_COMBO. It is not a keylogger. - A tab manager extension legitimately calls
chrome.tabs.query({})to enumerate all tabs. This is its entire purpose. - A marketing analytics SDK embedded in an extension will produce dozens of
SUSPICIOUS_URLandBASE64_PAYLOADfindings against known CDNs. - A highly optimized extension might use
new Function("return this")()for polyfill purposes, producing obfuscation findings.
The appropriate response to a high scanner score is always: read the flagged code and form a human judgement.
Common false positive sources
- Minified code is not malicious by default. Large production codebases minify their JavaScript. The scanner distinguishes minification from obfuscation by entropy and pattern-matching β but some legitimate extensions will score HIGH on obfuscation findings.
keydownlisteners are common. Keyboard shortcut handlers registerkeydown. The scanner requires theAPI_EXFIL_COMBOsecond signal (an outbound network call in the same file) before escalating to CRITICAL β but that second signal also exists in many legitimate extensions that both capture input and communicate with a backend.- Base64 images produce noise. Embedding images as data URIs generates many
BASE64_PAYLOADfindings. The scanner annotates these clearly as PNG/JPEG content so they can be quickly dismissed. localStorageflags are everywhere. Onboarding flows, A/B tests, and feature toggles all uselocalStorage. ATIME_BOMBfinding onlocalStorage.getItem("installDate")alone is weak evidence.
Why the confidence is high in this specific case
In the case of the YouTube Video Download extension, the conclusion of likely-malicious intent is based not on any single finding but on the convergence of multiple independently suspicious behaviors that have no plausible innocent explanation when taken together:
- Code hidden inside a PNG file (no legitimate extension needs to do this)
- Advertising overlay files (
astrologyvalley.js) that have no relationship to video downloading - A 12-hour activation delay (legitimate telemetry does not need to wait 12 hours)
- A
televzr_installedpersistence flag gating behavior (the extension gates behavior on install state) - Keylogger pattern co-located with network calls on social networking site content scripts
Each of these individually could be argued. Together, they paint a consistent picture of an extension designed to appear legitimate while conducting undisclosed user surveillance and advertising injection. But even here, I encourage you to review the source code in the extracted/ directory and reach your own conclusion.
Get the Scanner
extension-scanner.py is available at:
github.com/ernos/browser-xpi-malware-scanner
Dependencies are minimal: Python 3.8+, and optionally Pillow + NumPy for the LSB steganography checks. Install them with:
pip install -r requirements.txt
Scan a single extension:
python3 extension-scanner.py my-extension.xpi
Scan a whole directory of extensions recursively:
python3 extension-scanner.py ./extensions/ --min-severity HIGH --json report.json
Update the blocklist from live threat intelligence feeds:
python3 extension-scanner.py --update-blocklist
Contributions, new check modules, and blocklist additions are welcome via pull request.
Conclusion
Browser extensions are among the most under-scrutinised software on most users' machines. They run with elevated privileges, persist silently across browser sessions, and are trusted implicitly once installed. A tool like extension-scanner.py can't replace thorough manual code review β but it can immediately surface the most dangerous patterns, prioritise where a reviewer should look first, and make it significantly harder for malicious extensions to slip past unnoticed.
The "YouTube Video Download" extension examined here is a textbook case of a multi-payload extension: a functional feature set designed to justify installation, concealing keyloggers, bulk tab surveillance, install-date arming, invisible iframe injection, and a steganographic code-loading payload hidden in a PNG. Every single one of these behaviours was caught automatically, with line-level evidence, in a single command.
If you work on a platform that distributes browser extensions, or if you're a security researcher, I encourage you to run extension-scanner.py against your extension ecosystem. The results may surprise you.
Questions about the extension-scanner.py script or the article itself, contact info here:
Contact Form
https://www.github.com/ernos/browser-xpi-malware-scanner
Need an Android Developer or a full-stack website developer?
I specialize in Kotlin, Jetpack Compose, and Material Design 3. For websites, I use modern web technologies to create responsive and user-friendly experiences. Check out my portfolio or get in touch to discuss your project.


