Apex Security & Critical Updates
Your Apex API version is now a security control
The version number can change who sees what
A team raises the API version of an Apex class to adopt a new language feature, runs its familiar happy-path tests and deploys. The code still compiles, but users now receive access errors—or operations that previously relied on elevated behaviour stop working. Nothing obvious changed in the business logic. The security defaults did.
Salesforce’s current Secure Apex Classes guidance draws a hard boundary. In API version 67 and later, Apex runs in user mode by default: current-user object permissions and field-level security are enforced, and record-sharing rules are applied by default. In API version 66 and earlier, Apex runs in system mode by default and developers must enforce those layers. The JSBC Labs position is simple: an API-version upgrade is now a security migration, not routine metadata housekeeping.
Inventory the security boundary before editing code
List the API version, sharing declaration and entry point for every security-relevant class. Include Aura and Lightning controllers, REST resources, invocable actions, scheduled and asynchronous jobs, domain services and selectors. Map the calling personas, required data and any operation that intentionally crosses the caller’s normal access boundary.
This inventory reveals mixed-version call chains, which are likely during gradual adoption. Do not assume one org-wide default or infer behaviour from the newest class. Record the access expectation at each database operation. A class that must respect the caller and a reconciliation process that examines restricted records have different contracts.
Sharing is only one access layer
Historically, teams often treated with sharing as proof that an Apex controller was secure. It controls record-level sharing behaviour; it does not, by itself, establish object CRUD or field-level access in older API versions. A user could be allowed to see an Account record while lacking permission to read its margin field or update the object at all.
Salesforce Well-Architected describes four access layers: organisation, object, field and record. Review them separately. For pre-67 code, identify how CRUD and field security are enforced as well as the class sharing mode. For 67+ code, verify that default user-mode behaviour matches the intended contract. Security review should ask which layer authorises each data operation, not whether a reassuring keyword appears at the top of the file.
Make access intent explicit
Defaults reduce boilerplate, but explicit intent makes sensitive code reviewable. Salesforce supports user-mode and system-mode database operations for SOQL, SOSL and DML. Use user mode for user-initiated work unless a documented business requirement demands otherwise. It produces a clear failure when the caller lacks required access instead of silently returning or modifying more data than the UI would permit.
Use system mode as a narrow privilege boundary, not a convenience for making tests pass. Keep the privileged operation small, validate its inputs, constrain the records and fields it touches, and return only the minimum result. Name the business authority behind the elevation and capture it in an architecture decision. A reviewer should be able to explain why the user cannot perform the operation directly and why this exact code may do so on their behalf.
Choose failure or graceful degradation deliberately
User-mode access is appropriate when the operation must succeed completely or fail clearly. A request to update a restricted field should normally return an authorised, actionable error—not quietly claim success. That fail-fast behaviour protects the transaction contract and helps callers distinguish invalid data from insufficient access.
Security.stripInaccessible serves a different need. Salesforce documents it for removing fields the user cannot access, including sanitising sObjects deserialised from an untrusted source. It supports graceful degradation when returning a permitted subset is legitimate. Inspect the removed-field result and decide whether partial processing is safe. Never strip a required field, continue the transaction and hide the fact that the business outcome changed.
Treat sharing declarations as interface design
Declare sharing behaviour on service entry points instead of relying on ambiguous inheritance. Salesforce documents inherited sharing as an explicit way for a class to run in the caller’s sharing mode. That can be appropriate for reusable services, but only when the caller is a trusted part of the contract and the resulting variation is understood.
For an exposed controller or web-service class, prefer a declaration that communicates the boundary clearly. Trace helper classes that perform queries rather than assuming the entry class protects everything beneath it. If a reusable library supports both ordinary and privileged use, separate those operations into distinct methods or classes. One method whose security changes invisibly with its caller is difficult to test and dangerous to reuse.
Do not confuse identity with authorisation
An integration user, administrator or automated process may possess broad permissions, but that does not make every use of those permissions legitimate. Conversely, a business user’s action may need a carefully bounded system operation, such as applying a controlled ledger adjustment or completing a compliance workflow. The actor identifies who initiated work; the code’s access mode determines what the operation can do.
Define an authorisation policy for each entry point before selecting the execution mode. Check business preconditions as well as platform permissions. Where elevation is justified, record the initiating identity, operation and outcome without logging sensitive field values. Least privilege applies to code paths as much as permission sets: broad execution power should not leak into unrelated reads, responses or downstream actions.
Test denial as a first-class outcome
Administrator-only Apex tests prove little about an access model. Build test users that represent the actual permission combinations: permitted record and field, permitted record but restricted field, object access without record access, read without update, and no access. Assert both successful outcomes and the exact failure or redaction behaviour expected for each persona.
Add regression tests before changing a class API version. Exercise the public entry point, not only a selector in isolation, and include bulk operations and asynchronous continuations. Assert that responses do not serialise inaccessible fields and that failed authorisation leaves no partial writes. A security test is valuable only when it can fail because the boundary moved—not when every scenario executes as a system administrator.
Migrate by risk, not by folder
Do not bulk-upgrade every Apex class simply to make metadata versions consistent. Prioritise externally exposed controllers, services handling classified data and code with ambiguous sharing or manual permission checks. Establish baseline behaviour, make access intent explicit, upgrade a coherent slice and test it with production-like personas before expanding the change.
Where existing automation depends on system-mode defaults, decide whether that privilege remains valid. If it does, express it explicitly and document the exception. If it does not, correct the business process, permissions or data ownership rather than preserving accidental elevation. Monitor access failures after release by component and persona; a surge may reveal a real missing permission, an invalid design assumption or an attempted action that should remain blocked.
Put API-version security into governance
Add four checks to Apex review: target API version, class sharing declaration, access mode at each database boundary and persona-based security evidence. Require a short justification for every system-mode operation. Use static analysis to find missing declarations and risky patterns, but keep human review focused on business authority and data exposure—the parts a rule cannot infer.
Do not fear API 67+. Stronger user-mode defaults reduce the chance that custom code bypasses the platform’s permission model. The risk is treating the upgrade as syntactic and discovering the behavioural change in production. Version changes deserve the same discipline as permission redesign: inventory, explicit intent, negative testing, controlled rollout and evidence. Your Apex API version now participates directly in your security architecture.
Official references
- Salesforce Developers: Secure Apex Classes
- Salesforce Developers: Set an Access Mode for Database Operations
- Salesforce Developers: Apex Class Keywords
- Salesforce Developers: Enforce Security with stripInaccessible
- Salesforce Secure Coding Guide: Protect from Sharing Violations
- Salesforce Architects: Secure Well-Architected Solutions
- Salesforce Architects: Secure Development Lifecycle Patterns