Is Homey: Productivity New Tab safe?
Homey is medium risk. The extension sets/reads a recommendations UUID, sent to homey.place in a GET: homey.place/api/discovery/collection?lang=en&uid=36d8dbb5-afe7-4fd5-a0d1-7b8d838b6fef. Created once, appended with the interface language each load.…
Who publishes itAlexander Ostapenko - no other listings under this identity, 1 shared hostname
Alexander Ostapenko - no other listings under this identity, 1 shared hostname
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 1 hostname
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
Recommendation requests reuse a UUID
The extension sets/reads a recommendations UUID, sent to homey.place in a GET: homey.place/api/discovery/collection?lang=en&uid=36d8dbb5-afe7-4fd5-a0d1-7b8d838b6fef.
Created once, appended with the interface language each load.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You load the recommendation panel in the new-tab page.
The extension sends a stored recommendations UUID to its server with the language setting.
- Recommendations UUID36d8dbb5-afe7-4fd5-a0d1-7b8d838b6fef
Lets the server recognize the same browser profile across recommendation requests.
- Interface languageen
Tells the server what language to use for recommendation content.
- Storage keyrecommendations.recommendationsUid
Shows where the extension keeps the reusable identifier on your device.
The identifier is kept locally and reused the next time recommendations are requested.
- Location
- chrome.storage.local wrapper 'recommendations'
{ "recommendations": null, "recommendationsUid": "36d8dbb5-afe7-4fd5-a0d1-7b8d838b6fef", "suggestionsTimestamp": null, "recommendationsTimestamp": null}UUID creation, storage, and recommendation request
Stored recommendations UUID
static/js/background.jsfunction getRecommendationsStorage() { return new __WEBPACK_IMPORTED_MODULE_1__components_Storage_chromeStorage__["a" /* default */]('recommendations', { recommendations: null, recommendationsUid: null, recommendationsTimestamp: null, suggestionsTimestamp: null });}var getRecommendationUid = function () { var _ref = _asyncToGenerator( /*#__PURE__*/__WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.mark(function _callee() { var recommendationsStorage; return __WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return getRecommendationsStorage(); case 2: recommendationsStorage = _context.sent; if (recommendationsStorage.data.recommendationsUid) { _context.next = 7; break; } recommendationsStorage.data.recommendationsUid = Object(__WEBPACK_IMPORTED_MODULE_2_uuid__["a" /* v4 */])(); _context.next = 7; return recommendationsStorage.save(); case 7: return _context.abrupt("return", recommendationsStorage.data.recommendationsUid); case 8: case "end": return _context.stop(); } } }, _callee, this); })); return function getRecommendationUid() { return _ref.apply(this, arguments); };}();Recommendation load uses the UUID
static/js/background.jsvar loadRecommendations = function () { var _ref4 = _asyncToGenerator( /*#__PURE__*/__WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.mark(function _callee4() { var uid, recommendationsStorage, lang, recommendation; return __WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: _context4.next = 2; return getRecommendationUid(); case 2: uid = _context4.sent; _context4.next = 5; return getRecommendationsStorage(); case 5: recommendationsStorage = _context4.sent; lang = __WEBPACK_IMPORTED_MODULE_5__components_app_chromeHelper__["a" /* default */].instance().i18n.getMessage("locale"); _context4.next = 9; return __WEBPACK_IMPORTED_MODULE_3__api_recommendations__["getCollection"](uid, lang); case 9: recommendation = _context4.sent; if (!(!!recommendation && (typeof recommendation === "undefined" ? "undefined" : _typeof(recommendation)) === "object" && !!recommendation.collection && _typeof(recommendation.collection) === "object" && recommendation.collection.recommendations)) { _context4.next = 19; break; } _context4.next = 13; return getRecommendationsStorage(); case 13: recommendationsStorage = _context4.sent; recommendationsStorage.data.recommendations = recommendation.collection; recommendationsStorage.data.recommendationsTimestamp = Date.now(); _context4.next = 18; return recommendationsStorage.save(); case 18: return _context4.abrupt("return", recommendation.collection); case 19: _context4.next = 21; return getRecommendationsStorage(); case 21: recommendationsStorage = _context4.sent; recommendationsStorage.data.recommendationsTimestamp = Date.now(); _context4.next = 25; return recommendationsStorage.save(); case 25: return _context4.abrupt("return", null); case 26: case "end": return _context4.stop(); } } }, _callee4, this); })); return function loadRecommendations() { return _ref4.apply(this, arguments); };}();GET request query construction
static/js/main.jsfunction getFetchUrl(args) { return "https://homey.place" + args.endpoint + (args.query ? '?' + __WEBPACK_IMPORTED_MODULE_1_query_string__["stringify"](args.query) : '');}var getCollection = function () { var _ref = _asyncToGenerator( /*#__PURE__*/__WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.mark(function _callee(uid, lang) { var response; return __WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return Object(__WEBPACK_IMPORTED_MODULE_1__helpers_apiRequestHelper__["b" /* default */])({ endpoint: '/api/discovery/collection', type: 'GET', query: { 'uid': uid, 'lang': lang } }); case 2: response = _context.sent; return _context.abrupt("return", response.json()); case 4: case "end": return _context.stop(); } } }, _callee, _this); })); return function getCollection(_x, _x2) { return _ref.apply(this, arguments); };}();- homey.place
Extension server receiving UUID-tagged discovery recommendation requests.
Amplitude events reuse a device ID
A POST to api2.amplitude.com/2/httpapi fired after the new-tab experience loaded.
Evidence recorded 10 Amplitude events under one persistent device ID, incl. session_start, set as deviceId with locale and subscription status attached.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You open the extension's new-tab page.
The extension sends analytics events that reuse the same device identifier.
- Analytics project keyc32b70ea091457a622de197e24fb3461
Ties the event batch to this extension's Amplitude project.
- Device identifier8fbba8c3-1f6c-4e5a-9c7b-2e04d5a6f901 (illustrative; captured evidence recorded prefix 8fbba8c3)
Lets analytics events from the same browser profile be connected over time.
- Event namessession_start, [Amplitude] Page Viewed, Welcome Dialog Started
Describes what happened in the new-tab page during the session.
- User localeen
Adds language or locale context to the analytics profile.
- Extension version4.27.2
Ties the events to the installed extension build.
- Account user IDuser_id_count=0
No account user ID value was observed in the captured batch.
Amplitude initialization uses a persistent device identifier
New-tab page initializer
static/js/main.jskey: 'initAmplitude',value: function () { var _ref31 = _asyncToGenerator( /*#__PURE__*/__WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.mark(function _callee31() { var analyticsEnabled, anonymousId, licenseType, subscriptionStatus, identifyEvent; return __WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.wrap(function _callee31$(_context31) { while (1) { switch (_context31.prev = _context31.next) { case 0: _context31.next = 2; return Object(__WEBPACK_IMPORTED_MODULE_67__helpers_analyticsHelper__["a" /* getAnalyticsEnabled */])(); case 2: analyticsEnabled = _context31.sent; if (analyticsEnabled) { _context31.next = 6; break; } __WEBPACK_IMPORTED_MODULE_59__amplitude_analytics_browser__["d" /* setOptOut */](true); return _context31.abrupt('return'); case 6: _context31.next = 8; return Object(__WEBPACK_IMPORTED_MODULE_67__helpers_analyticsHelper__["b" /* getOrCreateAnonymousId */])(); case 8: anonymousId = _context31.sent; _context31.next = 11; return __WEBPACK_IMPORTED_MODULE_59__amplitude_analytics_browser__["c" /* init */]("c32b70ea091457a622de197e24fb3461", undefined, { defaultTracking: { sessions: true, pageViews: true, formInteractions: false, fileDownloads: false }, deviceId: anonymousId, minIdLength: 1, appVersion: chrome.runtime.getManifest().version }); case 11: licenseType = Object(__WEBPACK_IMPORTED_MODULE_50__helpers_licenseHelper__["c" /* getLicenseType */])(); subscriptionStatus = Object(__WEBPACK_IMPORTED_MODULE_50__helpers_licenseHelper__["d" /* getSubscriptionStatus */])(); identifyEvent = new __WEBPACK_IMPORTED_MODULE_59__amplitude_analytics_browser__["a" /* Identify */](); identifyEvent.set('App Platform', "CHROME"); identifyEvent.set('User Locale', chrome.runtime.getManifest().current_locale); identifyEvent.set('License Type', licenseType); identifyEvent.set('Subscription Status', subscriptionStatus); __WEBPACK_IMPORTED_MODULE_59__amplitude_analytics_browser__["b" /* identify */](identifyEvent); Object(__WEBPACK_IMPORTED_MODULE_60__helpers_offerImpressionTracker__["a" /* initImpressionTracker */])(); case 20: case 'end': return _context31.stop(); } } }, _callee31, this); })); function initAmplitude() { return _ref31.apply(this, arguments); } return initAmplitude;}()Background initializer
static/js/background.jsvar amplitudeInit = function () { var _ref4 = _asyncToGenerator( /*#__PURE__*/__WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.mark(function _callee4() { var anonymousId; return __WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.wrap(function _callee4$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: _context4.next = 2; return getOrCreateAnonymousId(); case 2: anonymousId = _context4.sent; _context4.next = 5; return __WEBPACK_IMPORTED_MODULE_3__amplitude_analytics_browser__["c" /* init */]("c32b70ea091457a622de197e24fb3461", undefined, { minIdLength: 1, appVersion: chrome.runtime.getManifest().version, deviceId: anonymousId }).promise; case 5: case "end": return _context4.stop(); } } }, _callee4, this); })); return function amplitudeInit() { return _ref4.apply(this, arguments); };}();- api2.amplitude.com
Amplitude analytics endpoint receiving extension event batches with the persistent device identifier.
Embedded pages lose security headers
On install, the extension adds a rule for sub-frame requests, stripping CSP, CSP-report-only, X-Frame-Options, and report-to headers, setting Sec-Fetch-Dest to document; matches Google, YouTube, ChatGPT, Gemini, WhatsApp, TikTok, Instagram.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You install or update the extension.
The extension registers a browser rule that changes response headers for selected embedded pages.
- Frame-blocking headerX-Frame-Options
Helps sites decide whether their pages may be displayed inside another page.
- Content policy headercontent-security-policy
Helps sites control what content and scripts their pages may load.
- Report-only policy headercontent-security-policy-report-only
Lets a site test content policy changes and receive reports without enforcing the policy.
- Reporting headerreport-to
Tells the browser where policy and network reports should be sent.
- Request destination headerSec-Fetch-Dest: document
The extension also marks matching requests as document loads.
Dynamic rule and site list used for header modification
Installed dynamic rule
static/js/background.jschrome.runtime.onInstalled.addListener(_asyncToGenerator( /*#__PURE__*/__WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.mark(function _callee3() { var rules; return __WEBPACK_IMPORTED_MODULE_0_babel_runtime_regenerator___default.a.wrap(function _callee3$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: rules = [{ id: 1, priority: 1, action: { type: "modifyHeaders", requestHeaders: [{ header: "Sec-Fetch-Dest", operation: "set", value: "document" }], responseHeaders: [{ operation: "remove", header: "X-Frame-Options" }, { header: "report-to", operation: "remove" }, { header: "content-security-policy-report-only", operation: "remove" }, { header: "content-security-policy", operation: "remove" }] }, condition: { resourceTypes: ["sub_frame"], requestDomains: __WEBPACK_IMPORTED_MODULE_3__components_pageViewer_PageViewerSitesList__["a" /* default */].filter(function (item) { return item.modifyHeaders; }).map(function (site) { return new URL(site.url).host; }), initiatorDomains: [chrome.runtime.id] } }]; _context3.next = 3; return chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds: rules.map(function (r) { return r.id; }), addRules: rules }); case 3: case "end": return _context3.stop(); } } }, _callee3, _this);})));Panel sites filtered by modifyHeaders
static/js/main.jsvar sitesList = [{ id: 'google-translate', category: 'google', url: 'https://translate.google.com/', dropCache: false, modifyHeaders: true, "css": ["content-scripts/google-app.css"], title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_translateProduct")}, { id: 'google-drive', category: 'google', url: 'https://drive.google.com/', dropCache: false, modifyHeaders: true, "css": ["content-scripts/google-app.css"], title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_driveProduct")}, { id: 'google-keep', category: 'google', url: 'https://keep.google.com/', dropCache: false, modifyHeaders: true, "css": ["content-scripts/google-app.css"], title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_keepProduct")}, { id: 'google-maps', category: 'google', url: 'https://www.google.com/maps', dropCache: true, modifyHeaders: true, title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_mapsProduct")}, { id: 'google-news', category: 'google', url: 'https://news.google.com/', dropCache: false, modifyHeaders: true, "css": ["content-scripts/google-app.css"], title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_newsProduct")}, { id: 'google-photos', category: 'google', url: 'https://photos.google.com/', dropCache: false, modifyHeaders: true, "css": ["content-scripts/google-photos.css"], title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_photosProduct")}, { id: 'google-finance', category: 'google', url: 'https://www.google.com/finance', dropCache: false, modifyHeaders: false, "css": ["content-scripts/google-finance.css"], title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_financeProduct")}, { id: 'google-youtube', category: 'google', url: 'https://www.youtube.com/', dropCache: false, modifyHeaders: true, title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_youtubeProduct")}, { id: 'google-youtube-music', category: 'google', url: 'https://music.youtube.com/', dropCache: false, modifyHeaders: true, title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_youtubeMusicProduct")}, { id: 'aliexpress', category: 'shopping', url: 'https://s.click.aliexpress.com/e/_A9f4PI', dropCache: false, modifyHeaders: false, title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("sites_aliexpress")}, { id: 'chat-gpt', category: 'ai', url: 'https://chatgpt.com/', dropCache: false, modifyHeaders: true, title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("sites_chatGpt")}, { id: 'google-bard', category: 'ai', url: 'https://gemini.google.com/', dropCache: false, modifyHeaders: true, "css": ["content-scripts/google-app.css"], title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("googleProductsGrid_bardProduct")}, { id: 'whatsapp', category: 'chat', url: 'https://web.whatsapp.com/', dropCache: false, modifyHeaders: true, title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("sites_whatsapp")}, { id: 'tiktok', category: 'social', url: 'https://www.tiktok.com/', dropCache: false, modifyHeaders: true, title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("sites_tikTok")}, { id: 'instagram', category: 'social', url: 'https://www.instagram.com/', dropCache: false, modifyHeaders: true, title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("sites_instagram")}, { id: 'youtube-shorts', category: 'social', url: 'https://www.youtube.com/shorts', dropCache: false, modifyHeaders: true, title: __WEBPACK_IMPORTED_MODULE_0__app_chromeHelper__["a" /* default */].instance().i18n.getMessage("sites_youtubeShorts")}];- translate.google.com
- drive.google.com
- keep.google.com
- news.google.com
- photos.google.com
- music.youtube.com
- chatgpt.com
- gemini.google.com
- web.whatsapp.com
- www.tiktok.com
Listed panel host with modifyHeaders enabled.
- www.google.com
Listed panel host for Maps with modifyHeaders enabled.
- www.youtube.com
Listed panel host with modifyHeaders enabled for YouTube and Shorts.