Is Vinted Checker safe?
Vinted Checker sends the full URL and page content of every product page you visit to its own servers, contradicting its privacy policy.
On any e-commerce product page across the web, Vinted Checker automatically collects the page's full URL along with cleaned HTML (meta tags, product data, and visible text, up to 50KB) and sends it to api.vintedchecker.com for analysis. This happens with no opt-out, on a separate code path from the extension's telemetry opt-out setting. The extension's bundled privacy policy explicitly states it does not collect browsing history and only sends page domains, not full URLs — directly contradicting this behavior.
Who publishes itNo other listings under this identity, 3 shared hostnames
No other listings under this identity, 3 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 3 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
VintedFinder sends your full page URL and content it denies collecting
Visiting a product page anywhere on the web makes VintedFinder upload the full page URL and cleaned page text to its own server.
Its bundled privacy policy explicitly says the full URL is never sent, only the domain.
You open a product page on an online store.
Any page with an add-to-cart button or JSON-LD product data qualifies.
The extension uploads the page's full URL and cleaned content to its own server.
This fires automatically a few seconds after load, with no click required.
| Field | Value | Why it matters | |
|---|---|---|---|
Full page address | https://www.zara.com/us/en/ribbed-knit-midi-dress-p04321.html | Every specific page you visit is logged, not only the site's domain. | |
Cleaned page content | <!-- META -->\n<meta property=\"og:title\" content=\"Ribbed Knit Midi Dress\"> | Visible product text, meta tags and structured data copied from the page you're viewing. | |
Detected product details | {\"brand\":\"Zara\",\"price\":49.9,\"currency\":\"USD\",\"color\":\"Black\"} | Brand, price, color and size shown on the page, tied to the URL above. | |
Browser language | en-US | Your browser's interface language, sent along with every request. |
PRIVACY.md, shipped inside the extension, states the full page URL is never sent, only the domain, and that no browsing data is collected. It never mentions the analyze-product or extract-product requests at all.
The unguarded upload path, from page snapshot to POST
function cleanHtmlForBackend() {
const startTime = performance.now();
const parts = [];
const metaTags = document.querySelectorAll(
'meta[property^="og:"], meta[property^="product:"], meta[name="description"], meta[name="keywords"], meta[name="brand"], meta[itemprop]'
);
if (metaTags.length > 0) {
const metaHtml = Array.from(metaTags).map((el) => el.outerHTML).join("\n");
parts.push(`<!-- META -->
${metaHtml}`);
}
const jsonLdScripts = document.querySelectorAll('script[type="application/ld+json"]');
if (jsonLdScripts.length > 0) {
const jsonLdHtml = Array.from(jsonLdScripts).map((el) => {
try {
const parsed = JSON.parse(el.textContent || "");
return `<script type="application/ld+json">${JSON.stringify(parsed)}<\/script>`;
} catch {
return "";
}
}).filter(Boolean).join("\n");
if (jsonLdHtml) {
parts.push(`<!-- JSON-LD -->
${jsonLdHtml}`);
}
}
const clone = document.body.cloneNode(true);
for (const selector of REMOVE_SELECTORS) {
try {
clone.querySelectorAll(selector).forEach((el) => el.remove());
} catch {
}
}
const walker = document.createTreeWalker(clone, NodeFilter.SHOW_COMMENT);
const comments = [];
while (walker.nextNode()) {
comments.push(walker.currentNode);
}
comments.forEach((c) => c.parentNode?.removeChild(c));
clone.querySelectorAll('[style*="display:none" i], [style*="display: none" i], [hidden], [aria-hidden="true"]').forEach((el) => el.remove());
clone.querySelectorAll("*").forEach((el) => {
const attrs = Array.from(el.attributes);
for (const attr of attrs) {
if (attr.name.startsWith("on") || attr.name.startsWith("data-") && !["data-product", "data-sku", "data-brand", "data-price", "data-product-id", "data-product-title", "data-product-name"].includes(attr.name.toLowerCase())) {
el.removeAttribute(attr.name);
}
}
});
const productZones = [];
const productSelectors = [
"h1",
"h2",
'[class*="breadcrumb" i]',
'[class*="product-title" i]',
'[class*="product-name" i]',
'[class*="product-info" i]',
'[class*="pdp" i]',
'[class*="price" i]',
'[class*="brand" i]',
'[itemprop="name"]',
'[itemprop="brand"]',
'[itemprop="price"]',
'[itemprop="description"]',
'[itemprop="offers"]',
"[data-product]",
"[data-sku]",
"[data-brand]"
];
const seen = /* @__PURE__ */ new Set();
for (const selector of productSelectors) {
try {
clone.querySelectorAll(selector).forEach((el) => {
if (!seen.has(el)) {
seen.add(el);
const html = el.outerHTML;
if (html.length < 5e3) {
productZones.push(html);
} else {
productZones.push(html.substring(0, 5e3));
}
}
});
} catch {
}
}
if (productZones.length > 0) {
parts.push(`<!-- PRODUCT ZONES -->
${productZones.join("\n")}`);
}
const productZonesSize = productZones.join("").length;
if (productZonesSize < 500) {
let bodyHtml = clone.innerHTML;
bodyHtml = bodyHtml.replace(/\s+/g, " ").trim();
if (bodyHtml.length > 1e4) {
bodyHtml = bodyHtml.substring(0, 1e4);
}
parts.push(`<!-- BODY FALLBACK -->
${bodyHtml}`);
}
let result = parts.join("\n\n");
const encoder = new TextEncoder();
let encoded = encoder.encode(result);
if (encoded.byteLength > MAX_OUTPUT_BYTES) {
let low = 0;
let high = result.length;
while (low < high) {
const mid = Math.floor((low + high + 1) / 2);
if (encoder.encode(result.substring(0, mid)).byteLength <= MAX_OUTPUT_BYTES) {
low = mid;
} else {
high = mid - 1;
}
}
result = result.substring(0, low);
}
const elapsed = performance.now() - startTime;
console.debug(`[HtmlCleaner] Cleaned HTML: ${encoder.encode(result).byteLength} bytes in ${elapsed.toFixed(1)}ms`);
return result;
} async refine(cleanedHtml, url, initialExtraction) {
this.abort();
const controller = new AbortController();
this.pendingRequest = controller;
const requestBody = {
cleanedHtml,
url,
language: chrome.i18n.getUILanguage(),
initialExtraction: {
brand: initialExtraction.brand || void 0,
title: initialExtraction.name || void 0,
price: initialExtraction.price,
currency: initialExtraction.currency,
color: initialExtraction.color,
size: initialExtraction.size,
confidence: initialExtraction.confidence,
strategy: initialExtraction.strategy
}
};
try {
debug("\u{1F504} BackendRefinement: Sending analysis request...");
const startTime = performance.now();
const response = await this.sendViaBackground(requestBody, controller.signal);
const elapsed = performance.now() - startTime;
debug(`\u2705 BackendRefinement: Response in ${elapsed.toFixed(0)}ms`, response);
if (!response || !response.success) {
debugWarn("\u26A0\uFE0F BackendRefinement: Backend returned error", response);
return null;
}
return response.data;
} catch (error) {
if (error.name === "AbortError") {
debug("\u23F9\uFE0F BackendRefinement: Request aborted");
} else {
debugWarn("\u26A0\uFE0F BackendRefinement: Request failed", error);
}
return null;
} finally {
if (this.pendingRequest === controller) {
this.pendingRequest = null;
}
}
} async function analyzeProduct(payload) {
try {
const response = await fetch(`${API_BASE_URL}/api/analyze-product`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (!response.ok) {
return { success: false, error: `API error: ${response.status}` };
}
const data = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: String(error) };
}
}- api.vintedchecker.com
VintedFinder's own backend. Receives the full page URL, cleaned page HTML and detected product fields from every matching page.
Hooks fetch inside VintedFinder's own background service worker so you can see the analyze-product request body, including your page URL, before it leaves your device.
(function () {
const TARGET_HOST = "api.vintedchecker.com";
const TARGET_PATH = "/api/analyze-product";
const originalFetch = self.fetch;
self.fetch = function (input, init) {
try {
const url = typeof input === "string" ? input : (input && input.url) || "";
if (url.includes(TARGET_HOST) && url.includes(TARGET_PATH)) {
const body = init && init.body ? init.body : null;
console.warn("[VintedFinder detector] outgoing analyze-product request", {
url,
bodyPreview: typeof body === "string" ? body.slice(0, 800) : body
});
}
} catch (err) {
console.error("[VintedFinder detector] hook error", err);
}
return originalFetch.apply(this, arguments);
};
console.log("[VintedFinder detector] hooked self.fetch, watching for " + TARGET_HOST + TARGET_PATH);
})();
- 1In chrome://extensions, enable Developer mode, click 'service worker' under VintedFinder, paste this in that console, then visit a product page.
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.
What it can do
Permissions this extension asks for, as declared in version 3.4.3. 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 api.vintedchecker.com
https://api.vintedchecker.com/*
Read and change your data on www.vinted.fr
https://www.vinted.fr/*
Read and change your data on www.vinted.com
https://www.vinted.com/*
Read and change your data on www.google-analytics.com
https://www.google-analytics.com/*
Store data in your browser
storage
Where it sends data
Destinations our analysis observed Vinted Checker 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.
- api.vintedchecker.com
Vinted Checker sends data to api.vintedchecker.com. No other extension we have analysed sends data here.