Is Tweet Hunter X: Sidebar for X safe?
Tweet Hunter is high risk. Tweet Hunter X routes X/Twitter search and sidebar data through Firebase Cloud Functions it operates, adding account IDs to search parameters and calling us-central1-ez4cast.cloudfunctions.net. Earlier testing missed this while signed out.…
Who publishes itlempire - 1 other listing from the same operator, 1 of them carrying a finding
lempire - 1 other listing from the same operator, 1 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
1 other listing published from this account, 100k+ users between them. 1 of them carries a finding.
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
X search data routes through Tweet Hunter Firebase functions
Tweet Hunter X routes X/Twitter search and sidebar data through Firebase Cloud Functions it operates, adding account IDs to search parameters and calling us-central1-ez4cast.cloudfunctions.net.
Earlier testing missed this while signed out.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You use Tweet Hunter's sidebar or search features on X.com.
The path requires an authenticated Tweet Hunter user and an X account selected in the extension.
The extension forwards the request through its Firebase Cloud Functions.
Search and highlight requests go to us-central1-ez4cast.cloudfunctions.net before results are shown in the sidebar.
- Tweet Hunter user IDYh7xA9kP2nQw45LmR8sT0uVb3cD2
Connects the request to your Tweet Hunter account.
- Selected X account IDtwitter-account-1847263950
Ties the request to the X account you are using inside the extension.
- Search queryfrom:securitywriter -is:retweet
Shows the X search or profile lookup that the sidebar is asking for.
- Requested tweet fieldscreated_at,public_metrics,referenced_tweets,attachments,author_id
Describes which tweet metadata the extension asks the remote function to return.
- Tweet records sent backtweet text, author username, like count, reply count, tweet URL
Can include the tweet text and author context selected by the sidebar feature.
No response body was recorded in traffic evidence; verification confirmed the code path and endpoint construction.
- tokenuserid
- Tweet Hunter user ID
- Authorization
- Bearer token from Tweet Hunter session
No request body or response body was recorded in traffic evidence.
- Content-Type
- application/json
Content script builds the Firebase requests
Runtime message wrapper adds account headers
dist/content.jsfunction N4(t, e) { let a = t?.user?.firebaseToken; return a ? new Promise(c => { WL(a, n => { if (!n) { c(null); return } chrome.runtime.sendMessage({ method: e.method, url: e.url, headers: { ...e.headers, tokenuserid: t?.user?.uid, Authorization: `Bearer ${n}` }, ...e.payload && { payload: e.payload } }, i => c(i)) }) }) : Promise.resolve(null)}Search and tweet upload endpoints
dist/content.jsvar Ov = "https://us-central1-ez4cast.cloudfunctions.net";async function r3(t) { try { let e = { type: "tweet", tweets: t }; chrome.runtime.sendMessage({ method: "POST", url: `${Ov}/twitterFetcher-push`, headers: { "Content-Type": "application/json" }, payload: e }) } catch (e) {}}async function c3(t, e, a) { if (!e?.idAccount || !a?.user?.uid) return { data: null, isPrivateList: !1 }; t.set("idUser", a.user.uid), t.set("idAccount", e.idAccount); let c = await N4(a, { method: "GET", url: `${Ov}/twitterFetcher-searchAllTweetsForExtension?${t.toString()}` }); return c?.error === "private_list" || c?.data?.error === "private_list" ? { data: null, isPrivateList: !0 } : { data: c?.data ?? null, isPrivateList: !1 }}All-time highlight endpoint
dist/content.jsO = async () => { f(!0), chrome.runtime.sendMessage({ method: "GET", url: `https://us-central1-ez4cast.cloudfunctions.net/tweetChampions-twemexGetAllTimeHighLights?username=${t}` }, U)}Service worker forwards caller-supplied network requests
Generic GET/POST forwarder
dist/background.jschrome.runtime.onMessage.addListener((n, e, m) => { if (n.message === "requestTweetHunterUser") return q0("responding to tweet hunter user request"), wa().then(y => y.json()).then(y => { q0("json: ", y), m(y) }), !0; if (n.method === "GET" && n.url) return fetch(n.url, { [n.headers ? "headers" : ""]: n.headers, [n.mode ? "mode" : ""]: n.mode, [n.credentials ? "credentials" : ""]: n.credentials }).then(y => y.json()).then(y => { y.success ? m({ success: 1, data: y }) : m({ success: 0, error: y.error }) }).catch(y => m({ success: 0, error: y.message })), !0; if (n.method === "POST" && n.url && n.payload) return fetch(n.url, { method: "POST", body: JSON.stringify(n.payload), [n.headers ? "headers" : ""]: n.headers, [n.mode ? "mode" : ""]: n.mode, [n.credentials ? "credentials" : ""]: n.credentials }).then(y => y.json()).then(y => { y.success ? m({ success: 1, data: y }) : m({ success: 0, error: y.error }) }).catch(y => m({ success: 0, error: y.message })), !0});- us-central1-ez4cast.cloudfunctions.net
Firebase Cloud Functions host used for X search, tweet upload, profile highlights, and AI reply generation endpoints.
- app.tweethunter.io
Tweet Hunter session host used by the service worker to obtain the signed-in user's session and token.
Tweet interactions upload tweet objects to Firebase
When you open the Quote Tweets widget on an X.com tweet page, Tweet Hunter X posts quote-tweet objects to us-central1-ez4cast.cloudfunctions.net/twitterFetcher-push, including tweet text, author fields, timestamps, and engagement counts.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You open the Quote Tweets widget on an X.com tweet page.
The widget appears on individual tweet pages and loads after you select the Quote Tweets control.
The extension sends a list of quote-tweet objects to its Firebase function.
The payload is created as type=tweet and forwarded by the background service worker as a POST.
- Tweet textLaunching our Q3 roadmap thread today (illustrative)
This can reveal the full text of posts that appear in the quote-tweet results you asked the extension to load.
- Tweet authorname=Acme Security, screen_name=acmesec, id_str=1523456789012345678 (illustrative)
This links the transmitted tweet content to the X account that authored it.
- Engagement countsreply_count=4, retweet_count=18, quote_count=6, favorite_count=143 (illustrative)
This adds popularity and interaction context to the tweet objects sent off the device.
- Tweet URLhttps://x.com/acmesec/status/1812345678901234567 (illustrative)
This gives the recipient a direct link back to the X.com post represented by the object.
- Media and quoted-tweet linksmedia_url_https=https://pbs.twimg.com/media/GQ4Example.jpg, quoted_tweet=1811111111111111111 (illustrative)
This can preserve attached media URLs and the ID of the quoted tweet in the data sent by the extension.
The trigger, tweet-object mapper, payload builder, and background forwarder
Quote Tweets widget trigger
dist/content.jsvar t11 = ({ currentPage: t, theme: e, autoLoad: a = !1 }) => { let { user: c } = k0(), n = F4(c), i = t.pageType === "showTweet" && t.tweetId, [o, h] = (0, i0.useState)([]), [l, v] = (0, i0.useState)(!1), [u, d] = (0, i0.useState)(null), [s, z] = (0, i0.useState)(a), x = async () => { v(!0), d(null), h([]); try { let M = `url:${i} -is:retweet`, f = new URLSearchParams({ query: M, maxResults: "100", tweet_fields: "created_at,public_metrics,referenced_tweets,attachments,author_id", user_fields: "name,username,profile_image_url,verified,protected", media_fields: "url,type", expansions: "author_id,referenced_tweets.id,attachments.media_keys" }), { data: p } = await c3(f, n, c); if (_1("QuoteTweets API Response:", p), !p?.success || !p?.tweets?.data) { _1("No tweets data in response"), d("Failed to load quote tweets"); return } let H = n3(p.tweets).filter(L => L.id_str !== i && L.quoted_tweet), V = (0, SS.default)(H, L => L.favorite_count).reverse(); r3(V), h(V) } catch (M) { _1("Quote tweets fetch error:", M), d("Failed to load quote tweets") } finally { v(!1) } }; return (0, i0.useEffect)(() => { z(a) }, [i]), (0, i0.useEffect)(() => { !i || !s || x() }, [i, n?.idAccount, s]), i ? s ? i0.default.createElement(P0, null, l && i0.default.createElement(H0, { themeColor: e.name }), u && i0.default.createElement("div", null, u), !l && !u && i0.default.createElement(E0, { tweets: o, showQuotes: !1, query: `Quote tweets for tweet ${i}` })) : i0.default.createElement(P0, null, i0.default.createElement(l3, { widgetId: "QuoteTweets", onLoad: () => z(!0) })) : null }, AS = B2(t11);Tweet-object mapper
dist/content.jsfunction n3(t) { return t.data ? t.data.map(e => { let a = t.includes?.users?.find(n => n.id === e.author_id), c = e.attachments?.media_keys?.map(n => t.includes?.media?.find(i => i.media_key === n)).filter(Boolean); return { id_str: e.id, id: e.id, text: e.text, user: a ? { id_str: a.id, name: a.name, screen_name: a.username, profile_image_url_https: a.profile_image_url, verified: a.verified, is_protected: a.protected } : null, entities: { media: c?.map(n => ({ media_url_https: n.url })) || [] }, created_at: e.created_at, twemexCreatedAt: new Date(e.created_at).toLocaleDateString(), favorited: !1, retweeted: !1, reply_count: e.public_metrics.reply_count, retweet_count: e.public_metrics.retweet_count, quote_count: e.public_metrics.quote_count, favorite_count: e.public_metrics.like_count, tweetUrl: `https://x.com/${a?.username}/status/${e.id}`, quoted_tweet: e.referenced_tweets?.find(n => n.type === "quoted")?.id } }) : []}Payload builder
dist/content.jsasync function r3(t) { try { let e = { type: "tweet", tweets: t }; chrome.runtime.sendMessage({ method: "POST", url: `${Ov}/twitterFetcher-push`, headers: { "Content-Type": "application/json" }, payload: e }) } catch (e) {}}Background POST forwarder
dist/background.jschrome.runtime.onMessage.addListener((n, e, m) => { if (n.message === "requestTweetHunterUser") return q0("responding to tweet hunter user request"), wa().then(y => y.json()).then(y => { q0("json: ", y), m(y) }), !0; if (n.method === "GET" && n.url) return fetch(n.url, { [n.headers ? "headers" : ""]: n.headers, [n.mode ? "mode" : ""]: n.mode, [n.credentials ? "credentials" : ""]: n.credentials }).then(y => y.json()).then(y => { y.success ? m({ success: 1, data: y }) : m({ success: 0, error: y.error }) }).catch(y => m({ success: 0, error: y.message })), !0; if (n.method === "POST" && n.url && n.payload) return fetch(n.url, { method: "POST", body: JSON.stringify(n.payload), [n.headers ? "headers" : ""]: n.headers, [n.mode ? "mode" : ""]: n.mode, [n.credentials ? "credentials" : ""]: n.credentials }).then(y => y.json()).then(y => { y.success ? m({ success: 1, data: y }) : m({ success: 0, error: y.error }) }).catch(y => m({ success: 0, error: y.message })), !0});- us-central1-ez4cast.cloudfunctions.net
Google Cloud Functions endpoint used by the extension backend to receive the type=tweet payload.