Is Hover Zoom+ safe?
HoverZoom is medium risk. On any Google domain, an injected script replaces XHR.prototype.open. Every XHR response feeds one sessionStorage entry, hoverZoomXHROpenData, unfiltered, so autocomplete, account, and search responses accumulate. Active on google.com.…
Who publishes itHoverZoom+ - no other listings under this identity
HoverZoom+ - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
All XHR Response Bodies Accumulated in sessionStorage on Google Domains
On any Google domain, an injected script replaces XHR.prototype.open.
Every XHR response feeds one sessionStorage entry, hoverZoomXHROpenData, unfiltered, so autocomplete, account, and search responses accumulate.
Active on google.com.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You visit any Google domain (google.com, google.co.uk, and roughly 150 other ccTLDs).
The hook runs on page load; no explicit user action is needed beyond navigation.
The extension replaces XMLHttpRequest.prototype.open so that every XHR response body is appended to sessionStorage['hoverZoomXHROpenData'] before any other page code can read it.
There is no URL filter, all XHR responses on the page, including account API calls, accumulate in the same storage key until the tab closes.
The XHR hook script injected into every Google page (js/hoverZoomXHROpen.js):
// Injected as a <script> tag into every google.* page.// Replaces XMLHttpRequest.prototype.open — affects ALL XHR on the page,// not just extension-initiated ones.if (typeof oldXHROpen !== 'function') { // guard: hook only once per tab oldXHROpen = window.XMLHttpRequest.prototype.open; // save original window.XMLHttpRequest.prototype.open = function(method, url, async, user, password) { // Register a listener before the request starts: this.addEventListener('load', function() { try { // No URL check — captures ALL XHR responses: let existing = sessionStorage.getItem('hoverZoomXHROpenData'); if (existing == null) { sessionStorage.setItem('hoverZoomXHROpenData', this.responseText); } else { // Concatenate indefinitely; only resets if storage is full: sessionStorage.setItem('hoverZoomXHROpenData', existing + this.responseText); } } catch {} }); // Proceed with the original open() call: return oldXHROpen.apply(this, arguments); };}XHR responses for one Google session: autocomplete, search snippets, account JSON. Grows until the 5MB cap, then only the latest is kept.
- Location
- sessionStorage key 'hoverZoomXHROpenData' (set in the page's own storage by the injected script)
{"app":{"client_id":"chrome_ext"},"features":{"suggest":true,"images":true},"images":[{"id":"BPbcgkAKO1TMCM","url":"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcT...","source":"https://example.com/photo.jpg","w":800,"h":600}]}{"items":[{"id":"autocomplete-0","content":"weather today"}]}Vinted Listing API Responses Intercepted and Stored in sessionStorage
On Vinted, an injected script replaces fetch and XHR.open.
Responses from /api/v2/catalog/items and /api/v2/homepage/ (photos/blocks key) are stored as hzVintedApiData.
Active on vinted.com; feeds the zoom plugin via a CustomEvent bridge.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You browse the Vinted catalog or homepage.
The hook activates on any vinted.com page where the extension's content script runs.
The extension replaces window.fetch and XMLHttpRequest.prototype.open so that responses from Vinted's listing API paths are stored in sessionStorage['hzVintedApiData'].
Responses are captured if they come from /api/v2/catalog/items or /api/v2/homepage/ and contain the string '"photos"' or '"blocks"' in the response body.
The fetch and XHR intercept hooks in hoverZoomVintedFetch.js (shipped source):
Annotated: both fetch and XHR are replaced to capture Vinted API responses
// Both window.fetch and XMLHttpRequest.prototype.open are replaced// so the extension can intercept responses before Vinted's own JS processes them.// Filter: only catalog and homepage API paths are intercepted:function shouldIntercept(url) { return url.includes('/api/v2/catalog/items') || url.includes('/api/v2/homepage/');}// Storage helper: overwrites (does not append) sessionStorage on each intercepted response:function store(text) { sessionStorage.setItem('hzVintedApiData', text); // latest response only document.dispatchEvent(new CustomEvent('hzVintedApiData')); // notify content script}// Fetch hook — response is cloned so Vinted's own .then() still resolves:window.fetch = function(...args) { const url = (typeof args[0] === 'string') ? args[0] : (args[0] && args[0].url) || ''; const promise = originalFetch.apply(this, args); if (shouldIntercept(url)) { promise.then(resp => { resp.clone().text().then(text => { if (text.includes('"photos"') || text.includes('"blocks"')) { store(text); // captured: Vinted listing API payload } }); }); } return promise; // original promise returned unmodified};Last Vinted listing: item details, seller IDs, login names, prices, photo URLs, read by the zoom plugin, overwritten each response.
- Location
- sessionStorage key 'hzVintedApiData' on vinted.com
{ "items": [ { "id": 74821903, "user_id": 12847291, "title": "Levi's 501 Jeans W30 L32", "price": "15.00", "currency": "EUR", "photos": [ { "id": 291847302, "full_size_url": "https://images1.vinted.net/a1/2ba5a/1713445291,f800,eec5,6e52e,5d45f.jpeg", "url": "https://images1.vinted.net/tc/2ba5a/1713445291,f800,eec5,6e52e,5d45f.jpeg", "thumbnails": [ { "type": "thumb310x430", "url": "https://images1.vinted.net/tc/thumb310x430/2ba5a.jpeg" } ] } ], "user": { "id": 12847291, "login": "fashionresale_fr", "profile_url": "https://www.vinted.fr/member/12847291-fashionresale-fr" } } ], "pagination": { "current_page": 1, "total_pages": 5, "total_entries": 82 }}Hovering Vinted Homepage Listings Triggers Authenticated Wardrobe API Crawl
Hovering a Vinted homepage listing (URL has homepage_session_id) sends paginated GETs to /api/v2/wardrobe/{sellerId}/items with the session cookie, fetching each hovered seller's wardrobe. 5 requests seen after 5 hovers, 12-383 KB each.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You hover your mouse over a listing on the Vinted homepage feed.
Only homepage feed items trigger this path, the extension checks that the item's href contains 'homepage_session_id=' (vinted.js:161).
The extension sends paginated GET requests to /api/v2/wardrobe/{sellerId}/items with your Vinted session cookies, fetching all pages of the seller's inventory.
Requests use credentials:'include', so Vinted's server receives them as authenticated calls under your session. The fetched data is cached locally for the zoom gallery.
HTTP 200, JSON payload containing the seller's wardrobe items: seller user ID, login name, item IDs, titles, prices (EUR), photo URLs, and pagination metadata ({total_pages: N, total_entries: M}). Response size ranged from 12 KB to 383 KB in observed requests.
- Accept
- application/json, text/plain, */*,image/webp
- Cookie
- v_uid=12847291; _vinted_fr_session=<redacted>
- Cache-Control
- no-store
Wardrobe crawl: hover detect, URL build, authenticated fetch (vinted.js + hoverZoomVintedFetch.js)
Annotated end-to-end flow
// STEP 1 — vinted.js detects a homepage feed item on hover:var useWardrobeApi = itemHref.includes('homepage_session_id=') || location.pathname === '/';// homepage_session_id= only appears on items from the Vinted homepage feed.// STEP 2 — buildWardrobeApiUrl() constructs the paginated endpoint:function buildWardrobeApiUrl(userId, page) { // wardrobePageSize = 20 (hard-coded) return '/api/v2/wardrobe/' + userId + '/items?page=' + page + '&per_page=20&order=newest_first';}// STEP 3 — crawlWardrobe() paginates until complete:function crawlWardrobe(userId) { // Sends hzVintedWardrobeRequest CustomEvent to the page-world script. // Page-world script issues the actual fetch with session cookies: _origFetch.call(window, wardrobeUrl, { credentials: 'include', // <-- attaches the user's Vinted session cookies cache: 'no-store', headers: { Accept: 'application/json, text/plain, */*,image/webp' } }); // Response is stored in plug._wardrobeCatalogs[userId] for the zoom gallery. // Crawl continues to the next page until state.complete === true.}- Seller user ID12847291
The numeric Vinted user ID of the seller whose wardrobe was fetched.
- Seller login namefashionresale_fr
The seller's visible Vinted username.
- Item listing URLshttps://www.vinted.fr/items/74821903-levis-501-jeans
Full URLs to every item in the seller's wardrobe, not just the one you hovered over.
- Item prices15.00 EUR
Price and currency for each listed item.
- Photo URLshttps://images1.vinted.net/a1/2ba5a/1713445291,f800.jpeg
Full-size and thumbnail photo URLs for every item in the wardrobe.
- Wardrobe size82 items across 5 pages
Total number of items the seller has listed (via pagination metadata).