With sharing vs without sharing in Apex, and inherited sharing
With sharing vs without sharing is the first access question to ask of any Apex class. The keyword decides whether the class sees only the records the running user is allowed to see, or every record in the org. Get it wrong on a class a community user or a site guest can call and the result is not a bug report, it is a data exposure.
The rules are short, but there are three keywords, a default that changed with API version 67.0, and a few edge cases (inner classes, triggers, async jobs) where the answer is not obvious from reading the class header.
This post covers what with sharing, without sharing and inherited sharing each do, what happens when the keyword is left out, when without sharing is the right choice, how it combines with guest users, and why none of this replaces object and field permission checks.
What does with sharing do?
Record sharing is the part of Salesforce security that decides which individual records a user can see: organization-wide defaults, the role hierarchy, sharing rules, manual shares, teams and territories. The Apex Developer Guide defines three class keywords that control whether Apex respects it:
with sharing: SOQL, SOSL and DML in the class respect the running user's record access. A query returns only records the user could open.without sharing: the class ignores record sharing. A query returns every matching record in the org.inherited sharing: the class takes the sharing mode of whatever called it.
The keyword applies to the class where the code is defined, not the class that calls it. If a with sharing controller calls a method in a without sharing helper, the helper's query ignores sharing.
public with sharing class CaseController {
@AuraEnabled
public static List<Case> openCases(Id accountId) {
return CaseQueries.forAccount(accountId); // runs without sharing
}
}
public without sharing class CaseQueries {
public static List<Case> forAccount(Id accountId) {
return [SELECT Id, Subject, Description FROM Case
WHERE AccountId = :accountId];
}
}
The entry class looks correct. The query is not. Any user who can call openCases reads every case on any account whose id they can supply.
What does inherited sharing mean?
inherited sharing is the safe choice for service and selector classes that are called from many places. Salesforce documents two behaviours:
- Called from another class, it runs in that caller's mode.
- Used as the entry point of a transaction (an Aura or Lightning controller, a Visualforce controller, an Apex REST service, or an async job), it runs as
with sharing.
That second rule is the reason to prefer it over leaving the keyword out. A class marked inherited sharing cannot become an unintended system-mode entry point because someone exposed it in a new way.
What happens when the sharing keyword is omitted?
This changed in API version 67.0 (Summer '26). Salesforce's Summer '26 architect highlights state that classes with no explicit sharing declaration now default to with sharing, and that database operations run in user mode by default.
For classes saved at API 66.0 or earlier, which is most code in most orgs, an omitted keyword means:
- Called from another class, the class runs in the caller's mode.
- Called directly by the platform as the entry point, it can run without sharing. Salesforce documents that a class with no keyword runs
with sharingwhen it is used as a Lightning controller, but Visualforce controllers, Apex REST and SOAP web services, and async classes (Queueable, Batchable, Schedulable,@future) with no caller to inherit from still run in system context. - An inner class does not take its outer class's keyword. A subclass takes its parent's.
The API version is per class, so one org can hold both behaviours. Bumping a class to 67.0 can also change what it returns without any error, because queries that used to see everything start respecting sharing and permissions. Salesforce recommends declaring the keyword explicitly on every class that queries or writes records, which removes the guesswork in both directions.
Triggers are the exception: they cannot carry a sharing declaration and run in system context. Put the logic in a handler class and give the handler a keyword.
When is without sharing legitimate?
There are real reasons to bypass sharing:
- Counting or aggregating across records the user cannot see, where only the total is returned.
- Reading configuration records that every user needs but cannot be granted access to.
- A system process, such as a nightly batch or a platform event handler, that must touch every record.
- A narrow lookup on behalf of an external user, for example confirming that a case number and email pair exist before showing a status.
What makes these safe is not the keyword but the shape of the code around it:
- Keep the
without sharingcode in a small, separate class with one purpose. Do not mark a whole controllerwithout sharingto make one query work. - Do not let the caller choose which record the bypass reads. A caller-supplied record id plus
without sharingis an insecure direct object reference. - Return the minimum: a count, a boolean, or the two fields the page shows.
- Leave a comment that says why, so the next reviewer does not have to rediscover it.
public with sharing class CaseStatusController {
@AuraEnabled
public static String status(String caseNumber, String email) {
return CaseStatusLookup.statusFor(caseNumber, email);
}
}
// Without sharing on purpose: external users cannot be shared every case.
// The caller proves knowledge of two values and gets one field back.
public without sharing class CaseStatusLookup {
public static String statusFor(String caseNumber, String email) {
List<Case> rows = [SELECT Status FROM Case
WHERE CaseNumber = :caseNumber
AND SuppliedEmail = :email LIMIT 1];
return rows.isEmpty() ? null : rows[0].Status;
}
}
Even this pattern deserves rate limiting at the site and a hard look at how guessable the two values are. A case number is sequential.
How does without sharing combine with guest users?
Guest users on Experience Cloud sites are the place where the keyword matters most. With secure guest record access, a guest sees no record unless a guest sharing rule grants it, which is a strong default. A without sharing class granted to the guest profile walks straight past it. The sharing rules no longer decide what the anonymous visitor reads; the Apex does.
That is the mechanism behind several recent guest data exposures, and we traced it step by step in the anatomy of a guest user data leak. The hardening list for the guest side is in our Experience Cloud guest user security checklist.
Sharing is not CRUD or FLS
with sharing controls which records a user sees. It does not check whether the user may read the object at all, or which fields they may see. A with sharing class can still return a salary field to a user whose profile hides it.
Object and field permissions are enforced separately: WITH USER_MODE on queries, as user on DML, AccessLevel.USER_MODE on Database methods, or Security.stripInaccessible. User mode also applies sharing, which makes it the simplest way to enforce all three together. The full picture, including what a security reviewer looks for, is on our CRUD and FLS page.
A checklist for Apex sharing
- Every class that queries or writes records declares a keyword.
- Service and selector classes use
inherited sharing. - Entry points (Aura, LWC, Visualforce, REST, web services, invocable actions) are
with sharingorinherited sharing. without sharinglives in small, documented helpers that never take a caller-chosen record id.- Async classes declare a keyword, since they have no caller to inherit from.
- Classes below API 67.0 are reviewed before their version is bumped.
- Object and field access is enforced as well, with user mode or
stripInaccessible.
What Vulkro for Salesforce checks
vulkro-sf scan works out how each Apex class and method actually runs (user mode, system mode with sharing, or system mode without sharing) along the call stack, following the Apex Developer Guide rules per API version, including inherited and omitted keywords, triggers and async entry points. The VS Code extension shows that mode as a lens on each class and entry method.
Checks built on it include:
apex-xmethod-sharing-bypass-via-without-sharing-helper: awith sharingAura, REST or invocable entry point that delegates to awithout sharinghelper which queries or writes records, like theCaseControllerexample above.apex-legacy-api-version-insecure-default: a class below API 67.0 with no keyword that is itself a non-Aura entry point and touches records.apex-no-sharing-declaration-class-infoandapex-without-sharing-class-info: an inventory of classes with no keyword or withwithout sharing, the latter noting whether the keyword is a deliberate opt-out or a leftover.apex-async-implicit-without-sharing: an async class with no keyword that touches personal data.apex-trigger-sharing-declaration: a trigger at API 67.0 with a declaration that will fail to deploy.
A legitimate without sharing helper will still be reported, because the engine cannot know your intent. Record the decision as an accepted risk in .vulkro/triage.toml so it is reviewed in pull requests like code. When a guest or community profile can reach the class, the finding becomes part of an attack path from that user to the object, with every step cited to a file and line. The Apex detectors page lists the sharing checks.
Where to see it
Run vulkro-sf scan ./force-app, or open the VS Code extension to see each class's run mode as you edit. For teams responsible for many orgs, see Vulkro for Salesforce platform teams. For a broader look at how access mistakes turn into breaches, read how Salesforce orgs get breached. To get set up, start here.
Frequently asked questions
What is the difference between with sharing and without sharing?
with sharing makes a class respect the running user's record access, so queries return only records the user could open. without sharing ignores record access and returns every matching record. Neither keyword checks object or field permissions.
What is the default sharing mode in Apex if no keyword is declared?
From API version 67.0, a class with no keyword runs with sharing. At 66.0 and earlier it takes the caller's mode, and when it is the entry point it can run in system context, apart from @AuraEnabled controllers. Declaring the keyword explicitly avoids depending on either rule.
When should I use inherited sharing?
Use it on service, selector and utility classes that many callers share. It follows the caller's mode, and when the class is itself the entry point it runs with sharing, so it never becomes an accidental system-mode door.
Does with sharing enforce field-level security?
No. Sharing controls which records are visible, not which objects or fields. Use WITH USER_MODE, as user DML or Security.stripInaccessible to enforce object and field permissions.
Sources
- Salesforce, Apex Developer Guide: Use the with sharing, without sharing, and inherited sharing keywords
- Salesforce, Lightning Aura Components Developer Guide: Securing Data in Apex Controllers
- Salesforce, Summer '26 release: architect highlights
- Salesforce, Apex Reference Guide: Apex Versioned Behavior Changes
