Skip to main content

Connected app hygiene after the Salesloft Drift and Gainsight token thefts

Between 8 and 18 August 2025, an actor tracked as UNC6395 used stolen OAuth tokens belonging to the Salesloft Drift integration to query Cases, Accounts, Users and Opportunities across hundreds of Salesforce customer orgs. They searched the exported data for cloud access keys, passwords and data-warehouse tokens, and deleted their query jobs afterwards. Salesforce revoked the tokens and removed Drift from AppExchange on 20 August; tokens for the Drift Email integration were also stolen.

In November it happened again. Salesforce revoked every access and refresh token for Gainsight-published apps and removed them from AppExchange. Google's threat intelligence group tied the activity to ShinyHunters. Salesforce's statement was precise: there was "no indication that this issue resulted from any vulnerability in the Salesforce platform".

Earlier in the year a third campaign needed no stolen token at all. UNC6040 phoned employees and talked them into entering a connection code that authorised a modified Data Loader, sometimes presented under a name like "My Ticket Portal". Three incidents, three routes in, and in each case the platform worked exactly as configured. The question for every admin is therefore what the configuration allows.

The problem is scope, not the vendor

A stolen token is only as dangerous as the grant behind it. A token scoped to one object for one integration user with a tight IP range gives an attacker a narrow window. A token with full scope plus refresh_token, issued to a user with administrator permissions and relaxed IP restrictions, gives them the org, indefinitely, from anywhere.

That second shape is common because it is the path of least friction at install time. The vendor's setup guide asks for broad scope so the integration never breaks, the admin accepts because the vendor is trusted, and nobody revisits the grant. The vendor was trusted in both 2025 cases. Trust in the vendor did not help, because the vendor's own systems were what got compromised.

So the useful inventory questions are not "which vendors do we use" but these:

  • Which connected apps hold full scope, and which of those also hold refresh_token?
  • Which integration users have admin-level permissions, and which of those have no login IP ranges?
  • Which apps run a client-credentials flow as an administrator with relaxed IP restrictions?
  • Who in the org can approve an app that has not been installed by an admin first?
  • Which tokens are currently valid for apps nobody has looked at this year?

Each of these is a yes or no per app or per user, and each one is readable from the org without a project.

What is visible in the metadata

If your connected apps are in source control, part of the inventory is a scan of the repository. Vulkro's connected-app checks report:

  • Full scope at High, raised when it is combined with RefreshToken, because that pair is the shape of both 2025 token-theft incidents.
  • An http:// callback URL at High (CWE-319), since the authorization code travels in the clear.
  • A hardcoded consumerSecret in metadata at Critical and a hardcoded consumerKey at Medium.

A connected app definition with the risky combination looks like this:

<ConnectedApp xmlns="http://soap.sforce.com/2006/04/metadata">
    <label>Vendor Sync</label>
    <oauthConfig>
        <callbackUrl>http://vendor.example/callback</callbackUrl>
        <scopes>Full</scopes>
        <scopes>RefreshToken</scopes>
    </oauthConfig>
</ConnectedApp>

The OAuth scope rules SF-OAUTH-SCOPE-001 to 008 go further than the single file. They report broad scopes with no IP or scope constraint, request input flowing into a Named Credential callout, refresh_token granted to a provider that is not verified, apps installable without admin pre-approval, and client-credentials flows that run as an administrator with relaxed IP restrictions. Run them with vulkro-sf scan ./force-app as part of the normal package scan.

Metadata has a limit, and it is worth being plain about it. A connected app that a user approved from the AppExchange, or an app the vendor hosts and your org only authorised, has no file in your repository. The Drift and Gainsight integrations were in that category for most customers. The repository scan tells you about the apps you built. It does not tell you about the apps you accepted.

What is only visible in the live org

For the accepted apps you need to ask the org itself. vulkro-sf org connected-apps --target-org <alias> lists every connected app the org knows about with its scopes, its policies and the users it is bound to, which answers the first two inventory questions above.

vulkro-sf org oauth-risk -o <alias> goes through the rest. According to the breach class map it checks each app's scope, its IP rules and token policy, who can approve uninstalled apps, the revoke list, LoginHistory for the integration users, and the app names present in the org against the indicators published for the 2025 campaigns. That last check is the one that would have caught a Data Loader named "My Ticket Portal".

How the live-org commands get access matters, since the whole topic is tokens. Vulkro never handles Salesforce credentials. It runs the Salesforce CLI against an alias you already authenticated, and the token stays in the Salesforce CLI's own store. There is no Vulkro connected app to add to the inventory you are trying to shrink.

The review that has to recur

A one-time cleanup after an incident is what most orgs did in August 2025, and then again in November. The lesson of the second incident is that the inventory decays: a new vendor arrives, a project needs a quick integration, the broad-scope grant comes back.

The practical cadence is a quarterly run of the two org commands with the output kept, so the diff between quarters is the review. New apps, new full grants and new integration users without IP ranges stand out against the previous report rather than against memory. Pair it with a rule that an integration user never carries an administrator profile, and that any app requesting full plus refresh_token needs a written reason that names the objects it actually reads.

For the wider picture of how these grants chain into a breach, how Salesforce orgs get breached shows the paths, and connected apps and OAuth tokens covers the integration surface as a whole.

What to do this week

  • Run vulkro-sf org connected-apps --target-org <alias> and sort by scope. Every full plus refresh_token grant needs an owner and a reason by Friday.
  • Run vulkro-sf org oauth-risk -o <alias> and clear the approver list first: nobody outside the admin team should be able to approve an uninstalled app.
  • Give every integration user a login IP range. If the vendor cannot supply one, that is a finding about the vendor.
  • Scan the repository with vulkro-sf scan ./force-app and remove any hardcoded consumerSecret before anything else.
  • Put the quarterly re-run in the calendar now, with the report filed somewhere the next run can diff against.

The live-org setup page explains the alias-based access model before you run anything. To run the inventory on your own org, start the 14-day trial.