Is Vinted Checker safe?

Medium risk

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.

45Risk
Who publishes it

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.

vinted.hr
Also called by 1 other listing: Asify
vinted.dk
Also called by 2 other listings: Asify, Convert Label
vinted.fi
Also called by 3 other listings, including Asify, Convert Label

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI FOUND

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

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 did this

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.

02EvidenceFIELD TABLE
What the analyze-product request contains
FieldValueWhy it matters
Full page address
https://www.zara.com/us/en/ribbed-knit-midi-dress-p04321.htmlEvery 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-USYour browser's interface language, sent along with every request.
03EvidencePLAIN NOTE
What the privacy policy promises

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.

04EvidenceCODE COMPARE
The code that does this

The unguarded upload path, from page snapshot to POST

What it actually does
Building the page snapshotdist/extension/content.js:2752
  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;
  }
Sending it from the content scriptdist/extension/content.js:2586
    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;
        }
      }
    }
Posting it from the background service workerdist/extension/background.js:171
  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) };
    }
  }
05EvidenceTHIRD PARTY LIST
Where the page data goes
  • api.vintedchecker.com

    VintedFinder's own backend. Receives the full page URL, cleaned page HTML and detected product fields from every matching page.

06EvidenceARTIFACT
Check if you're affected

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.

RequiresChrome DevToolschrome://extensions Developer mode enabled
vintedfinder-request-detector.js · js
(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);
})();
How to run it
  1. 1
    In chrome://extensions, enable Developer mode, click 'service worker' under VintedFinder, paste this in that console, then visit a product page.
07EvidencePLAIN NOTE
Observation

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.

Updated 30 September 2026adnlgjmdhnfdekcdhcabklolhcdhkphm