Is Speed Dial [FVD] - New Tab Page, 3D, Sync... safe?
FVD Speed Dial is high risk. Every new tab, FVD Speed Dial reads up to 10,000 history entries from the last 60 days, checking them against sponsored dials' target sites; sites visited 4+ times qualify for a tile. Observed live. Not disclosed in the Store listing.…
Who publishes itAPPTITUDE LLC - 3 other listings from the same operator, none carrying a finding
APPTITUDE LLC - 3 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
3 other listings published from this account, 407k+ users between them, none of them carrying a finding.
Shared hosts - 8 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
New-Tab Extension Scans 60 Days of Browsing History to Pick Sponsored Tiles
Every new tab, FVD Speed Dial reads up to 10,000 history entries from the last 60 days, checking them against sponsored dials' target sites; sites visited 4+ times qualify for a tile.
Observed live.
Not disclosed in the Store listing.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You open a new tab.
The extension reads up to 10,000 browser history entries from the last 60 days and uses them, on-device, to decide which sponsored 'Recommended' tiles to show you.
We observed this during dynamic analysis: the extension's own console logging showed chrome.history.search firing with a 60-day lookback and returning real history entries right after the sponsored-dial catalog was fetched.
History is read, then matched against each dial's target host with a minimum-visits threshold:
Reading up to 10,000 history entries from the last 60 days
js/bg/serverdialsModule.jsconst history = { // ... search: function (startTime, query, cb) { chrome.history.search( { text: query, startTime: startTime, maxResults: 10000, }, function (items) { debug.log('Search all items in the history for', query, 'found', items.length); cb(null, items); } ); },};function HistorySearch() { this.fetchHistoryFor = config.fetchHistoryFor; this.historyData = '';}HistorySearch.prototype.getMinHistoryTime = function () { return new Date().getTime() - this.fetchHistoryFor;};HistorySearch.prototype.init = function (cb) { const self = this; const minHistoryTime = this.getMinHistoryTime(); debug.log('Fetch history since', new Date(minHistoryTime)); history.search(minHistoryTime, '', function (err, items) { if (err) { return cb(err); } const lines = items.map(function (item) { return item.url + '|' + item.visitCount; }); debug.log('History data URLs count:', lines.length); self.historyData = lines.join('\n').toLowerCase(); debug.log('History data size:', self.historyData.length / 1024 / 1024, 'MB'); cb(null); });};Matching each candidate dial's site against your local history (4+ visits qualifies)
js/bg/serverdialsModule.jsHistorySearch.prototype.getVisitsCountBySign = function (sign) { const regExp = new RegExp(escapeStringRegexp(sign) + '[^\\|]+\\|([0-9]+)', 'ig'); let m; let visits = 0; do { m = regExp.exec(this.historyData); if (m) { visits += parseInt(m[1]); } } while (m); return visits;};HistorySearch.prototype.getChoices = function (dialsChoices, cb) { const self = this; const choices = []; const startLookUpTime = new Date().getTime(); dialsChoices.forEach(function (dialChoice) { if (!dialChoice.checkHost) { return; } const visitsCount = self.getVisitsCountBySign(dialChoice.checkHost); if (visitsCount >= config.minVisits) { // this url was found in history, add dial for this url debug.log('Host', dialChoice.checkHost, 'found in the history, add choice', dialChoice); dialChoice.orderValue = visitsCount; choices.push(dialChoice); } }); setTimeout(function () { cb(null, choices); });};- Pages you visitedhttps://www.homedepot.com/s/patio+furniture|6
Every history entry's URL, pulled from your browser history and matched against each sponsored dial's target site.
- History window scannedLast 60 days
How far back the extension looks in your browsing history on every scan.
- Entries read per scanUp to 10,000 entries
The maximum number of history rows pulled into memory each time the match runs.
- Visit threshold for sponsored tile4 visits
A dial's target site only becomes eligible to show as 'Sponsored' once you've visited it at least this many times in the window.
Observed during dynamic analysis: this request returned IP-derived geolocation (country code, region, city) immediately after the sponsored-dial catalog was fetched. The response location is passed into the same dial-picking flow that reads your history, to filter which dials are eligible for your location.
- fvdspeeddial.com
Serves the catalog of candidate sponsored/recommended dials for the new tab page. The request carries a userType parameter and, when set, a country parameter, not raw history.
- geoloc.tempest.com
Returns IP-derived geolocation (country, region, city) used to filter which sponsored dials are eligible for your location.
IP and city sent to geoloc.tempest.com on every new session
Two GET requests to https://geoloc.tempest.com/ fired on new session start, replying with country/city/lat/lng from your IP, stored in chrome.storage.session and attached to every Analytics event.
Not named in the Store listing.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You open the new tab page after 30 minutes of inactivity, starting a new session.
The extension fetches https://geoloc.tempest.com/ over your connection. tempest.com sees your IP and replies with country, city, and coordinates, stored and attached to analytics.
tempest.com is a separate company; it is not named in the extension's Chrome Web Store listing.
200 OK, application/json: {"cc":"GB","CityName":"Brighton","lat":50.83,"lng":-0.19}
- Host
- geoloc.tempest.com
- Accept
- */*
- User-Agent
- Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
- Your IP address82.46.0.0/16 (UK residential ISP)
tempest.com sees the request's source IP, which is how it knows your country/city; the IP isn't a body field, it's the connection.
- Country codeGB
Two-letter country code returned in the response and stored locally.
- City nameBrighton
Best-guess city from the IP. Forwarded to Google Analytics on every later event.
- Latitude / longitude50.83, -0.19
Approximate coordinates derived from the IP, accurate to a city or neighbourhood. Stored in session memory.
The code that does this, from the extension's shipping source.
The same code, lightly annotated
// js/bg/google-analytics.js — runs whenever a new session beginsasync getOrCreateSessionId() { let { sessionData } = await chrome.storage.session.get('sessionData'); // ... if expired or missing, fall through to fetch ... if (!sessionData) { // Single GET to tempest.com — your IP is the lookup key. const clientGeolocationResult = await fetch('https://geoloc.tempest.com'); const clientGeolocation = await clientGeolocationResult.json(); // {cc:'GB', CityName:'Brighton', lat:50.83, lng:-0.19} sessionData = { session_id: Date.now().toString(), timestamp: Date.now().toString(), geolocation: JSON.stringify(clientGeolocation), }; await chrome.storage.session.set({ sessionData }); } return sessionData.session_id;}Held in session memory (cleared on Chrome quit) and re-attached to every Analytics event sent over the next 30 minutes of activity.
- Location
- chrome.storage.session, key 'sessionData'
{ "sessionData": { "timestamp": "1744640329122", "session_id": "1744640329122", "geolocation": "{\"cc\":\"GB\",\"CityName\":\"Brighton\",\"lat\":50.83,\"lng\":-0.19}" }}- geoloc.tempest.com
Receives a GET each new session and dial-picker refresh; logs see your IP. Returns country, city, lat/lng. Owned by Tempest, not named in the Store listing.
- www.google-analytics.com
Receives the city and country (derived above) on every later GA4 event. See companion claim 'GA4 telemetry on install/update/dial click'.
Run in the Speed Dial [FVD] service worker DevTools console. Hooks fetch() so any call to https://geoloc.tempest.com is logged with timestamp and the JSON body returned. Confirms the lookup happens and shows you the exact data tempest.com handed back about your connection.
- Chrome with Developer mode enabled
- Speed Dial [FVD] installed
// speed-dial-geoloc-watcher.js// Paste into the service worker DevTools console for Speed Dial [FVD].(function () { const orig = self.fetch.bind(self); self.fetch = async function (input, init) { const url = typeof input === 'string' ? input : input.url; const isGeo = url && url.includes('geoloc.tempest.com'); if (isGeo) { console.log('[geoloc-watcher]', new Date().toISOString(), 'GET', url); } const res = await orig(input, init); if (isGeo) { try { const clone = res.clone(); const body = await clone.text(); console.log('[geoloc-watcher] response body:', body); } catch (e) { console.warn('[geoloc-watcher] could not read body', e); } } return res; }; console.log('[geoloc-watcher] installed. Open a new tab after 30+ min idle, or run: chrome.storage.session.remove("sessionData")');})();- 1Visit chrome://extensions, enable Developer mode.
- 2Find 'Speed Dial [FVD]', click Service worker.
- 3Paste the script, press Enter.
- 4Clear sessionData, open a new tab.
- 5Watch console for the GET and JSON.