Verified 2026-09 against https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_keywords_sharing.htm and https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_perms_enforcing.htm.
Three separate things
Apex runs in system mode by default. "Secure" means enforcing all three of these, and each needs a different construct:
| What | Construct | Covers | Does not cover |
|---|---|---|---|
| Record access (sharing rules, role hierarchy, OWD) | with sharing on the class |
Which rows the user can see | Object or field permissions |
| Object and field permissions (CRUD/FLS) | WITH USER_MODE on SOQL, AccessLevel.USER_MODE on DML |
Which objects and fields the user may read or write | Nothing beyond the operation it is on |
| Data already fetched in system mode | Security.stripInaccessible |
Removes fields and records the user cannot access | Does not re-run the query |
The single most common mistake is assuming with sharing is enough. It is not: a class declared
with sharing will still happily read a field the running user has no access to.
Class-level sharing
public with sharing class OrderService { } // enforces sharing rules
public with sharing class OrderSelector { } // selectors declare it too
public without sharing class BatchReconciler { } // system mode — justify it
with sharing— the default choice for anything reached from a user action, and for shared services and selectors.without sharing— deliberate elevation only. Every use needs a comment saying why, and the method should be narrow enough that the elevation is contained.- The org standard recognises these two keywords only.
inherited sharingexists on the platform but is not used here: the reviewer's question is always "which mode does this class run in", and an explicit answer is cheaper than tracing the caller. A class with no declaration at all defaults to system mode when entered directly, so the keyword is never optional.
Note: sharing declarations are not inherited by inner classes from the outer class in the way people expect — declare inner classes explicitly.
Query and DML access level
// Static SOQL
List<Account> accounts = [SELECT Id, Name FROM Account WITH USER_MODE];
// Dynamic SOQL — bind variables, never concatenation
String q = 'SELECT Id, Name FROM Account WHERE Industry = :industry';
List<Account> results = Database.queryWithBinds(
q,
new Map<String, Object>{ 'industry' => industry },
AccessLevel.USER_MODE
);
// DML
Database.insert(newAccounts, AccessLevel.USER_MODE);
Database.update(changed, AccessLevel.USER_MODE);
AccessLevel.SYSTEM_MODE is available and explicit — use it where elevation is intended, so that the
intent is visible at the call site rather than implied by its absence.
Migrating WITH SECURITY_ENFORCED
WITH USER_MODE supersedes WITH SECURITY_ENFORCED. It:
- covers polymorphic lookup fields, which
SECURITY_ENFORCEDdoes not, - applies to DML through
AccessLevel, giving one consistent model, - produces clearer exceptions.
Migration rule: replace, never combine.
// Before
List<Contact> cs = [SELECT Id, Email FROM Contact WITH SECURITY_ENFORCED];
// After
List<Contact> cs = [SELECT Id, Email FROM Contact WITH USER_MODE];
Writing both clauses on one query is redundant and signals that nobody was sure which applied.
Security.stripInaccessible
For the case where a query genuinely ran in system mode but the result goes back to a user:
SObjectAccessDecision decision = Security.stripInaccessible(
AccessType.READABLE,
systemModeRecords
);
return decision.getRecords();
- Use
AccessType.READABLEbefore returning,CREATABLE/UPDATABLEbefore DML on user-supplied data. getRemovedFields()tells you what was stripped — useful in tests to assert enforcement actually happened.- It strips fields; it does not add back rows the user should have seen, and it does not make a system-mode query safe if the rows were the problem. If you can run the query in user mode, do that instead.
Where enforcement is easy to miss
@AuraEnabledmethods — the entry point from any component, including from a guest user on a public site. Each one needs the full set.- Apex REST / SOAP endpoints — same, plus input validation.
- Batch, Queueable and Schedulable — run as the user who enqueued them but commonly in system mode; be explicit about which access level each query should use.
- Triggers — run in system mode by design. Enforcement belongs at the service entry point, not in the trigger.
- Flows calling invocable Apex — the invocable method's class declaration decides sharing.
Testing enforcement
Assert it, do not assume it:
@IsTest
static void restrictedUserSeesNothing() {
User u = TestDataFactory.restrictedUser();
System.runAs(u) {
Test.startTest();
List<Order> result = OrderService.recentOrders(accountId);
Test.stopTest();
Assert.areEqual(0, result.size(), 'Restricted user should see no orders');
}
}
A test that only runs as the admin proves nothing about enforcement — the admin passes every check.