Least privilege and privilege escalation
Vulkro scores how far each identity in your org drifts from least
privilege, and where Apex code lets a user reach past the privilege the
platform actually granted them. The signals on this page split into
three groups: static least-privilege signals on profiles, permission
sets, and session policy (the sf-lp-* family); a per-user effective
FLS audit that composes Profile plus Permission Set assignments into a
single "what can this human really read and edit" picture
(SF-FLS-USER-001 / -002); and system-context privilege escalation
in Apex, where without sharing code or a runtime assignment lets a
caller elevate beyond their own grant (SF-PRIVESC-SYS-*).
sf-lp-* least-privilege signals
These are configuration-level findings. Each one is a specific shape that pushes an identity above the access it needs.
sf-lp-fls-overgrant: a profile or permission set grants field-leveledit(orreadon a tier-1 PII field) on a field that the assigned users have no business need to touch. The finding names the granting entity, the object, the field, and the access level so a reviewer can revoke the single field rather than the whole permission set.sf-lp-toxic-combo: the toxic admin combination. A single identity that holdsAuthorApexANDModifyAllDataANDApiEnabledat the same time. Each alone is defensible; together they let one account write Apex that runs in system context, touch every record, and drive the org over the API with no UI footprint. This is the highest-value account for an attacker to phish, and the combination is rarely required for a real human user.sf-lp-custom-perm-broad: a custom permission that gates a sensitive capability is enabled on a broad audience (a profile assigned to many users, or a permission set with a large assignee count) rather than a tightly scoped group. Broad custom permissions defeat the point of gating a feature behind one.sf-lp-session-not-high-assurance: a permission set or profile that grants a sensitive capability (for example one of the toxic-combo perms) but does not require a high-assurance session. The capability is reachable from a standard-assurance session, so a replayed cookie reaches it without a step-up.sf-lp-integration-user-no-ip: an integration user (a user whose profile or permission set carriesApiEnabledand is used by a Connected App or named credential) with no login IP range restriction. The account can authenticate from anywhere on the Internet, which is the precondition that made vendor-side OAuth token theft tractable in the published 2025-26 incidents.
Guest-reachable privileged action: SF-GUEST-AURA-001
The site guest user runs unauthenticated. When a guest profile or
permission set grants an Apex class via classAccesses, every
@AuraEnabled method on that class is reachable by an anonymous visitor.
SF-GUEST-AURA-001 pairs that grant with the class body and flags an
@AuraEnabled method that performs a privileged operation - DML
(insert / update / delete), an async job (System.schedule /
enqueueJob / Database.executeBatch), or a dynamic Database.query -
with no real authorization gate (WITH SECURITY_ENFORCED / USER_MODE /
Security.stripInaccessible / a CRUD describe check / a custom-permission
check). The result is an unauthenticated privileged action (CWE-862). The
guest signal is taken from the profile/permission-set path (relative to the
scan root) and its guestUser / sharingGuestRules body, and a
WITH USER_MODE fragment that lives only inside a SOQL string literal is
not mistaken for a real enforcement clause.
Per-user effective FLS audit: SF-FLS-USER-001 / -002
The sf-lp-fls-overgrant signal looks at one granting entity at a time.
The per-user audit answers a harder question: across every Profile
and Permission Set assigned to a given user, what is the union of fields
that user can effectively read or edit? A user can be under the bar on
each permission set individually and still end up with broad effective
access once the assignments are composed.
SF-FLS-USER-001(broad effective read): the union of read-enabled fields across the user's Profile plus all assigned Permission Sets exceeds the broad-access bar. Reported with the contributing entities so you can see which assignment pushed the user over.SF-FLS-USER-002(broad effective edit): the same composition for field-leveledit. Edit is the higher-severity half because it carries data-integrity and tampering risk on top of disclosure.
Why it matters. Per-entity FLS review misses the additive case: the classic "permission set sprawl" pattern where a long-tenured employee accumulates ten permission sets, none of which is individually alarming, whose union grants read or edit on most of the data model.
Tuning. The broad-access bar (the number of effective fields above
which a user is flagged) is configurable through the
VULKRO_SF_FLS_USER_BROAD_BAR environment variable. Raise it for orgs
with intentionally wide power-user roles; lower it for high-sensitivity
environments where any broad effective grant should surface.
System-context privilege escalation: SF-PRIVESC-SYS-001 / -002
These two findings cover Apex that lets a caller act above their own grant.
SF-PRIVESC-SYS-001(without-sharing DML on request data, no CRUD/FLS check): an Apex class declaredwithout sharing(or inheriting system context) that performs DML using values taken from request data (an Aura/LWC@AuraEnabledmethod, a REST resource, a Visualforce controller action) without first checking CRUD or FLS on the affected object and fields. Because the class runs in system context, the platform does not enforce the caller's sharing or field-level security, so the caller writes records they could never edit through the UI. Fix by enforcingWITH SECURITY_ENFORCED(orSecurity.stripInaccessible) and an explicitSchema.sObjectType...isUpdateable()/isCreateable()check before the DML, or by running the workwith sharingwhen the caller's own access is the intended boundary.SF-PRIVESC-SYS-002(runtime PermissionSetAssignment self-elevation): Apex that inserts aPermissionSetAssignment(or aPermissionSetGroupAssignment) at runtime, granting a permission set to a user based on request-controlled input. A method that lets the caller pick which permission set to assign, or which user to assign it to, is a direct privilege-escalation primitive: the caller grants themselves (or an account they control) a powerful permission set without an admin in the loop. Fix by removing runtime assignment of privileged permission sets, hard-coding the assignable set to a vetted, non-privileged allow-list, and never deriving the target user from request data.
Why it matters. Both findings describe the same failure: code that runs above the caller and trusts the caller's input. System context is a deliberate, powerful tool; the bug is using it without re-imposing the access checks the platform would otherwise have enforced.
Example: a triggering pattern
public without sharing class AccessGrantController {
@AuraEnabled
public static void grant(Id userId, Id permSetId) {
// No check that the caller may assign this permission set,
// and both ids come straight from the client.
insert new PermissionSetAssignment(
AssigneeId = userId,
PermissionSetId = permSetId
);
}
}
SF-PRIVESC-SYS-002 fires: an @AuraEnabled method on a system-context
class inserts a PermissionSetAssignment from request-controlled ids.
Example: a pattern that does not trigger
public with sharing class SelfServiceController {
@AuraEnabled
public static void updateMyPhone(String phone) {
User u = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId()];
if (Schema.sObjectType.User.fields.Phone.isUpdateable()) {
u.Phone = phone;
update u;
}
}
}
The class runs with sharing, the target record is the caller's own
user, and an FLS check guards the write. No privilege-escalation finding
emits.
Where to go next
- Profiles and permission sets: the per-identity over-privilege layer
the
sf-lp-*signals build on. - Sharing-rule field and record-type leakage: the configuration side of broad effective access.
- OWASP guidance on access control failures: https://owasp.org/Top10/A01_2021-Broken_Access_Control/