Custom Apex and Lightning code can quietly bypass the record sharing, object permissions and field-level security that Salesforce administrators spend years configuring. On most platforms, authorisation is something the developer adds; in Salesforce it already exists in the platform, and the most common secure coding failure is code that runs with more access than the user who triggered it. This guide explains how the Apex sharing keywords, user and system mode, Security.stripInaccessible and Lightning Web Security work, how to avoid SOQL injection and cross-site scripting, how to handle callout credentials, what to log, and the static analysis rules that catch common mistakes. It is for Salesforce developers, technical architects and the security teams that review custom code and AppExchange packages. For tenant-level configuration, see the Salesforce Security Guide.
Key takeaways
- Sharing and CRUD/FLS are separate controls. The
with sharingkeyword controls which records a user can see; it does not enforce object or field permissions. You need both. - Declare everything explicitly. Salesforce recommends declaring a sharing mode on every class that performs SOQL or DML, and setting an explicit access mode on database operations, especially because defaults changed in API version 67.0.
- Use user mode for user-driven data access.
WITH USER_MODE,insert as userandAccessLevel.USER_MODEapply the running user's sharing, object permissions and field-level security in one step. - Bind, do not concatenate. Static SOQL with bind variables prevents SOQL injection;
String.escapeSingleQuotesis the fallback for unavoidable dynamic queries. - Callout credentials belong in named and external credentials, not in code or custom settings, and every connected app and integration user is a non-human identity to inventory and scope.
How identity and access work in Apex
Every Apex transaction runs as a user, but how much of that user's access the code respects depends on three separate settings. Getting secure code right means reasoning about each one.
| Control | What it governs | How code sets it |
|---|---|---|
| Record sharing | Which records (rows) the user may see or edit, from org-wide defaults, role hierarchy, sharing rules and manual shares | Class keyword: with sharing, without sharing, inherited sharing; user mode always applies sharing |
| Object permissions (CRUD) | Whether the user may create, read, edit or delete a type of object | Access mode on queries and DML (WITH USER_MODE, as user, AccessLevel.USER_MODE); Security.stripInaccessible |
| Field-level security (FLS) | Which fields the user may read or edit | Same access modes; Security.stripInaccessible for graceful removal of fields |
The sharing keywords
with sharing: enforces the current user's sharing rules. Salesforce recommends setting it explicitly so code runs in the current user's context.without sharing: ignores the current user's sharing rules, so the class can reach records the user could not otherwise see. Salesforce recommends it only for classes that genuinely need system-level access, and mainly for targeted elevation.inherited sharing: takes the caller's mode at run time. When the class is itself an entry point, such as an Aura controller, an@AuraEnabledmethod called from a Lightning web component, a Visualforce controller, an Apex REST service or asynchronous Apex, it runswith sharing. It runswithout sharingonly when called from an existingwithout sharingcontext.- No keyword: the Apex Developer Guide states that classes with no sharing keyword run
with sharingfrom API version 67.0, while in 66.0 and earlier the mode depends on inheritance and entry point and otherwise defaults towithout sharing. A codebase usually mixes API versions, so do not rely on the default.
User mode and system mode
Apex lets each database operation declare its access mode. For SOQL and SOSL, add WITH USER_MODE or WITH SYSTEM_MODE. For DML, write insert as user acc; or as system. For Database and Search methods, pass AccessLevel.USER_MODE or AccessLevel.SYSTEM_MODE; the *WithBinds query methods require this parameter. User mode applies the running user's sharing rules, field-level security and object permissions; in system mode, the class sharing keyword decides whether record sharing applies. Salesforce documents that from API version 67.0 Apex runs in user mode by default, while earlier versions default to system mode, which is a strong reason to state the mode on every operation.
- Error handling in user mode: a user mode SOQL query reports all field-level errors at once through
QueryException.getInaccessibleFields(); DML exposes field errors throughDMLException.getDmlFieldNames(), andDatabaseDML throughSaveResult.getErrors(). Missing object permission raises a security exception. - Graceful degradation:
Security.stripInaccessibletakes records and anAccessType(READABLE,CREATABLE,UPDATABLE,UPSERTABLE) and returns copies with fields the user cannot access removed, including relationship fields.getRemovedFields()shows what was removed. Use it to filter query results before returning them to a component, and to clean records deserialised from untrusted JSON before DML. It enforces field and object permissions, not record sharing, and it throws onAggregateResult. - Elevation for one operation: Salesforce documents
AccessLevel.withPermissionSetId()as a Developer Preview way to add a permission set's access to a single operation without carrying it over. Treat preview features as subject to change.
Admin roles and privileged code paths
System mode and without sharing are the Apex equivalents of privileged access. Treat every use as a privileged code path that needs a reason and a reviewer.
- Isolate elevation: put the few operations that must run in system mode in small, separately named classes (for example
...ElevatedService) declaredwithout sharing, called fromwith sharingentry points that perform their own authorisation check first. - Check the caller's right to the action, not just the data: before an elevated operation, check a custom permission or permission set assignment that represents the business action (for example "approve refunds"), not a profile name.
- Triggers run in system context: Salesforce advises setting an explicit access mode on database operations in triggers and handler classes. Do not let trigger handlers become a path to update fields the user could not edit through the UI.
- Guard
@AuraEnabledand REST methods: any@AuraEnabled,@RestResourceorwebservicemethod is callable directly by an authenticated user with the right access to the class, not only through your component. Validate every parameter and enforce sharing and FLS inside the method. - Restrict Apex class access: grant access to controller classes through permission sets for the users who need the feature, rather than to all profiles.
- Guest and community users: code reachable by unauthenticated site guest users needs the strictest review, because a single
without sharingcontroller can expose records to the internet.
Injection and client-side security
SOQL and SOSL injection
The Apex Developer Guide describes SOQL injection as user input that modifies a dynamic SOQL statement, typically when a value is concatenated into a string passed to Database.query. Its guidance is to avoid dynamic SOQL and use static queries with bind variables (WHERE Name = :queryName), and, where dynamic SOQL is unavoidable, to sanitise user input with String.escapeSingleQuotes, which escapes single quotation marks. Field and object names cannot be bound, so check them against an allowlist built from the schema (for example from Schema.getGlobalDescribe()) rather than escaping them.
Cross-site scripting and redirects
- Visualforce: rely on default output escaping and avoid disabling it; values from URL parameters need encoding for the context they are written into.
- Apex error messages:
addErrorwith escaping disabled displays the message directly in the UI. PMD'sApexXSSFromEscapeFalserule flags this. - Open redirects: do not build
PageReferenceredirects from user-controlled values; PMD'sApexOpenRedirectrule flags redirects to user-controlled locations. - Lightning components: Salesforce describes Lightning Web Security (LWS) as a security architecture that runs components in an isolated JavaScript environment dedicated to their namespace. It replaced Lightning Locker as the default, is enabled by default for orgs created in Winter '23 and later, and became generally available for Lightning web and Aura components in all orgs as of Summer '23. LWS reduces the blast radius between namespaces; it does not make it safe to inject untrusted HTML into your own component, so avoid writing user data into the DOM as HTML.
- Third-party libraries: load JavaScript as static resources reviewed like any dependency, and keep Content Security Policy trusted sites minimal.
Integrations, API tokens and service accounts
Most serious Salesforce data losses in recent years came through integrations rather than custom code bugs. In the ShinyHunters Salesforce data theft campaign, callers persuaded employees to authorise an attacker-controlled connected app, often a modified Data Loader, and then exported data through the API. In the Salesloft Drift breach, stolen OAuth tokens from one integration opened many Salesforce tenants. Code and configuration that handle integrations deserve the same rigour as authentication code.
| Pattern | Risk | Secure approach |
|---|---|---|
| Credentials in Apex, custom settings or custom metadata | Readable by anyone with code or setup access; copied into sandboxes | Named credential for the endpoint plus external credential for authentication; PMD ApexSuggestUsingNamedCred flags hard-coded credentials in requests |
| Callouts over plain HTTP | Interception and tampering | HTTPS only; PMD ApexInsecureEndpoint flags plain http endpoints |
Hard-coded keys and IVs in Crypto calls | Encrypted data is effectively unprotected | Generated keys and IVs; PMD ApexBadCrypto flags hard-wired values |
| Integration user with System Administrator profile | Any token theft becomes full tenant compromise | Dedicated integration user with a minimal permission set, API-only where possible, IP restrictions where the platform allows |
| Any user can authorise new connected apps | Consent phishing installs attacker apps | Restrict who can approve and manage connected apps; allowlist approved apps; review scopes and owners |
Salesforce describes a named credential as specifying a callout endpoint URL and its authentication parameters in one definition, with the authentication held in a linked external credential. External credentials support protocols including OAuth (browser, client credentials and JWT bearer flows), JWT, AWS Signature v4, basic and custom authentication, and access is granted through permission sets or profiles. Legacy named credentials are no longer updated or enhanced, so new code should use the Winter '23 model. Inventory every connected app, integration user, named credential and external credential with an owner and scopes. See the Securing Salesforce Agentforce Guide for agent actions, which also execute Apex and flows on a user's behalf.
Logging and detection
- Event Monitoring log objects: Salesforce's object reference lists event log objects including
ApexCalloutEventLog,ApexExecutionEventLog,ApexRestApiEventLog,ApexSoapApiEventLog,ApexTriggerEventLog,ApexUnexpectedExcpEventLog,AuraRequestEventLog,BulkApiEventLogandBulkApi2EventLog. Availability depends on licence. See the Salesforce Event Monitoring Detection Guide. - What to alert on: unusually large or new bulk exports, new connected app authorisations, API activity from unexpected networks, spikes in unexpected Apex exceptions (often a sign of probing
@AuraEnabledmethods), and callouts to new endpoints. - Application logging: log security decisions in elevated code paths (who requested, which permission was checked, which records were affected) without logging field values the user could not see.
- Do not log secrets: never write tokens, session IDs or credential values to debug logs or custom log objects; debug logs are readable by administrators and support users.
Hardening baseline for custom code
- Explicit sharing on every class that runs SOQL, SOSL or DML, with
with sharingas the default choice andinherited sharingfor reusable services. - Explicit access mode on every database operation: user mode for user-initiated work; system mode only in reviewed elevated classes.
- FLS on output: filter records with
Security.stripInaccessibleor user mode queries before returning them from@AuraEnabledor REST methods. - Static SOQL with binds; allowlist any dynamic object or field names.
- No credentials in code or metadata: named and external credentials only.
- Static analysis in CI: run Salesforce Code Analyzer with the PMD Apex security rules (
ApexCRUDViolation,ApexSharingViolations,ApexSOQLInjection,ApexXSSFromURLParam,ApexXSSFromEscapeFalse,ApexOpenRedirect,ApexInsecureEndpoint,ApexBadCrypto,ApexSuggestUsingNamedCred,ApexDangerousMethods) and triage findings. Note that the PMDApexCRUDViolationdescription still assumes Apex runs in system mode by default, so check its findings against your API versions. See the SAST Triage and False Positive Management Guide. - Security tests: write Apex tests that run as a restricted user with
System.runAsand assert that inaccessible records and fields are not returned and that writes fail. - Peer review gates: any new
without sharing,SYSTEM_MODE, guest-accessible controller or connected app needs security review before deployment.
Rolling this out on an existing org
- Run the PMD security rules across the codebase to get a baseline count by rule.
- Fix guest-accessible and
@AuraEnabledentry points first, then REST services, then batch and scheduled jobs. - Add explicit sharing keywords and access modes class by class, with restricted-user tests, deploying to a full sandbox first.
- Gate new code in CI on the security rules, leaving legacy findings in a tracked backlog.
- If a change breaks a business process because users genuinely lacked access, fix the permission model rather than reverting to system mode; revert only the specific class while the permission change is agreed.
Framework mapping
| Framework | Reference | Relevance to Apex secure coding |
|---|---|---|
| OWASP ASVS 5.0 | 8.2.1, 8.2.2, 8.3.1 (authorisation); 1.2.4 (injection); 13.3.1 (secrets management) | Function and data-level access, server-side enforcement, parameterised queries, vaulted secrets |
| NIST SP 800-53 Rev. 5 | AC-3 Access Enforcement; AC-6 Least Privilege | Sharing, CRUD and FLS enforcement; minimal elevated code paths and integration users |
| NIST SP 800-53 Rev. 5 | SA-11(1) Static Code Analysis; SI-10 Information Input Validation | PMD security rules in CI; SOQL binding and output encoding |
| NIST SP 800-53 Rev. 5 | IA-5 Authenticator Management; AU-2 Event Logging | Named and external credentials; Event Monitoring coverage |
| NIST CSF 2.0 | PR.AA-05; PR.PS-06 | Least privilege access permissions; secure development integrated |
Common misconfigurations
- Assuming
with sharingcovers field security: it does not. Awith sharingcontroller can still return salary or national ID fields the user cannot see on the page layout. - Classes with no sharing keyword: behaviour depends on API version and caller, which reviewers cannot reliably predict. PMD's
ApexSharingViolationsflags classes that use DML without an explicit sharing mode. - Trusting the component to restrict calls: an
@AuraEnabledmethod that only your admin page calls is still callable by any user with access to the class. - Concatenated dynamic SOQL from list view filters, search boxes or sort parameters.
- Integration users with broad profiles and long-lived tokens, and ordinary users able to authorise connected apps, as the integration breaches above show.
- Secrets in custom settings or debug logs, copied into every sandbox refresh and visible to anyone with setup access.
Practitioner checklist
- Declare
with sharing,without sharingorinherited sharingon every class that queries or writes data. - Set an explicit user or system access mode on every SOQL, SOSL and DML operation.
- Filter output with
Security.stripInaccessibleor user mode before returning records to components or APIs. - Validate parameters and enforce authorisation inside every
@AuraEnabled, REST and webservice method. - Use static SOQL with bind variables and allowlist dynamic field and object names.
- Move all callout credentials to named and external credentials and scope integration users with minimal permission sets.
- Restrict connected app approval and inventory every connected app with owner and scopes.
- Run the PMD Apex security rules in CI and triage the results.
- Write restricted-user tests with
System.runAsfor every data-returning entry point. - Monitor Apex, API and bulk export event logs and alert on unusual exports and new app authorisations.
Standards and references
- Salesforce: Apex Developer Guide, Using the with sharing, without sharing, and inherited sharing Keywords
- Salesforce: Apex Developer Guide, Enforce User Mode for Database Operations
- Salesforce: Apex Developer Guide, Enforce Security with the stripInaccessible Method
- Salesforce: Apex Developer Guide, SOQL Injection
- Salesforce: Lightning Web Security
- Salesforce Help: Named Credentials
- PMD: Apex security rules
- OWASP: Application Security Verification Standard 5.0
Related NHI Mgmt Group resources: Salesforce Security Guide · Securing Salesforce Agentforce Guide · Salesforce Event Monitoring Detection Guide · Secure Code Review Guide