Is Cheap Flight Finder safe?

Medium risk

This extension notifies you when cheaper flight prices are available during your flight search.

It compares prices across hundreds of flight booking websites and highlights cheaper deals for the flight you are searching for. The extension is free to use and is available in multiple languages.

Trip.comv1.7.18Chrome Web Store
52Risk
Who publishes it

Trip.com Travel Singapore Pte. Ltd. - no other listings under this identity

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Trip.com
Declared legal entity
Trip.com Travel Singapore Pte. Ltd.
Registered contact
Trip.com

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Extension reports visited travel sites and a persistent ID to fly.trip.com

Visiting a travel site, incl.

Booking.com, Skyscanner, Expedia, and a dozen competitors, makes the extension POST the URL to fly.trip.com/logUserInfo with a UUID made at install.

Tracked sites load from a remote config, expandable anytime.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You visit any travel booking website.

The extension monitors all tab navigations and checks each completed URL against a remotely-updated list of airline and hotel booking sites.

The extension did this

The extension sends the full URL you visited and a permanent identifier to fly.trip.com.

A throttled POST request carries the visited URL, the fly_uid UUID, the referral source, and extension version to fly.trip.com/logUserInfo, automatically, with no prompt.

Captured request
POSThttps://fly.trip.com/logUserInfo

HTTP 200; request confirmed observed in dynamic analysis (2 POST requests captured to /logUserInfo from background service worker).

Headers
uid
e85d34d7-c895-4b7f-a7d9-e144e0e10cf8
source
direct
version
1.7.16
Content-Type
application/json
authorization
ade5f8beb741
Body
{  "url": "https://www.booking.com/flights/",  "extends": {    "version": "1.7.16",    "source": "direct",    "uid": "e85d34d7-c895-4b7f-a7d9-e144e0e10cf8"  }}
Fields included in every logUserInfo POST
  • Visited URL
    https://www.skyscanner.com/transport/flights/lon/jfk/250815/

    The full address of the travel site visited, including search parameters that may reveal destination, dates, and travel preferences.

  • Persistent device ID (fly_uid)
    e85d34d7-c895-4b7f-a7d9-e144e0e10cf8

    A UUID generated once when you installed the extension and never rotated. It links every travel-site visit to the same device across time.

  • Referral source
    direct

    Tracks which channel referred you to install the extension (e.g. 'direct', an affiliate code).

  • Extension version
    1.7.16

    The installed version of the extension, used by the server to segment reports.

When this fires

Every 5 minutes

URL reporting is throttled to at most once every 5 minutes per qualifying navigation event. The throttle window is set by logInterval in the remote config (default 300,000 ms) and can be changed server-side without an extension update.

The code that does this

Navigation listener: URL matching and throttled POST dispatch (background.js lines 592-636)

Readable version

p() — the throttled logUserInfo sender (background.js lines 470–527)

background.js
function p(e) {  // e = { url: <visited URL> }  // Reads fly_uid from chrome.storage.sync, appends to POST body  const uid   = yield chrome.storage.sync.get('fly_uid');  const source = yield chrome.storage.sync.get('fly_source');  return fetch('https://fly.trip.com/logUserInfo', {    method: 'POST',    headers: {      'Content-Type': 'application/json',      authorization: 'ade5f8beb741',      version: '1.7.16',      source,      uid    },    body: JSON.stringify({      ...e,              // { url: <visited URL> }      extends: { version: '1.7.16', source, uid }    })  }).then(r => r.json());}
Where the browsing data is sent
    • fly.trip.com

    Trip.com Group (NASDAQ: TCOM), receives visited URL and persistent UUID at /logUserInfo; also serves remote config at /client/config that controls which sites are tracked.

Remote config from fly.trip.com sets which sites are tracked and how often

A GET to fly.trip.com/client/config returns config for the extension's own tracking: otaReg/travelReg gate /logUserInfo, logInterval sets the gap.

Fallbacks apply only on fetch failure; on success, server values persist locally.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-829
Source
Dynamic sandbox
What actually happens
You did this

The background service worker runs on install, and again roughly every 24 hours as the user browses.

The extension did this

It fetches configuration from fly.trip.com/client/config and stores the returned tracking-site patterns and send interval for use on future page navigations.

Captured request
GEThttps://fly.trip.com/client/config

200 OK. Observed during dynamic analysis. Response body contains otaReg, travelReg, and logInterval fields that set which visited-page URLs get sent to fly.trip.com/logUserInfo and how often, matching the fallback shape hardcoded in background.js.

Headers
authorization
ade5f8beb741
Config fields that gate the tracking behavior
  • Tracked booking-site list (otaReg)
    (booking|kiwi|skyscanner|expedia|agoda|priceline|ctrip|...)\.(com|co|sg|uk|es|jp|ko) (default fallback value shipped in the extension)

    A regular expression naming which travel-booking domains (e.g. booking.com, expedia.com, priceline.com) trigger a URL send when visited.

  • Tracked keyword pattern (travelReg)
    (search|fly|flight|flights|travel|from|hotel|train) (default fallback value shipped in the extension)

    A regular expression matching travel-related keywords (search, fly, flight, hotel, train) anywhere in a visited URL.

  • Send interval (logInterval)
    300000 (5 minutes; default fallback value shipped in the extension)

    The minimum time between URL sends to fly.trip.com/logUserInfo, in milliseconds.

  • Authorization header
    ade5f8beb741

    A fixed API key hardcoded in the extension's code, sent with every config and tracking request.

The code that does this

Config-fetch function with hardcoded fallback values

Readable version
    function h() {      return l(this, void 0, void 0, function*() {        const e = yield fetch(`${t}/client/config`, {          headers: {            authorization: "ade5f8beb741"          }        }), r = yield e.json();        0 === r.errorCode ? u.set(c.CONFIG, r.data) : u.set(c.CONFIG, {          isShowLoading: !1,          loadingTimeout: 3e3,          higherDiscountRate: .05,          otaReg: "(booking|kiwi|skyscanner|expedia|agoda|priceline|ctrip|makemytrip|traveloka|edreams|onthebeach|jtb|tuniu|hotelscombined|elong|qunar|hrs|alitrip|weg|docholiday|cleartrip|rakuten)\\.(com|co|sg|uk|es|jp|ko)",          travelReg: "(search|fly|flight|flights|travel|from|hotel|train)",          logInterval: 3e5        })      })    }
When this fires

Every 1 day

The config fetch runs on extension install and is re-triggered on the first completed page navigation more than 24 hours after the last refresh, so a server-side change to otaReg/travelReg/logInterval can take effect fleet-wide within about a day without an extension update.

Extension posts visited travel-site URLs with a persistent ID to trip.com

The background script POSTs a visited URL, install ID, and source to fly.trip.com/logUserInfo, gated by 2 remote regexes (travel sites, keywords).

Matches go to trip.com, debounced 5 min; capture fired on the onboarding page at install.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

User navigates to a page matching the extension's travel/OTA site patterns, such as booking.com, expedia.com, or skyscanner.com.

The extension did this

The background script sends the visited page's full URL, along with a persistent installation ID, to fly.trip.com/logUserInfo.

Captured request
POSThttps://fly.trip.com/logUserInfo

Captured during dynamic analysis. The URL field carries the page navigated to; because the extension's own onboarding page was the first completed navigation after install, it is the URL captured here, but the same code path fires for any subsequent page matching the travel/OTA regex.

Headers
uid
b3985d23-9220-4185-abea-47e0e13a81c6
source
direct
version
1.7.16
Content-Type
application/json
authorization
ade5f8beb741
Body
{  "url": "https://us.trip.com/pages/flights/pages/save-fly-crx-welcome.html?locale=en-US&utm_source=direct",  "extends": {    "version": "1.7.16",    "source": "direct",    "uid": "b3985d23-9220-4185-abea-47e0e13a81c6"  }}
Fields sent to fly.trip.com/logUserInfo
  • Visited page URL
    https://www.booking.com/searchresults.html?ss=Paris (illustrative)

    The full URL of the page you navigated to, including query string, is sent when it matches a travel/OTA pattern.

  • Persistent installation ID
    b3985d23-9220-4185-abea-47e0e13a81c6

    A UUID generated once on install, stored in sync storage, sent with every request to link visits to this installation over time.

  • Install source
    direct

    Records where the extension install originated from (e.g. a referral or campaign tag).

  • Extension version
    1.7.16

    The installed extension version, sent with every request.

The code that does this

Navigation listener that matches visited URLs and posts them to fly.trip.com

Readable version
    let d = !1,      f = 0;    function h() {      return l(this, void 0, void 0, function*() {        const e = yield fetch(`${t}/client/config`, {          headers: {            authorization: "ade5f8beb741"          }        }), r = yield e.json();        0 === r.errorCode ? u.set(c.CONFIG, r.data) : u.set(c.CONFIG, {          isShowLoading: !1,          loadingTimeout: 3e3,          higherDiscountRate: .05,          otaReg: "(booking|kiwi|skyscanner|expedia|agoda|priceline|ctrip|makemytrip|traveloka|edreams|onthebeach|jtb|tuniu|hotelscombined|elong|qunar|hrs|alitrip|weg|docholiday|cleartrip|rakuten)\\.(com|co|sg|uk|es|jp|ko)",          travelReg: "(search|fly|flight|flights|travel|from|hotel|train)",          logInterval: 3e5        })      })    }    function p(e) {      return function(e) {        return r = this, n = arguments, u = function*({          url: e,          data: r,          options: n,          method: i = "POST"        }) {          const u = yield o.get(a.FLY_UID), c = yield o.get(a.SOURCE);          return fetch(`${t}${e}`, Object.assign({            method: i,            headers: {              "Content-Type": "application/json",              authorization: "ade5f8beb741",              version: s,              source: c,              uid: u            },            body: "POST" === i ? JSON.stringify(Object.assign(Object.assign({}, r || {}), {              extends: {                version: s,                source: c,                uid: u              }            })) : void 0          }, n || {})).then(t => t.json())        }, new((i = void 0) || (i = Promise))(function(t, e) {          function o(t) {            try {              s(u.next(t))            } catch (t) {              e(t)            }          }          function a(t) {            try {              s(u.throw(t))            } catch (t) {              e(t)            }          }          function s(e) {            var r;            e.done ? t(e.value) : (r = e.value, r instanceof i ? r : new i(function(t) {              t(r)            })).then(o, a)          }          s((u = u.apply(r, n || [])).next())        });        var r, n, i, u      }({        url: "/logUserInfo",        method: "POST",        data: e      })    }    var v = r(7350),      y = r.n(v),      g = function(t, e, r, n) {        return new(r || (r = Promise))(function(i, o) {          function a(t) {            try {              u(n.next(t))            } catch (t) {              o(t)            }          }          function s(t) {            try {              u(n.throw(t))            } catch (t) {              o(t)            }          }          function u(t) {            var e;            t.done ? i(t.value) : (e = t.value, e instanceof r ? e : new r(function(t) {              t(e)            })).then(a, s)          }          u((n = n.apply(t, e || [])).next())        })      };    const m = {        da: "da-DK",        de: "de-DE",        el: "el-GR",        "en-US": "en-US",        es: "es-ES",        fi: "fi-FI",        fr: "fr-FR",        id: "id-ID",        it: "it-IT",        ja: "ja-JP",        ko: "ko-KR",        ms: "ms-MY",        nl: "nl-NL",        pl: "pl-PL",        "pt-BR": "pt-BR",        "pt-PT": "pt-PT",        ru: "ru-RU",        sv: "sv-SE",        th: "th-TH",        tr: "tr-TR",        uk: "uk-UA",        vi: "vi-VN",        "zh-CN": "zh-HK",        "zh-TW": "zh-HK"      },      x = ["https://fly.trip.com/checkPriceV3"];    chrome.webRequest.onBeforeRequest.addListener(t => {      t.url, t.tabId    }, {      urls: ["https://fly.trip.com/*"]    }, ["requestBody"]);    let w = null;    try {      let r, i;      chrome.tabs.onUpdated.addListener((e, n, v) => g(void 0, void 0, void 0, function*() {        yield function() {          return l(this, void 0, void 0, function*() {            if (d) return;            d = !0;            const e = f || (yield o.get(a.LAST_UPDATE_DATE)) || 0,              r = yield o.get(a.FLY_UID), n = yield o.get(a.SOURCE), i = yield chrome.cookies.get({                url: "https://fly.trip.com",                name: "cticket"              }), u = Date.now();            if (u - e > 864e5 && r && n) {              f = u, yield o.set(a.LAST_UPDATE_DATE, u), h();              try {                yield fetch(`${t}/trackerV2`, {                  credentials: "include",                  method: "POST",                  headers: {                    "Content-Type": "application/json",                    authorization: "ade5f8beb741"                  },                  body: JSON.stringify({                    cticket: null == i ? void 0 : i.value,                    extends: {                      version: s,                      source: n,                      uid: r                    }                  })                })              } catch (t) {}            }            d = !1          })        }();        const e = yield u.get(c.CONFIG);        if (e && !r && !i) {          const {            otaReg: t,            travelReg: n          } = e;          r = new RegExp(t), i = new RegExp(n)        }        e && e.logInterval && !w && (w = y()(p, e.logInterval)), v.url && "complete" === n.status && (r.test(v.url) || i.test(v.url)) && (null == w || w({          url: v.url        }))
When this fires

Every 5 minutes

The extension can send at most one matching URL per debounce window; the window length is set by the logInterval value returned from the extension's remote config (5 minutes/300000ms by default) and can be changed by the server at any time without an extension update.

Where it sends data

Destinations our analysis observed Cheap Flight Finder contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • trip.com

    Cheap Flight Finder sends data to trip.com. No other extension we have analysed sends data here.

Updated 30 September 2026papdhfmmdhepoeeimnkdhnadpmemeamk