Is JSON Formatter safe?
JSON Formatter embeds a GiveFreely affiliate tracking library that monitors shopping browsing and sends search result URLs across all websites.
On every page load, the extension checks the current hostname against a remote GiveFreely merchant list and fires analytics events — including the page URL, a persistent device ID, and the names of competing affiliate extensions installed — to GiveFreely's servers. On Google search pages it collects all result URLs (including sponsored ads) and reports them to GiveFreely. When a user accepts a donation offer through the GiveFreely popup, the extension opens a hidden pinned tab to a wild.link affiliate URL that encodes the original page URL, user ID, and charity identifiers before closing after 30 seconds.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Extension Sends Your IP to MaxMind Using a Hardcoded API Key
JSON Formatter's GiveFreely feature calls MaxMind's GeoIP API on install, using a hardcoded account ID/key, same for all users, before any action.
Gets HTTP 401, so lookup fails, but the IP reaches MaxMind, uncached, on every popup mount.
You install the extension, or its GiveFreely charity popup loads on a page you visit.
No search, click, or settings change is required -- the check runs as soon as the popup component initializes.
The extension sends your IP address to MaxMind's GeoIP API using a credential built into its code.
The same MaxMind account ID and license key ship inside every install, in both the content script and the background service worker.
| Content-Type | application/json |
| Authorization | Basic OTEzOTkxOnFlYmpaWF9DOGNRY0lxSHA4WTVjNGxzU1pRSlM2VW9MMExzTF9tbWs= |
The Authorization header is base64, not encryption -- decoding it recovers the account credential in plain text. MaxMind currently rejects this specific credential (HTTP 401), so the decoded value below does not expose a working account.
913991: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
Country/IP lookup, content/core.js:297 and worker/worker.js:1
// content/core.js:297 -- runs when the GfApp popup component mounts on any page
const countryCode = await (async () => {
return (
(await storage.get("countryCode")) ||
(async () => {
logger.debug("detecting country");
const requestOptions = {
method: "GET",
headers: {
"Content-Type": "application/json",
// Hardcoded MaxMind account credential, identical on every install
Authorization:
"Basic OTEzOTkxOnFlYmpaWF9DOGNRY0lxSHA4WTVjNGxzU1pRSlM2VW9MMExzTF9tbWs=",
},
};
try {
const response = await fetch(
"https://geoip.maxmind.com/geoip/v2.1/country/me",
requestOptions,
);
logger.debug("Received geolocation response", { status: response.status });
if (!response.ok) {
logger.error("Failed to get geolocation data", {
status: response.status,
statusText: response.statusText,
});
return;
}
const geo = await response.json();
if (!geo) throw Error("There was an error fetching geoip look up");
await storage.set("countryCode", geo.country.iso_code);
return geo.country.iso_code;
} catch (err) {
logger.error("Failed to get geolocation data", { error: err }, true);
}
})()
);
})() ?? "us";// worker/worker.js:1 -- named `E3` in the shipped bundle
const getCountryCode = async () =>
(await storage.get("countryCode")) ||
(async () => {
logger.debug("detecting country");
const requestOptions = {
method: "GET",
headers: {
"Content-Type": "application/json",
// Same hardcoded MaxMind account credential as content/core.js
Authorization:
"Basic OTEzOTkxOnFlYmpaWF9DOGNRY0lxSHA4WTVjNGxzU1pRSlM2VW9MMExzTF9tbWs=",
},
};
try {
const response = await fetch(
"https://geoip.maxmind.com/geoip/v2.1/country/me",
requestOptions,
);
logger.debug("Received geolocation response", { status: response.status });
if (!response.ok) {
logger.error("Failed to get geolocation data", {
status: response.status,
statusText: response.statusText,
});
return;
}
const geo = await response.json();
if (!geo) throw Error("There was an error fetching geoip look up");
await storage.set("countryCode", geo.country.iso_code);
return geo.country.iso_code;
} catch (err) {
logger.error("Failed to get geolocation data", { error: err }, true);
}
})();- geoip.maxmind.com
MaxMind, Inc. GeoIP2 API. Resolves the IP to a country. Extension authenticates with a fixed embedded credential; MaxMind returns 401 but still gets the IP each request.
Decodes the Basic-Auth Authorization header the extension sends to MaxMind on every install, revealing the embedded MaxMind account ID and license key.
// Decodes the hardcoded Basic-Auth Authorization header that
// content/core.js and worker/worker.js send to MaxMind's GeoIP API
// (https://geoip.maxmind.com/geoip/v2.1/country/me) on every install.
//
// The header is present verbatim in the shipped extension code and was
// observed on the wire during dynamic analysis, from both the background
// service worker and a live content-script context.
const authHeader =
"Basic OTEzOTkxOnFlYmpaWF9DOGNRY0lxSHA4WTVjNGxzU1pRSlM2VW9MMExzTF9tbWs=";
const base64Credential = authHeader.replace(/^Basic\s+/, "");
const decoded = Buffer.from(base64Credential, "base64").toString("utf8");
const [accountId, licenseKey] = decoded.split(":");
console.log("Authorization header (as sent on the wire):");
console.log(" " + authHeader);
console.log();
console.log("Decoded credential (account_id:license_key):");
console.log(" " + decoded);
console.log();
console.log("MaxMind account ID: " + accountId);
console.log("MaxMind license key: " + licenseKey);
console.log();
console.log("Note: MaxMind currently returns HTTP 401 AUTHORIZATION_INVALID for this");
console.log("credential, so it no longer authenticates a real lookup. The outbound");
console.log("request -- and the IP address it discloses to MaxMind -- still happens");
console.log("every time the extension's country-detection code runs, regardless of");
console.log("whether MaxMind accepts the credential.");
- 1node maxmind-basic-auth-decode.js
What it can do
Permissions this extension asks for, as declared in version 0.10.0. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on every site you visit
*://*/* and 1 more
Store data in your browser
storage
Store an unlimited amount of data in your browser
unlimitedStorage
Watch every request your browser makes
webRequest
Where it sends data
Destinations our analysis observed JSON Formatter contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- cdn.givefreely.com
JSON Formatter sends data to cdn.givefreely.com. 5 other extensions we have analysed send data here.
- wild.link
JSON Formatter sends data to wild.link. 2 other extensions we have analysed send data here.
- geoip.maxmind.com
JSON Formatter sends data to geoip.maxmind.com. One other extension we have analysed sends data here.