Is AVG SafePrice safe?
AVG SafePrice is medium risk. AVG SafePrice injects a script into every page overwriting navigation functions, logging every page visited, even untyped ones. Dynamic analysis caught it reporting Amazon transitions to AVG's worker. Onward transmission is off, changeable.
Who publishes itGen Digital - 17 other listings from the same operator, none carrying a finding
Gen Digital - 17 other listings from the same operator, none carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
1 other listing published from this account, 500k+ users between them, none of them carrying a finding.
Same operator - 16 listings
Published under a different store account, but sharing the registered address, contact or declared legal entity this one gave the store.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
MAIN-World Script Watches Every URL You Navigate To
AVG SafePrice injects a script into every page overwriting navigation functions, logging every page visited, even untyped ones.
Dynamic analysis caught it reporting Amazon transitions to AVG's worker.
Onward transmission is off, changeable.
You load or navigate to any page over HTTP or HTTPS.
Fires on chrome.webNavigation.onCommitted for the top frame of any http:// or https:// tab -- there is no check for a shopping/merchant domain before this fires.
The extension injects a script into the page's own JavaScript context that watches every way you can navigate away from it.
chrome.scripting.executeScript loads redirectionChainSiteScript.js with world:'MAIN', giving it the same JS context the page itself runs in, rather than the isolated content-script sandbox most extensions use.
Unconditional injection on every navigation, and the navigation-API patches it installs
const REDIRECTION_CHAIN_SCRIPT = "redirectionChainSiteScript.js";
class RedirectionChainSiteScriptInjector {
constructor(logger) {
this.logger = logger;
this.started = false;
}
start() {
if (this.started) return;
this.started = true;
// Registered on chrome.webNavigation.onCommitted -- fires for every
// committed navigation in every tab, not just shopping sites.
webNavigationOnCommitted.addListener((navDetails) => {
if (navDetails.tabId < 0 || navDetails.frameId !== 0) return;
const url = navDetails.url;
if (url && (url.startsWith("http://") || url.startsWith("https://"))) {
// No allowlist check against a merchant domain list here --
// any committed http/https top-frame navigation qualifies.
chrome.scripting
.executeScript({
siteScriptPath: [REDIRECTION_CHAIN_SCRIPT],
tabId: navDetails.tabId,
frameIds: [0],
world: "MAIN", // runs in the page's own JS context, not the isolated content-script world
})
.catch((err) => {
this.logger.debug(`Failed to inject ${REDIRECTION_CHAIN_SCRIPT} into tab ${navDetails.tabId}:`, err);
});
}
});
}
}const HOOK_FLAG = "__centlyRdSiteHookInstalled__";
const win = window;
if (win[HOOK_FLAG]) return; // only patch once per page
function reportHop(payload) {
const message = { source: "cently-rd-site", payload };
try {
window.postMessage(message, "*");
} catch (e) {
try {
window.postMessage(JSON.parse(JSON.stringify(message)), "*");
} catch (e2) {}
}
}
function captureStack() {
const { stack } = new Error();
if (stack) return stack.split("\n").slice(2, 8).join("\n");
}
function resolveUrl(value) {
if (value == null) return;
try {
return new URL(value.toString(), location.href).href;
} catch (e) {
return value.toString();
}
}
win[HOOK_FLAG] = true;
const locationProto = Location.prototype;
// Patch location.assign / .replace / .reload
function patchLocationMethod(methodName, label) {
const original = locationProto[methodName];
if (typeof original !== "function") return;
const patched = function (...args) {
const targetUrl = methodName === "reload" ? location.href : (resolveUrl(args[0]) ?? location.href);
reportHop({ kind: "js_location_change", url: location.href, targetUrl, method: label, stack: captureStack() });
return original.apply(this, args);
};
Object.defineProperty(locationProto, methodName, { value: patched, writable: true, configurable: true });
}
patchLocationMethod("assign", "assign");
patchLocationMethod("replace", "replace");
patchLocationMethod("reload", "reload");
// Patch location.href / pathname / hash / search / host / hostname / port / protocol setters
const locationProps = ["href", "pathname", "hash", "search", "host", "hostname", "port", "protocol"];
for (const prop of locationProps) {
const descriptor = Object.getOwnPropertyDescriptor(locationProto, prop);
if (!descriptor || typeof descriptor.set !== "function") continue;
const originalGet = descriptor.get;
const originalSet = descriptor.set;
Object.defineProperty(locationProto, prop, {
configurable: true,
enumerable: descriptor.enumerable,
get: originalGet,
set(value) {
const targetUrl = prop === "href" ? (resolveUrl(value) ?? value) : (() => {
try {
const u = new URL(location.href);
u[prop] = value;
return u.href;
} catch (e) {
return value;
}
})();
reportHop({ kind: "js_location_change", url: location.href, targetUrl, method: prop, stack: captureStack() });
return originalSet.call(this, value);
},
});
}
// Patch window.location assignment itself
try {
const proto = Object.getPrototypeOf(window);
const descriptor = (proto && Object.getOwnPropertyDescriptor(proto, "location")) || Object.getOwnPropertyDescriptor(window, "location");
if (descriptor && typeof descriptor.set === "function") {
const originalSet = descriptor.set;
Object.defineProperty(window, "location", {
configurable: true,
enumerable: descriptor.enumerable ?? true,
get: descriptor.get,
set(value) {
reportHop({ kind: "js_location_change", url: location.href, targetUrl: resolveUrl(value) ?? value, method: "window_location_setter", stack: captureStack() });
return originalSet.call(window, value);
},
});
}
} catch (e) {}
// Patch window.open, history.pushState, history.replaceState
const originalOpen = window.open;
function patchHistoryMethod(methodName) {
const original = history[methodName];
history[methodName] = function (state, title, url) {
reportHop({ kind: "history_state_change", url: location.href, method: methodName, targetUrl: resolveUrl(url ?? undefined), stack: captureStack() });
return original.call(this, state, title, url);
};
}
if (typeof originalOpen === "function") {
window.open = function (url, target, features) {
reportHop({ kind: "js_window_open", url: location.href, targetUrl: resolveUrl(url), target, features, stack: captureStack() });
return originalOpen.call(window, url, target, features);
};
}
patchHistoryMethod("pushState");
patchHistoryMethod("replaceState");| Field | Value | Why it matters | |
|---|---|---|---|
Page you were on | https://www.amazon.com/s?k=usb+cable | The full URL of the page you were viewing right before the navigation happened. | |
Page you're going to | https://www.amazon.com/dp/B08JPMTV2S (illustrative -- exact captured product URL not quoted in the DA evidence record) | The full URL you're navigating to, resolved to an absolute URL even if the page only passed a relative path. | |
How the navigation happened | history_state_change (history.replaceState) | Which browser API triggered the report -- a location property set, history.pushState/replaceState, or window.open. |
- eb.nextgenshopping.com
Serves the live remote config, including the 'sendAnalytics' flag controlling onward transmission. Returned false in every test session, June-August 2026.
- c.nextgenshopping.com
Snowplow collector that would receive the assembled redirect-chain event if sendAnalytics is enabled remotely. No such event was observed; the flag was off in all tests.
We confirmed the injection and the API patching fire on ordinary navigation, and we confirmed the patched hooks capture real before/after URLs during a live browsing session. We did not observe the final step -- the assembled chain being sent to AVG's analytics collector -- because AVG's own server-side configuration currently returns sendAnalytics=false. That flag lives entirely on AVG's servers and can be changed at any time without a client-side update, so its current 'off' state is not a guarantee about how the extension behaves for all users or at all times.