Is Surfe safe?

High risk

Surfe is high risk. On a Surfe CRM update flow on LinkedIn, the extension can request Voyager profile data via the active session and PUT mapped fields to https://api.prod.surfe.com/auth/contacts. Testing lacked a token and session, but the code path exists.…

Surfe Publishersv3.15.0Chrome Web Store
75Risk
Who publishes it

GROWSTER - 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
Surfe Publishers
Declared legal entity
GROWSTER
Registered address
17 Allée des Gassets, Serris 77700, FR
Registered contact
GROWSTER

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

LinkedIn profile data syncs to Surfe CRM

On a Surfe CRM update flow on LinkedIn, the extension can request Voyager profile data via the active session and PUT mapped fields to https://api.prod.surfe.com/auth/contacts.

Testing lacked a token and session, but the code path exists.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You use a Surfe CRM update flow while viewing LinkedIn.

The extension did this

The extension can read LinkedIn profile data with your active session and send mapped contact fields to Surfe.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://www.linkedin.com/voyager/api/me
401 Unauthorized in the unauthenticated test browser; no /auth/contacts request body was captured because the Surfe API token was absent and LinkedIn cookies were unavailable.
03EvidenceFIELD TABLE
Contact fields prepared for the Surfe update
FieldValueWhy it matters
LinkedIn identity
Jane Doe, https://www.linkedin.com/in/jane-doe/Identifies the LinkedIn member being synced into the CRM record.
Profile text
headline: Account Executive; summary: Helps enterprise teams evaluate CRM toolingAdds biographical and job context from the LinkedIn profile to the CRM record.
Location and language
Paris, Ile-de-France, FR; language: enAdds where the LinkedIn member is based and the locale LinkedIn returned for the profile.
Company details
Surfe, linkedinID: 1234567, industry: Software DevelopmentLinks the person to company records and company LinkedIn identifiers in the CRM.
CRM-side cookies
hubspotapi-csrf=abc123; hubspotapi=session-valueCan include CRM cookies that the extension attaches when updating the contact record.
04EvidenceCODE COMPARE
The code that does this

The CRM update path reads LinkedIn profile data and PUTs it to Surfe

What it actually does
Surfe API base URL and LinkedIn hostdeobfuscated/inject.js:11109-11118
      be = "https://api.prod.surfe.com",
      xe = "https://app.surfe.com",
      Ce = "https://account.surfe.com",
      Ee = "pub4cc3fa1c9969c63aabeda33f780bd9ee",
      Se = "datadoghq.eu",
      ke = parseInt("MISSING_ENV_VAR".DATADOG_RUM_SAMPLE_RATE ?? ""),
      Ne = parseInt("MISSING_ENV_VAR".DATADOG_RUM_SAMPLE_RATE ?? ""),
      je = "https://intercom.help/surfe/en/",
      Re = "linkedin.com",
      Te = "production",
HTTP helper sends JSON to the Surfe API with the stored tokendeobfuscated/inject.js:23354-23470
      Wf = (e, t, n = {}, r) => {
        const {
          url: a,
          body: o,
          token: i
        } = (e => {
          try {
            const {
              url: t,
              params: n,
              body: r,
              token: a = ""
            } = e, o = null !== r ? JSON.stringify(r) : null, i = new URLSearchParams(n);
            Array.from(i.entries()).forEach(([e, t]) => {
              "undefined" === t && i.set(e, "")
            });
            const s = new URL(t, e.baseUrl ?? be);
            return s.search = i.toString(), {
              body: o,
              url: s.toString(),
              token: a
            }
          } catch (e) {
            return yv("parse-request", e), {
              url: "",
              token: "",
              body: ""
            }
          }
        })(t);
        return Promise.resolve().then(async () => {
          const t = await (async e => {
            if (e.includes("feature-flags")) return {};
            try {
              const {
                isSidePanelEnabled: e = "false"
              } = await ww(["isSidePanelEnabled"]);
              return {
                "X-Extension-Type": "true" === e ? "sidepanel" : "injected"
              }
            } catch (e) {
              return gv("Failed to resolve X-Extension-Type header", {
                error: e
              }), {}
            }
          })(a);
          return fetch(a, {
            method: e,
            headers: {
              Accept: "application/json",
              "Content-Type": "application/json",
              "Extension-Version": nv(),
              Authorization: `Bearer ${i}`,
              ...t,
              ...n
            },
            body: o,
            signal: r?.signal
          })
        }).then(async e => {
          const {
            headers: n
          } = e;
          return n.get("deprecated") && gv(`${t.url} was deprecated in ${n.get("expires")??""}. Please update Surfe to the newest version.\nYou can refer to https://docs.surfe.dev/ for more information, or https://intercom.help/surfe/en/ if you need some help.`), 204 === e.status || "0" === e.headers.get("content-length") ? {
            data: {},
            status: e.status
          } : (async (e, t) => {
            try {
              const n = await e.text();
              if (!n && e.ok) return {
                data: {},
                status: e.status
              };
              const r = n ? JSON.parse(n) : null;
              return e.status >= 300 && qf(r, e.status, e.headers, t), {
                data: r,
                status: e.status
              }
            } catch (t) {
              if (t instanceof Zf) throw t;
              if (e.ok) return {
                data: {},
                status: e.status
              };
              throw t
            }
          })(e, r)
        })
      },
      Gf = async (e, t, n, r) => {
        const {
          token: a
        } = await ww(["token"]);
        if (!a) return gv("API token not found", {
          endpoint: t.url
        }), {
          data: {},
          status: 401
        };
        try {
          return await Wf(e, {
            ...t,
            token: a
          }, n, r)
        } catch (e) {
          if (e instanceof Zf && (((e, t) => {
              const n = e;
              if ("object" == typeof e && xw(n.message)) {
                const t = JSON.parse(n.message ?? "{}"),
                  r = "invalid_grant" === t.error && "expired access/refresh token" === t.error_description;
                return r && yv("invalid-grant-error", e), r
              }
              return !(401 !== e.data.code && 403 !== e.data.code || !t || !Le.includes(t))
            })(e) || 14 === e.data.errorCode)) return wf(), Promise.reject();
          throw e
        }
      }, Kf = (() => {
LinkedIn Voyager fetch uses the active session CSRF tokendeobfuscated/inject.js:37163-37238
    async function AM(e, t, n, r = {
      retry: !0
    }) {
      const {
        sessionID: a
      } = await ww();
      if (!a) throw new Error("No session ID");
      const o = await async function(e) {
        let t = await OM();
        if (t.endpoints || (t = await OM()), !t.endpoints) return [];
        let n = t.endpoints[e];
        if (n && 0 !== n.length || (t = await OM()), !t.endpoints) return [];
        if (n = t.endpoints[e], !n || 0 === n.length) throw new Error(`Invalid Endpoint ${e}`);
        return n.sort((e, t) => e.date && t.date ? new Date(t.date).getTime() - new Date(e.date).getTime() : 0), n.map(e => e.endpoint)
      }(e);
      for (const i of o) try {
        const o = i.replace(/\%\%.*?\%\%/g, e => {
            if (!t) throw Error("No arguments given for dynamic part of url");
            if (!t[e.replace(/\%/g, "")]) throw Error("Wrong arguments given for dynamic part of url");
            return t[e.replace(/\%/g, "")]
          }),
          s = `https://www.${Re}${o}${n??""}`,
          l = await fetch(s, {
            method: "GET",
            headers: {
              accept: "application/vnd.linkedin.normalized+json+2.1",
              "csrf-token": a,
              "sec-fetch-dest": "empty",
              "sec-fetch-mode": "cors",
              "sec-fetch-site": "same-origin",
              "x-li-lang": "en_US",
              "x-restli-protocol-version": "2.0.0"
            },
            referrerPolicy: "no-referrer-when-downgrade",
            mode: "cors",
            credentials: "include"
          });
        if (200 !== l.status) {
          const e = await l.text();
          yv("failedLIRequest", {
            message: `linked-in-request url=${s} res=${l.status}`,
            errorDetails: {
              body: e,
              returnStatus: l.status,
              id: "failedLIRequest"
            }
          })
        }
        if (403 === l.status && r.retry) {
          if (await MM()) return chrome.runtime.sendMessage({
            msg: "refresh-li-cookies"
          }), await fw("sessionID"), await gw({
            lastSessionIDUpdate: (new Date).toISOString()
          }), vv("linkedin-endpoint-query", {
            msg: "refresh-session-cookies"
          }), AM(e, t, n, {
            retry: !1
          })
        }
        if (200 !== l.status || "error" === l.type) {
          const {
            statusText: e,
            status: t
          } = l;
          yv("linkedin-endpoint-query", Hf({
            statusCode: t,
            statusText: e
          }));
          continue
        }
        return await l.json()
      } catch (e) {
        yv("linkedin-endpoint-query", Hf(e))
      }
      return null
    }
Contact profile extraction calls the configured LinkedIn endpointsdeobfuscated/inject.js:37769-37912
      jA = async e => {
        if (NA.profile && e === NA.profileUrl && Date.now() - NA.fetchedAt < 3e5) return NA.profile;
        const t = !!e;
        e || (e = window.location.href);
        const n = Kw(e),
          r = await AM("contactProfile", {
            id: n
          });
        if (!r?.included || 0 === r.included.length) throw Error("Could not fetch contact profile from LinkedIn...");
        let a = "";
        const o = [],
          i = [],
          s = [],
          l = new iA;
        for (const e of r.included)
          if (pA(e)) {
            l.linkedinUrl = `https://www.linkedin.com/in/${e.publicIdentifier??""}/`, l.entityURN = e.entityUrn, l.publicIdentifier = e.publicIdentifier, l.objectUrn = e.objectUrn, l.firstName = e.firstName, l.lastName = e.lastName, l.address = e.address, l.countryCode = e.location?.countryCode, l.language = e.primaryLocale?.language, l.headline = e.headline, l.summary = e.summary, l.premium = e.premium, l.creator = e.creator, l.hasSalesNav = !!e.premium, l.premium = e.premium, l.creator = e.creator;
            const n = e.birthDateOn?.day,
              r = e.birthDateOn?.month;
            if (l.birthDate = n && r ? `${n}-${r}` : void 0, e.profilePicture) {
              const {
                displayImageWithFrameReferenceUnion: t,
                displayImageReference: n
              } = e.profilePicture, r = n ?? t;
              if (r) {
                const e = r.vectorImage.rootUrl,
                  t = r.vectorImage.artifacts;
                for (const n of t)
                  if (l.pictureUrl = e + n.fileIdentifyingUrlPathSegment, l.pictureExpiryDate = new Date(n.expiresAt), 400 === n.width) break
              }
            }
            if (a = e["*profilePositionGroups"], !t) {
              const t = await zf({
                isSalesNav: !1
              });
              if ("" !== t && t !== `${e.firstName} ${e.lastName}`) {
                const e = t.split(" ");
                l.firstName = e[0], e.length <= 1 ? l.lastName = "" : l.lastName = e.slice(1).join(" ")
              }
            }
          } else if (hA(e)) l.educations.push({
          schoolName: e.schoolName,
          description: e.description,
          fieldOfStudy: e.fieldOfStudy
        });
        else if (fA(e)) {
          const t = new Date,
            n = t.setMonth(t.getMonth() - 6),
            r = e.dateRange?.start && {
              ...e.dateRange.start,
              month: e.dateRange.start.month ? e.dateRange.start.month - 1 : e.dateRange.start.month
            },
            a = e.dateRange?.end && {
              ...e.dateRange.end,
              month: e.dateRange.end.month ? e.dateRange.end.month - 1 : e.dateRange.end.month
            },
            {
              startDate: i,
              endDate: s
            } = kA(r, a);
          o.push({
            entityUrn: e.entityUrn,
            companyName: e.companyName,
            companyUrn: e.companyUrn,
            dateRange: e.dateRange,
            startDate_: i,
            endDate_: s,
            isRecent: +n <= +s
          })
        } else if (mA(e)) {
          const {
            startDate: t,
            endDate: n
          } = kA(e.dateRange?.start, e.dateRange?.end), r = new Date, a = r.setMonth(r.getMonth() - 6);
          l.positions.push({
            companyName: e.companyName,
            dateRange: e.dateRange,
            title: e.title,
            startDate_: t,
            endDate_: n,
            description: e.description,
            location: e.locationName,
            companyUrn: e.companyUrn,
            isRecent: +a <= +n
          })
        } else if (wA(e)) l.languages.push({
          name: e.name,
          proficiency: e.proficiency
        });
        else if (vA(e)) l.skills.push(e.name), l.completeSkills || (l.completeSkills = []), l.completeSkills.push({
          name: e.name,
          entityUrn: e.entityUrn
        });
        else if (yA(e)) s.push({
          name: e.name,
          linkedinUrl: e.url,
          companyUrn: e.entityUrn,
          universalName: e.universalName
        });
        else if (bA(e)) i.push({
          entityUrn: e.entityUrn,
          "*elements": e["*elements"]
        });
        else if (gA(e)) {
          const {
            city: t,
            state: n
          } = $w(e.defaultLocalizedName);
          l.location = e.defaultLocalizedName, l.city = t, l.state = n
        }
        const c = await AM("contactProfileSupplementary", {
          id: n
        });
        if (c?.included || 0 === c?.included?.length)
          for (const e of c.included)
            if (xA(e)) {
              const t = e.memberRelationshipUnion?.noConnection?.memberDistance;
              if (t) switch (t) {
                case uA.OUT_OF_NETWORK:
                  l.connectionDegreeLevel = "out of network";
                  break;
                case uA.DISTANCE_3:
                  l.connectionDegreeLevel = "3";
                  break;
                case uA.DISTANCE_2:
                  l.connectionDegreeLevel = "2";
                  break;
                default:
                  l.connectionDegreeLevel = "unknown"
              } else l.connectionDegreeLevel = "1"
            } else CA(e) && Number(e.createdAt) && (l.connectionDate = new Date(Number(e.createdAt)).toJSON());
        let d = [];
        for (const e of i) e.entityUrn === a && e["*elements"] && (d = e["*elements"]);
        l.positions.sort((e, t) => {
          const n = +t.endDate_ - +e.endDate_;
          return 0 === n ? +t.startDate_ - +e.startDate_ : n
        });
        for (const e of l.positions)
          for (const t of o) t.companyUrn !== e.companyUrn || t.title || (t.title = e.title);
        if (l.companies = s, d.length > 0)
          for (const e of d)
            for (const t of o) t.entityUrn === e && l.positionGroups.push(t);
        return e && (NA.fetchedAt = Date.now(), NA.profile = l, NA.profileUrl = e), l
      }, RA = async (e, t, n) => {
CRM contact field mappingdeobfuscated/inject.js:38498-38570
      }), XA = e => {
        return {
          id: e.id,
          firstName: e.firstName,
          lastName: e.lastName,
          headline: e.headline,
          summary: e.summary,
          location: e.location,
          address: e.address,
          birthDate: e.birthDate,
          city: e.city,
          state: e.state,
          countryCode: e.countryCode,
          connectionDegreeLevel: e.connectionDegreeLevel,
          connectionDate: e.connectionDate,
          jobTitle: e.jobTitle,
          linkedinUrl: e.linkedinUrl,
          profileURL: e.crmUrl,
          entityURN: e.entityURN,
          pictureUrl: e.pictureUrl,
          birthYear: e.birthYear,
          company: e.company && (t = e.company, {
            id: t.id,
            name: t.name,
            tagline: t.tagline,
            description: t.description,
            foundedYear: t.foundedYear,
            phone: t.phone,
            companyType: t.type,
            linkedinHandle: t.linkedinHandle,
            linkedinID: t.linkedinID,
            linkedinUrl: t.linkedinUrl,
            entityUrn: t.entityUrn,
            companyPageUrl: t.pageUrl,
            headquarter: t.headquarter,
            staffCountRange: t.staffCountRange,
            extraFields: t.extraFields,
            stage: t.stage,
            status: t.status,
            industry: t.industry,
            owner: t.owner?.name,
            ownerID: t.owner?.id,
            ownerPictureURL: t.owner?.pictureUrl,
            website: t.website,
            createdAt: t.createdAt,
            updatedAt: t.updatedAt,
            salesNavUrn: ""
          }),
          companyName: e.company?.name,
          educations: e.educations,
          positions: e.positions,
          positionGroups: e.positionGroups,
          languages: e.languages,
          skills: e.skills,
          email: e.email,
          oldEmail: e.oldEmail,
          phone: e.phone,
          oldPhone: e.oldPhone,
          stage: e.stage,
          status: e.status,
          extraFields: e.extraFields,
          customFields: e.customFields,
          tags: e.tags,
          ownerID: e.owner?.id,
          ownerName: e.owner?.name,
          isLead: e.isLead,
          personalWebsite: e.personalWebsite,
          entitySalesNavURN: e.entitySalesNavURN,
          secondPhone: e.secondPhone,
          salesNavUrn: ""
        };
        var t
      }, _A = async e => {
Surfe contact update requestdeobfuscated/inject.js:38614-38640
      }, eL = async (e, t, n) => {
        const {
          isLead: r = !1,
          isRelink: a = !1,
          auto: o = !1,
          forceCompanyUpdate: i = !1,
          cookies: s
        } = n ?? {}, l = r || t.isLead, c = {
          ...XA(t),
          salesNavURN: t.salesNavURN
        }, {
          data: d
        } = await Gf("PUT", {
          url: "/auth/contacts",
          params: {
            id: e,
            auto: o,
            lead: l,
            relink: a,
            forceCompanyUpdate: i
          },
          body: {
            ...c,
            cookies: s
          }
        });
        return JA(d)
Save handler reaches the LinkedIn profile read pathdeobfuscated/inject.js:79176-79193
            onSave: () => {
              w(!1), g(!1), (async () => {
                if (i(!0), n) {
                  const e = a ? await LA() : await jA();
                  await qA(n, {
                    linkedinUrl: e.linkedinUrl
                  })
                } else if (r) {
                  const e = a ? await EA() : await oA();
                  await tA(r.id, {
                    linkedinUrl: e.linkedinUrl
                  }, {
                    linkedinID: XM(e.entityUrn)
                  })
                }
                i(!1)
              })()
            },
05EvidenceTHIRD PARTY LIST
Hosts involved in the contact-sync path
  • www.linkedin.com

    LinkedIn Voyager API host queried for profile and supplementary profile data.

  • api.prod.surfe.com

    Surfe API host that receives the mapped CRM contact update.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-200
SourceAI SANDBOX

Surfe hooks LinkedIn fetch when li-protect is enabled

Surfe injects a page-level script on LinkedIn pages at document start.

When li-protect cookie is true, the script replaces window.fetch with a Proxy, checks URLs for the lnokhhhekhiapce marker, rewriting matches to the extension's index.js.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You browse LinkedIn while Surfe's li-protect cookie is set to true.

The extension did this

The extension injects a page script that replaces window.fetch and rewrites matching request URLs.

The rewrite applies when a fetched URL contains the extension-related marker lnokhhhekhiapce.

02EvidenceFIELD TABLE
Concrete fields used by the fetch hook
FieldValueWhy it matters
Activation cookie
li-protect=trueDetermines whether the URL-rewrite branch is active on the LinkedIn page.
Matched URL marker
lnokhhhekhiapceThe script checks request URLs for this extension-related marker before rewriting them.
Replacement URL
chrome-extension://xxxxxxxxxxxxxxxx/index.jsMatching requests are redirected to an extension resource instead of their original URL.
Hook marker
window.fetchReplaced = trueMarks that the page's fetch function has been replaced.
03EvidenceCODE COMPARE
The code that does this

LinkedIn page script injection and fetch replacement

What it actually does
Readable LinkedIn injectordeobfuscated/assets/scripts/boot.js:1
var host = location.hostname;
if (("linkedin.com" === host || host.endsWith(".linkedin.com")) && "text/html" === document.contentType) {
  var s = document.createElement("script");
  s.src = chrome.runtime.getURL("assets/scripts/index.js");
  s.onload = function() { this.remove(); };
  (document.head || document.documentElement).appendChild(s);
}
Readable decoded fetch hookdeobfuscated/assets/scripts/index.js:41-71
function getCookie(name) {
  const parts = "; " + document.cookie;
  const split = parts.split("; " + name + "=");
  if (split.length === 2) return split.pop().split(";").shift();
}
(function() {
  const originalFetch = window.fetch;
  const protectEnabled = getCookie("li-protect") === "true";
  window.fetch = new Proxy(originalFetch, {
    apply: function(target, thisArg, args) {
      if (args.length > 0 && typeof args[0] === "string" && typeof args[0].includes === "function") {
        if (protectEnabled && args[0].includes("lnokhhhekhiapce")) {
          args[0] = "chrome-extension://xxxxxxxxxxxxxxxx/index.js";
        }
        if (args[0].includes("/sales-api/salesApiProfiles/")) {
          return target.apply(thisArg, args).then(function(response) {
            const clone = response.clone();
            clone.json().then(function(json) {
              const data = json && json.data ? json.data : json;
              if (data && data.flagshipProfileUrl) {
                window._surfeFlagshipProfileUrl = data.flagshipProfileUrl;
                window.postMessage({ type: "surfe-salesnav-profile-resolved", flagshipProfileUrl: data.flagshipProfileUrl }, "*");
              }
            }).catch(function() {});
            return response;
          });
        }
      }
      return target.apply(thisArg, args);
    }
  });
  window.fetchReplaced = true;
})();
SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-829
SourceAI SANDBOX

Surfe loads server-controlled feature flags

We observed the extension request Surfe feature flags from https://api.prod.surfe.com/feature-flags on load.

The 8,695-byte response returned 59 flags, checked before enabling side panel, LinkedIn, template, CRM, and message features.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You load Chrome with the Surfe extension installed.

The extension did this

The extension requests feature flags from Surfe before you interact with a Surfe control.

Those returned flags are later checked to decide which extension features are active.

02EvidenceNETWORK CAPTURE
Captured request
GEThttps://api.prod.surfe.com/feature-flags
200 OK, 8,695-byte JSON response containing 59 feature flags.
03EvidenceFIELD TABLE
Feature-control fields observed in the response and source
FieldValueWhy it matters
Chrome side panel flag
CHROME_SIDEPANEL_ENABLED: trueControls whether the Surfe side panel behavior is enabled for your browser.
LinkedIn injection flag
HIDE_LI_INJECTION_ENABLED: trueControls whether a Surfe LinkedIn page integration is active in your browser.
Templates flag
INJECTED_TEMPLATES_ENABLED: trueControls whether injected templates are available while you use Surfe.
Feature endpoint
https://api.prod.surfe.com/feature-flagsIdentifies the Surfe server endpoint that supplies the configuration.
04EvidenceCODE COMPARE
The code that does this

The feature flag loader requests /feature-flags and reads flag values

What it actually does
Readable feature-flag loaderdeobfuscated/inject.js:34079-34147
hR = {
  CHROME_SIDEPANEL_ENABLED: { value: true },
  HIDE_LI_INJECTION_ENABLED: { value: true },
  UNIFY_SIGN_IN_ENABLED: { value: false },
  INJECTED_CONVERSATION_ENABLED: { value: true },
  INJECTED_TEMPLATES_ENABLED: { value: true },
  SIDEPANEL_MESSAGE_GENERATION_ENABLED: { value: true }
};
mR = ["feature-flags"];
fR = async () => {
  const { token } = await ww(["token"]);
  const { key, shouldResetCache } = ym(token);
  const result = await cf.fetchQuery({
    queryKey: [...mR, key],
    queryFn: () => (async reset => {
      const { token } = await ww(["token"]);
      const request = token ? Gf : Wf;
      const timestamp = reset ? (new Date).getTime() : void 0;
      const { data } = await request("GET", {
        url: "/feature-flags",
        params: timestamp ? { timestamp } : void 0
      });
      return "true" === "MISSING_ENV_VAR".FF_OVERRIDE_ENABLED
        ? { flags: { ...data.flags, ...hR } }
        : data;
    })(shouldResetCache),
    staleTime: 300000
  });
  return result;
};
wR = (featureFlags, flagName) => !!featureFlags.flags[flagName]?.value;
gR = async flagName => {
  const featureFlags = await fR();
  return wR(featureFlags, flagName);
};
05EvidenceTHIRD PARTY LIST
Remote configuration destination
  • api.prod.surfe.com

    Surfe API host that returns feature flags used by the extension.

+1 more finding not shown

Updated 30 September 2026kojhcdejfimplnokhhhekhiapceggamn