[readonly] markdown buffer
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.
List<Bank_Account__c> accounts = [
SELECT Id, Balance__c
FROM Bank_Account__c
WITH USER_MODE
];
This can enforce the running user's access to the object, fields, and records. It does not prove that this application should return every accessible account, accept the supplied filter, or disclose the result in this context.
User mode answers platform access questions. The application still has to authorise the operation.
Access is not the same as authority
A service agent may be able to read a Case but only issue a refund when the order is within the refund window, below their approval threshold, and not already refunded.
Those are domain rules. User mode cannot infer them from permissions, and hiding a button in LWC does not enforce them. Keep the checks in the server-side command.
Salesforce's secure Apex guidance separates record sharing from object and field permissions. Make both boundaries visible:
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
];
}
}
Explicit access modes remain readable when defaults or API versions change.
The client is still untrusted
An LWC call is a network boundary. Treat record IDs, sort fields, filter expressions, object names, prices, and booleans claiming validation passed as untrusted input.
Derive sensitive values from records and allow-list metadata-driven choices.
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 make unsafe string concatenation safe.
Return only the data the client needs. A focused DTO prevents a new sensitive query field from accidentally crossing the boundary. Control error messages too: log technical details with a correlation reference rather than returning raw exceptions containing field names, IDs, or query information.
Elevation should be obvious
Some maintenance, reconciliation, and audit operations genuinely require system mode.
Keep that elevation narrow. The method or class should explain why it exists, which checks run first, what data is elevated, and how the action is audited. Do not make a large service without sharing because one line needs privilege.
Security.stripInaccessible() is useful when graceful degradation is the product contract. If silently dropping a field changes the operation's meaning, fail clearly instead. Degradation is a product decision, not an exception-avoidance trick.
Test the boundary you designed
Use representative non-admin users. Cover the intended user, missing object or field access, a visible record with a forbidden domain action, and any explicit elevated path.
A passing system-context test proves the logic runs. It does not prove the access model is correct.
User mode is the database access floor. Secure Apex still has to answer whether this caller may perform this operation, whether the request is safe, what minimum data is needed, where elevation occurs, and what failure may reveal.
Secure defaults make mistakes harder. They do not make those questions disappear.