Security & Sharing

Apex Security in Summer '26 — WITH USER_MODE vs WITH SECURITY_ENFORCED vs stripInaccessible

By Rishabh Panwar · Published 15 September 2026 · 8 min read · Advanced

Ask a group of Salesforce developers what with sharing protects and many will say “the data.” It only decides which rows a user sees. Field access and object access are separate checks, and Apex security in Summer ‘26 changed how all of them behave. From API v67.0, database operations run in user mode by default, classes with no sharing keyword default to with sharing, and WITH SECURITY_ENFORCED no longer compiles.

What follows is about how the keywords, clauses and methods fit together. If you are in the middle of a version bump and something just broke, the API v67 user mode scenario is the faster read. Org-wide defaults, roles and sharing rules are covered in the security model deep dive.

Access control has four layers

Each layer is independent. Getting one right tells you nothing about the others. Object and field permissions increasingly live in permission sets; the permission sets migration guide covers that move.

LayerWhat it controlsWhere it’s set
Object (CRUD)Whether the user can use this object at allProfiles and permission sets
Field (FLS)Which fields on the object the user can see or editProfiles and permission sets
Record (sharing)Which rows the user can seeOrg-wide defaults, role hierarchy, sharing rules
ExposureWhether the code path can be called, and by whom@AuraEnabled, Apex REST, invocable methods, guest user profile

with sharing covers the record layer and nothing else. It doesn’t check object permissions or field-level security. “I added with sharing, so it’s secure” is the answer that fails security reviews.

What changed in API v67.0

BehaviourAPI 66 and earlierAPI 67 and later
Default mode for SOQL, SOSL and DMLSystem mode: object permissions and FLS are bypassedUser mode: object permissions, FLS and sharing are enforced
Class with no sharing keywordDepends on context (see the next section)Runs with sharing
WITH SECURITY_ENFORCEDWorksCompile error

Three points that trip people up:

  • The change applies per class. Upgrading the org changes nothing. Each class keeps its old behaviour until its own apiVersion in the -meta.xml file is raised to 67.0.
  • Triggers always run in system mode, on every API version.
  • Some classes are most likely to break on a version bump: @AuraEnabled controllers, @InvocableMethod classes, Apex REST services and batch classes. These often relied on system mode without anyone noticing. Expect insufficient-access errors or fewer rows.

What an omitted sharing keyword does on older versions

A common belief is that “no keyword means without sharing.” For entry points on older versions, that isn’t reliably true. The Apex Developer Guide lists the rules for a class with no declaration on API 66 or earlier, applied in this order:

  1. If any class in its inheritance chain is saved at API 67 or later, it runs with sharing.
  2. If it is an Aura controller, or an @AuraEnabled method called from a Lightning web component, it runs with sharing.
  3. If it isn’t an Apex entry point, it uses the sharing mode of the class that called it.
  4. Otherwise, it runs without sharing.

So an Aura or LWC controller with no keyword still enforces sharing, while a Visualforce controller or an Apex REST service with no keyword falls through to rule 4 and runs without sharing.

This is easy to confuse with inherited sharing. A class declared inherited sharing runs with sharing whenever it is an entry point, including Visualforce controllers, Apex REST services and async Apex. Anonymous Apex always runs with sharing.

Working out the effective mode of an undeclared v66 class means tracing both its inheritance chain and its callers. That is why Salesforce recommends always declaring a sharing keyword on any class that queries or modifies data.

Choosing the mode per operation

// Static SOQL
List<Account> a1 = [SELECT Id FROM Account WITH USER_MODE];
List<Account> a2 = [SELECT Id FROM Account WITH SYSTEM_MODE];

// Dynamic SOQL
List<Account> a3 = Database.query(queryString, AccessLevel.USER_MODE);
List<Account> a4 = Database.queryWithBinds(queryString, binds, AccessLevel.USER_MODE);

// DML, keyword form
insert as user newAccounts;
update as system accountsToFix;

// DML, Database method form
Database.insert(newAccounts, AccessLevel.USER_MODE);

How the class keyword and the access mode combine

Class keywordOperation modeSharing enforcedObject permissions and FLS enforced
with sharingSystem modeYesNo
with sharingUser modeYesYes
without sharingSystem modeNoNo
without sharingUser modeYesYes

The last row surprises people and comes up in interviews. An explicit without sharing on the class doesn’t override WITH USER_MODE on the query. User mode enforces sharing regardless.

WITH SECURITY_ENFORCED vs WITH USER_MODE

WITH SECURITY_ENFORCED (API 66 and earlier)WITH USER_MODE
Which parts of the query are checkedSELECT and FROM onlyThe whole query, including WHERE and ORDER BY
Record sharingNot appliedApplied
Polymorphic fields (such as Owner or Task.WhatId)Not handledCovered
DML supportQueries onlyYes, through as user or AccessLevel
On a violationThrowsThrows

The WHERE clause gap

With WITH SECURITY_ENFORCED, this query succeeds for a user who can’t read SSN__c, because the field appears only in the filter:

List<Account> accts = [
    SELECT Id FROM Account
    WHERE SSN__c != null
    WITH SECURITY_ENFORCED
];

The field never shows up in the results, so nothing looks wrong. But a caller who can control the filter or sort order can page through results and work out the field’s values. WITH USER_MODE closes that gap by checking every clause.

The error messages differ on purpose

ClauseError on a restricted fieldWhat it reveals
WITH SECURITY_ENFORCED”Insufficient permissions: secure query included inaccessible field”Confirms the field exists
WITH USER_MODE”No such column ‘SSN__c’ on entity ‘Account‘“Doesn’t confirm the field exists

User mode hides the schema from users who can’t see it.

Degrading gracefully with stripInaccessible

Both clauses throw when they hit a restricted field. When a feature should keep working with less data (typically a UI), use Security.stripInaccessible:

SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.READABLE,
    [SELECT Id, Name, SSN__c FROM Account WHERE Industry = :industry]
);
List<Account> visible = decision.getRecords();      // SSN__c removed, rows kept
Set<String> removed = decision.getRemovedFields().get('Account');

stripInaccessible handles object and field access only. It doesn’t enforce record sharing, so pair it with a with sharing class or a user-mode query.

Throw or degrade?

ContextChoiceReason
Integration or background jobThrow (WITH USER_MODE)A loud failure is safer than silently incomplete data
UI componentDegrade (stripInaccessible)The page keeps working with the fields the user can see

Being able to explain why you picked one is what interviewers look for.

A worked example

Setup: a restricted user has no FLS on Test_Field__c. Account org-wide default is Private and the user owns 9 of 20 Accounts. The code is an Aura controller with no sharing keyword.

Class API versionQuery clauseResult
66 or earlier (tested on v64)None9 rows and no error. Sharing is enforced because it is an Aura entry point, but FLS isn’t, so the restricted field comes back
66 or earlierWITH SECURITY_ENFORCEDThrows “insufficient permissions”
67 or laterNoneThrows “No such column”, because user mode is now the default
67 or laterWITH USER_MODESame error, confirming user mode was already in effect

The first row is the lesson. The record layer worked and the field layer didn’t. And if the component doesn’t render that field, the page looks perfectly fine while the value sits in the network response for anyone who opens browser developer tools. A page that looks right is no evidence that access is right.

The exposure layer

Every @AuraEnabled method is a public endpoint. Any authenticated user can call it directly, whatever the UI shows, so hiding a component does nothing for access control. Apex REST services and invocable methods work the same way.

Least privilege applies to your SELECT clause too. If a component only shows names, don’t query SSN__c, because the value lands in the response whether you render it or not.

Lightning Data Service enforces object permissions, FLS and sharing automatically. Apex on API 66 or earlier doesn’t, and that difference answers most LWC record-access questions.

Injection is a separate risk that user mode doesn’t address. See the SOQL injection guide.

What reviewers and interviewers listen for

  • Name the layer. “This is a field-level security gap; sharing is already working” is stronger than “I’d add with sharing.”
  • State the API version. The correct answer often differs between v66 and v67, and saying so shows you are current.
  • Don’t propose WITH SECURITY_ENFORCED as a fix. It no longer compiles on v67 and dates your knowledge.

Frequently asked questions

What is the difference between WITH USER_MODE and WITH SECURITY_ENFORCED?

WITH SECURITY_ENFORCED checked object and field access only for fields in the SELECT and FROM clauses, didn't apply sharing, and didn't cover polymorphic fields. WITH USER_MODE checks the whole query, including WHERE, enforces sharing, and has DML equivalents. From API v67.0, WITH SECURITY_ENFORCED is a compile error.

Does with sharing enforce field-level security?

No. with sharing controls record access only. Object permissions and field-level security need USER_MODE, stripInaccessible, or explicit describe checks.

What happens when a without sharing class runs a query WITH USER_MODE?

Sharing is enforced. USER_MODE applies object permissions, field-level security and record sharing, and the query-level mode takes precedence over the class-level without sharing keyword.

What does a class with no sharing keyword do on API 66 or earlier?

It depends. It runs with sharing if it is an Aura controller or an @AuraEnabled method called from LWC, or if any class in its inheritance chain is saved at v67 or later. If it isn't an entry point, it takes the sharing mode of its caller. Otherwise it runs without sharing.

When should I use Security.stripInaccessible instead of WITH USER_MODE?

Use stripInaccessible when the feature should keep working with fewer fields, such as a UI component. It removes fields the user can't access and returns the rest. WITH USER_MODE throws on a violation, which suits integrations where failing loudly is safer. stripInaccessible doesn't enforce record sharing.

Do Apex triggers respect user mode in Summer '26?

No. Triggers always run in system mode on every API version. Put logic that needs user-level access in a handler class with an explicit sharing keyword and access mode.