Lateral Movement Paths
This page documents the attack patterns SFHound is designed to surface. Each path corresponds to one or more traversal queries you can run in BloodHound CE.
The general model
Every lateral movement path in Salesforce follows one of two patterns:
Permission escalation — A user holds or gains a permission that allows them to directly alter the org’s security posture (e.g., ManageProfilesPermissionsets → add ModifyAllData to a Permission Set → assign to self).
Data exfiltration — A user’s effective permissions, resolved through their Profile/PermissionSet/group membership, allow them to read or export data they should not have access to.
The SFHound graph resolves both patterns through traversal queries.
Path 1 — Profile/PermissionSet chain to system permission
The most common path. A user is assigned a Permission Set Group containing a Permission Set that grants a dangerous system permission.
SFUser ──AssignedPermissionSetGroup──► SFPermissionSetGroup │ IncludesPermissionSet ▼ SFPermissionSet │ ModifyAllData ▼ SFOrganizationDetection query:
MATCH p=(u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps)-[:ModifyAllData|AuthorApex|ManageUsers|ManageProfilesPermissionsets]->(org:SFOrganization)WHERE (ps:SFProfile OR ps:SFPermissionSet)RETURN pLIMIT 500Path 2 — ManageProfiles self-escalation
A user with ManageProfilesPermissionsets can add ModifyAllData to any Permission Set, then assign that set to themselves.
Compromised user └── ManageProfilesPermissionsets ──► SFOrganization
Attacker action: edit SFPermissionSet → add ModifyAllData edge assign SFPermissionSet to self → gains ModifyAllDataDetection query — who can manage profiles?
MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet]->(ps)-[:ManageProfilesPermissionsets]->(org:SFOrganization)WHERE ps:SFProfile OR ps:SFPermissionSetRETURN pathPath 3 — ManageUsers → create admin
A user with ManageUsers can create a new user with a System Administrator profile, or re-activate a dormant admin account.
Detection query:
MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet]->(ps)-[:ManageUsers]->(org:SFOrganization)WHERE ps:SFProfile OR ps:SFPermissionSetRETURN pathHardening: Pair with enhanced transaction security policies that alert on UserCreate events by non-admin principals.
Path 4 — AuthorApex → arbitrary code execution
An identity with AuthorApex can deploy a trigger or class that runs in system context, bypassing field-level security and sharing. From there:
- Create a backdoor admin user
- Exfiltrate data via HTTPS callout to an attacker-controlled server
- Delete or overwrite audit records
Detection query:
MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps)-[:AuthorApex]->(org:SFOrganization)WHERE (ps:SFProfile OR ps:SFPermissionSet)RETURN pathPath 5 — ViewAllData + ApiEnabled → bulk exfiltration
Neither permission alone is necessarily alarming. Together, they allow a service account to extract every record via the REST or Bulk API without triggering row-level sharing checks.
Detection query:
MATCH (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps1)-[:ViewAllData]->(org:SFOrganization), (u)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps2)-[:ApiEnabled]->(org)WHERE (ps1:SFProfile OR ps1:SFPermissionSet) AND (ps2:SFProfile OR ps2:SFPermissionSet)RETURN DISTINCT u.name AS User, u.Username AS UsernamePath 6 — CanModifyAll on private object → sharing bypass
Even with a private OWD, a user with CanModifyAll on a private object can read, edit, and delete all records of that type, bypassing role hierarchy and sharing rules entirely.
SFUser ──AssignedPermissionSet──► SFPermissionSet │ CanModifyAll ▼ SFSObject (InternalSharingModel: Private)Detection query:
MATCH (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(p)-[:CanModifyAll]->(obj:SFSObject)WHERE obj.InternalSharingModel = "Private"AND (p:SFPermissionSet OR p:SFProfile)RETURN DISTINCT u.name AS User, obj.name AS Object, obj.Label, obj.InternalSharingModelORDER BY u.namePath 7 — OAuth Connected App abuse
A user with CanAuthorize on a Connected App that has AdminApprovedUsersOnly = False can self-authorise and obtain a long-lived OAuth token. If the Connected App has permissive scopes (e.g., full), that token grants the effective permissions of the user to any third-party service.
SFUser ──AssignedProfile──► SFProfile │ CanAuthorize ▼ SFConnectedApp (AdminApprovedUsersOnly: false)Detection query:
MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet*1..2]->(ps)-[:CanAuthorize]->(app:SFConnectedApp)WHERE app.AdminApprovedUsersOnly = FalseRETURN pathPath 8 — Role hierarchy record visibility escalation
A user assigned to a high position in the role hierarchy can read all records owned by users in any descendant role — even without explicit sharing or ViewAllData.
Detection: who sits at the root of the hierarchy?
MATCH (r:SFRole)WHERE NOT ()-[:InheritsRole]->(r)MATCH (u:SFUser)-[:HasRole]->(r)RETURN u.name, r.Name AS TopRolePath 9 — Nested Public Group membership
A user added to a low-trust Public Group that is nested inside a high-trust group gains all the sharing-rule visibility and Connected App access of the outer group.
Detection: deep group membership paths
MATCH path = (outer:SFGroup)-[:HasMember*2..5]->(u:SFUser)RETURN pathLIMIT 200Chained path example — full escalation story
Compromised service account └── AssignedPermissionSet → "Integration_Read_Only" PermissionSet └── ManageProfilesPermissionsets → SFOrganization (misconfigured!) │ Attacker edits "Integration_Read_Only": replaces single scoped permission with ModifyAllData │ └── ModifyAllData → SFOrganization └── Bulk-exports all records via REST APIThis exact chain is detectable by traversing all paths from any user to ManageProfilesPermissionsets and checking if the same user also has ApiEnabled.