Log Entry
Apex User Mode Is Not Your Whole Security Model
Apex user mode is a very good default.
It is not a security architecture by itself.
In API version 67.0 and later, database operations default to user mode and classes default to sharing enforcement. That removes a large category of accidental elevated access.
It does not answer every security question an application has.
List<Bank_Account__c> accounts = [
SELECT Id, Balance__c
FROM Bank_Account__c
WITH USER_MODE
];
This query can enforce the running user's access to the object, fields, and records.
It does not prove that this particular application operation should return every accessible bank account, accept the supplied filter, or disclose the result in this context.
Quick snapshot
User mode helps enforce:
- object permissions
- field-level permissions
- record access
- newer access controls included in the platform's user-mode evaluation
Secure Apex still needs:
- an explicit sharing and access-mode design
- authorisation for the business operation
- validation of all caller-supplied input
- safe dynamic query construction
- output minimisation
- controlled error messages
- tests using representative non-admin users
- narrow, reviewed system-mode boundaries where elevation is required
Salesforce's secure Apex guidance also separates record sharing from object and field permissions and recommends making access modes explicit for maintainability.
Accessible does not always mean authorised
Platform permissions answer broad access questions.
Application rules can be narrower.
A service agent might have read access to Case and its fields but should only execute "issue refund" when:
- the case belongs to the relevant region
- the order is within the refund window
- the amount is below their approval threshold
- the case has not already been refunded
Those are domain authorisation rules.
User mode cannot infer them from field permissions.
Keep them in the server-side command even if the LWC hides the button. A caller can invoke an exposed Apex method without following the intended clicks.
Treat every client value as untrusted
An LWC calling Apex is a network boundary, even though both parts live in the same Salesforce project.
Do not trust:
- record IDs
- requested field names
- sort fields
- filter expressions
- prices or calculated totals
- booleans claiming that validation already passed
- object API names
The server should derive sensitive values from trusted records and allow-list any metadata-driven inputs.
private static final Set<String> ALLOWED_SORT_FIELDS = new Set<String>{
'CreatedDate',
'Priority',
'Status'
};
private static String requireSortField(String requestedField) {
if (!ALLOWED_SORT_FIELDS.contains(requestedField)) {
throw new RequestException('Unsupported sort field.');
}
return requestedField;
}
User mode does not turn unsafe string concatenation into safe query construction.
Sharing and CRUD/FLS are different layers
Class sharing controls record-level visibility.
Database access mode controls object and field permissions for the operation, alongside applicable record access.
Use explicit declarations where possible:
public with sharing class CaseSearchService {
public static List<Case> search(String term) {
return [
SELECT Id, CaseNumber, Subject
FROM Case
WHERE Subject LIKE :('%' + term + '%')
WITH USER_MODE
LIMIT 100
];
}
}
The explicit with sharing and WITH USER_MODE make the intended boundary readable even when defaults or API versions change.
Use stripInaccessible() when graceful degradation is the contract
Strict user-mode queries are useful when missing access should fail the operation.
Sometimes the contract is to return the accessible subset of fields or sanitise incoming records before DML. Security.stripInaccessible() supports that shape and lets code inspect which fields were removed.
That choice must be deliberate.
Silently stripping a field from an update can be confusing if the user believes the full command succeeded. If a removed field changes the meaning of the operation, return a clear permission error instead.
Graceful degradation is a product decision, not merely an exception-avoidance technique.
Minimise the response
Do not return an sObject full of fields just because the user can technically read them.
Return the fields the component needs.
A focused DTO can make that contract explicit:
public class CaseSummary {
@AuraEnabled public Id id;
@AuraEnabled public String caseNumber;
@AuraEnabled public String subject;
@AuraEnabled public String status;
}
This reduces accidental coupling and makes security review easier. A new sensitive field added to an internal query does not automatically cross the client boundary.
System mode should look unusual
Some operations genuinely require elevation:
- a controlled maintenance process
- a reconciliation service
- an internal audit write
- an operation that must update a protected system-owned field
Make the elevation narrow and visible.
I prefer a small method or class whose name, comments, and tests explain:
- why elevation is required
- which data is elevated
- which checks happen before elevation
- what is written or returned
- how the action is audited
Do not mark a large service without sharing because one line needs elevated access.
Privilege should have a small blast radius.
Errors can leak too
Returning raw exception text can disclose field names, query details, record IDs, or internal implementation.
Log the technical exception with a correlation reference. Return a controlled message to the caller.
Security includes what the application reveals when it fails.
Test as the user who matters
Tests run with substantial access unless you deliberately create and use representative users.
Use System.runAs() to exercise record-sharing scenarios, and create permission arrangements appropriate to the code under test. Cover:
- a user with the intended access
- a user missing object access
- a user missing a sensitive field
- a user who can see the record but cannot perform the domain action
- the explicit elevated path, if one exists
A passing system-context test proves the logic runs. It does not prove the access model is correct.
The right mental model
User mode is the database access floor.
On top of it, the application still needs to decide:
- Is this caller allowed to perform this operation?
- Is the request safe and valid?
- What is the minimum data the caller needs?
- Does any step require elevation?
- What can be revealed on failure?
Secure defaults are valuable because they make mistakes harder.
They do not make those design questions disappear.