A Salesforce user has profile-level access to the Account object but not to the Contact object. They visit a page that lists Accounts and, for each Account, shows the primary contact. The page should fail to show contact data for that user; instead, contact records appear. The bug: the Apex controller does not enforce field-level security or CRUD checks, so it returns data the user is not entitled to see.
This is the canonical CRUD/FLS bypass. Apex code runs with elevated permissions by default. The user's profile and permission sets matter for declarative work (page layouts, list views), but Apex queries and DML bypass these unless the code explicitly enforces them.
Two patterns enforce CRUD and FLS in Apex. They have different ergonomics, different audit implications, and different applicability. Knowing when to use which is the difference between Apex code that passes Salesforce's Security Review (required for AppExchange) and code that fails.
What "without enforcement" looks like
A typical Apex method that violates CRUD/FLS by default:
public with sharing class ContactController {
@AuraEnabled
public static List<Contact> getContactsForAccount(Id accountId) {
return [SELECT Id, FirstName, LastName, Email, Phone
FROM Contact
WHERE AccountId = :accountId];
}
}
The class uses with sharing, which enforces row-level sharing rules. But field-level security and object-level CRUD are not enforced. A user without read access to Contact.Email will see emails anyway. A user without object-level read on Contact will see Contacts (sharing-allowed records) anyway.
The with sharing keyword is necessary but not sufficient. CRUD and FLS need their own enforcement.
Pattern 1: WITH SECURITY_ENFORCED
Added to SOQL in 2019 and stable since. The query refuses to execute if the running user lacks CRUD on any queried object or FLS on any queried field.
public with sharing class ContactController {
@AuraEnabled
public static List<Contact> getContactsForAccount(Id accountId) {
return [SELECT Id, FirstName, LastName, Email, Phone
FROM Contact
WHERE AccountId = :accountId
WITH SECURITY_ENFORCED];
}
}
Behavior:
- User lacks read on Contact:
QueryExceptionat runtime. - User lacks FLS on Email:
QueryExceptionat runtime. - All accessible: query returns rows normally.
What's good: one keyword, all CRUD and FLS in scope. Compile-time errors for typos. The query either fully complies or fails loudly.
What's limited:
- Only works in inline SOQL, not in Database.query.
- Only enforces on read. DML still requires separate enforcement.
- The failure is binary: query throws. The caller has to handle the exception. No partial result.
WITH SECURITY_ENFORCED is the right choice when the calling code expects all-or-nothing access. The user either sees all the data or sees an error.
Pattern 2: Security.stripInaccessible
Introduced shortly after WITH SECURITY_ENFORCED, this approach strips inaccessible fields from a record collection instead of throwing.
public with sharing class ContactController {
@AuraEnabled
public static List<Contact> getContactsForAccount(Id accountId) {
List<Contact> contacts = [SELECT Id, FirstName, LastName, Email, Phone
FROM Contact
WHERE AccountId = :accountId];
SObjectAccessDecision decision = Security.stripInaccessible(
AccessType.READABLE, contacts
);
return decision.getRecords();
}
}
Behavior:
- User lacks read on Contact:
decision.getRecords()returns an empty list. - User lacks FLS on Email: the Email field is stripped from every returned record. Other fields visible normally.
- All accessible: every record and field returned.
What's good: graceful degradation. The user sees what they are entitled to see. Useful for read scenarios where partial access is meaningful.
What's tricky:
- Works for read, create, update, and delete (
AccessType.CREATABLE, etc.). - Result is wrapped in
SObjectAccessDecision. Caller has to extract viagetRecords()(orgetRemovedFields()for diagnostics). - The stripped fields are unset on the returned records. Apex code that subsequently accesses them gets null. Test for this.
Security.stripInaccessible is the right choice when the calling code can handle missing data gracefully (e.g., a list view that just shows fewer columns for users with restricted FLS).
Pattern 3: USER_MODE vs SYSTEM_MODE (newer)
Salesforce introduced explicit access mode modifiers in Spring 2023:
List<Contact> contacts = [SELECT Id, Email FROM Contact
WHERE AccountId = :accountId
WITH USER_MODE];
WITH USER_MODE is roughly equivalent to combining WITH SECURITY_ENFORCED plus sharing enforcement. The query runs as if the current user issued it: CRUD, FLS, and sharing rules all apply.
WITH SYSTEM_MODE is the inverse: explicit acknowledgment that the query bypasses all of those. Used when the code legitimately needs elevated access (e.g., a status-update operation that runs on behalf of all users).
For new code, prefer WITH USER_MODE as the default. Use WITH SYSTEM_MODE only when justified, and document why.
DML supports the same pattern via Database.insert(records, AccessLevel.USER_MODE) and similar.
When each pattern fits
A practical decision guide:
| Scenario | Pattern |
|---|---|
| Standard read query, all-or-nothing access | WITH USER_MODE (preferred) or WITH SECURITY_ENFORCED |
| Read query, graceful degradation OK | Security.stripInaccessible after the query |
| DML on user-provided data | Database.insert(records, AccessLevel.USER_MODE) or Security.stripInaccessible before insert |
| Legitimately elevated operation (system job) | WITH SYSTEM_MODE with explicit documentation |
| Dynamic SOQL (Database.query) | Security.stripInaccessible (since WITH clauses do not work in dynamic SOQL) |
The pattern selected should match the operational reality: does the user expect to see all the data or be told nothing is available, or can the UI handle partial visibility.
Sharing vs CRUD/FLS
Three orthogonal security layers in Apex:
| Layer | What it controls | Enforced by |
|---|---|---|
| Sharing | Which records the user can see (row-level) | with sharing keyword |
| CRUD | Which objects the user can read/create/update/delete | WITH SECURITY_ENFORCED, USER_MODE, stripInaccessible |
| FLS | Which fields the user can see on accessible objects | Same as CRUD |
All three should be enforced in standard Apex code. with sharing is necessary but only covers one layer. The other two need separate enforcement.
The without sharing keyword exists for cases where the code legitimately needs to bypass sharing (e.g., a customer service operation that needs to see all cases regardless of ownership). It is rarely the right answer; document the justification when used.
inherited sharing is a third option that inherits the calling context's sharing mode. Useful for utility classes called from many places.
Common security mistakes
Five patterns Sapota has seen in audits:
with sharingalone, no CRUD/FLS enforcement. Sharing covers rows but FLS is bypassed. Users see fields they should not.without sharingas a default. Class declaredwithout sharing"just in case." Every method runs with elevated access regardless of need.- Dynamic SOQL with no stripInaccessible. WITH clauses do not work in
Database.query. The query runs as system. AddSecurity.stripInaccessibleafter. - DML without access checks.
insert records;runs without checking if the user is allowed to create those records. UseDatabase.insert(records, AccessLevel.USER_MODE). - Security check, then ignored result.
Security.stripInaccessible(...)called but the original (non-stripped) list is returned to the caller. The check was theater.
Security Review for AppExchange
Apex code in an AppExchange-bound package must pass Salesforce Security Review. The review specifically checks CRUD/FLS enforcement. Code that bypasses these by default fails the review.
The patterns that pass:
- Every SOQL query uses WITH USER_MODE or WITH SECURITY_ENFORCED, or every result passes through stripInaccessible.
- Every DML statement uses Database.insert/update with USER_MODE, or records pass through stripInaccessible before DML.
without sharingis used sparingly with documented justification.with sharingis the default on all controller-style classes.
For ISVs preparing for Security Review, the audit pass should explicitly grep for unenforced queries and DML. Tools like Salesforce Code Analyzer (PMD-based) flag these patterns.
What good Apex security looks like
A Salesforce org or managed package with healthy security discipline:
with sharing(orinherited sharing) is the default on every controller-style class.- Every SOQL on user-touched data uses USER_MODE, SECURITY_ENFORCED, or stripInaccessible.
- Every DML on user-provided data enforces CRUD via the same patterns.
without sharingand SYSTEM_MODE used with documented justification.- Code review checks security patterns explicitly.
Sapota's Salesforce team holds the Platform Developer I credential and treats CRUD/FLS enforcement as a code review gating criterion. The patterns are well-defined; the discipline is what keeps Apex code passing internal audits and Security Review.
Auditing Apex code for CRUD/FLS compliance or preparing for Security Review? Sapota's Salesforce team handles security audits, refactor design, and AppExchange Security Review prep on production engagements. Get in touch ->
See our full platform services for the stack we cover.








