Is Tweet Hunter X: Sidebar for X safe?

High risk

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.…

lemlistv1.16.3Chrome Web Store
75Risk
Who publishes it

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.

Publisher
lemlist
Declared legal entity
lempire
Registered address
8 Rue de Cléry, Paris 75002, FR
Registered contact
lempire

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.

us-central1-ez4cast.cloudfunctions.net
Also called by 1 other listing: Taplio X

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
What actually happens
You did this

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

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.

Fields the code adds or forwards
  • Tweet Hunter user ID
    Yh7xA9kP2nQw45LmR8sT0uVb3cD2

    Connects the request to your Tweet Hunter account.

  • Selected X account ID
    twitter-account-1847263950

    Ties the request to the X account you are using inside the extension.

  • Search query
    from:securitywriter -is:retweet

    Shows the X search or profile lookup that the sidebar is asking for.

  • Requested tweet fields
    created_at,public_metrics,referenced_tweets,attachments,author_id

    Describes which tweet metadata the extension asks the remote function to return.

  • Tweet records sent back
    tweet text, author username, like count, reply count, tweet URL

    Can include the tweet text and author context selected by the sidebar feature.

Captured request
GEThttps://us-central1-ez4cast.cloudfunctions.net/twitterFetcher-searchAllTweetsForExtension

No response body was recorded in traffic evidence; verification confirmed the code path and endpoint construction.

Headers
tokenuserid
Tweet Hunter user ID
Authorization
Bearer token from Tweet Hunter session
Captured request
POSThttps://us-central1-ez4cast.cloudfunctions.net/twitterFetcher-push

No request body or response body was recorded in traffic evidence.

Headers
Content-Type
application/json
The code that does this

Content script builds the Firebase requests

Readable version

Runtime message wrapper adds account headers

dist/content.js
function 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.js
var 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.js
O = async () => {  f(!0), chrome.runtime.sendMessage({    method: "GET",    url: `https://us-central1-ez4cast.cloudfunctions.net/tweetChampions-twemexGetAllTimeHighLights?username=${t}`  }, U)}
The code that does this

Service worker forwards caller-supplied network requests

Readable version

Generic GET/POST forwarder

dist/background.js
chrome.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});
Remote hosts involved in the verified path
    • 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
What actually happens
You did this

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

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.

Fields assembled in each uploaded tweet object
  • Tweet text
    Launching 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 author
    name=Acme Security, screen_name=acmesec, id_str=1523456789012345678 (illustrative)

    This links the transmitted tweet content to the X account that authored it.

  • Engagement counts
    reply_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 URL
    https://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 links
    media_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.

Captured request
POSThttps://us-central1-ez4cast.cloudfunctions.net/twitterFetcher-push
The code that does this

The trigger, tweet-object mapper, payload builder, and background forwarder

Readable version

Quote Tweets widget trigger

dist/content.js
var 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.js
function 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.js
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) {}}

Background POST forwarder

dist/background.js
chrome.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});
Network destination used by the tweet upload path
    • us-central1-ez4cast.cloudfunctions.net

    Google Cloud Functions endpoint used by the extension backend to receive the type=tweet payload.

Updated 30 September 2026amoldiondpmjdnllknhklocndiibkcoe