Apex Security Essentials — with/without sharing and CRUD/FLS with Security.stripInaccessible
In Salesforce development, secure Apex means two things:
-
Enforcing record access through
with sharing,without sharing, orinherited sharing. -
Enforcing object and field-level permissions using CRUD/FLS checks.
Apex doesn’t automatically verify these for you—you have to do it.
The safest, most reliable pattern is to use with sharing by default and use Security.stripInaccessible to sanitize records for reading and writing.
Record Access: with sharing, without sharing, and inherited sharing
These keywords determine which records your code can see or modify.
They align directly with Salesforce’s sharing model (organization-wide defaults, roles, and sharing rules).
Core Concept
-
with sharing– Enforces all record-level security such as OWD, roles, and sharing rules. Users only see records they’re permitted to. -
without sharing– Runs in system mode, ignoring sharing entirely. Use sparingly for admin-level operations. -
inherited sharing– Adopts the caller’s context automatically. This is the best choice for reusable service or helper classes.
Real-World Example
A service that reads Cases should respect the caller’s sharing rules.
A narrowly scoped admin helper might need system context to fix ownership.
// Honors caller’s record access (recommended default for services)
public inherited sharing class CaseReader {
public static List<Case> myOpenCases() {
return [
SELECT Id, Subject, Status, OwnerId
FROM Case
WHERE IsClosed = false
ORDER BY LastModifiedDate DESC
LIMIT 100
];
}
}
// Escalated, narrowly scoped operation (use sparingly)
public without sharing class OwnershipFixer {
public static void assignToQueue(Set<Id> caseIds, Id queueId) {
// still must do CRUD/FLS checks! (sharing bypassed ≠ permissions bypassed)
List<Case> rows = [SELECT Id, OwnerId FROM Case WHERE Id IN :caseIds];
for (Case c : rows) c.OwnerId = queueId;
update rows;
}
}
? Tip: Keep most classes declared as with sharing or inherited sharing.
If you must use without sharing, isolate it to the smallest section of code and still perform CRUD/FLS enforcement.
CRUD/FLS Checks (Describe) — Block Insecure Operations Up Front
Apex doesn’t enforce CRUD (Create, Read, Update, Delete) or FLS (Field-Level Security).
You must check these permissions yourself.
Core Concept
Use Schema.Describe to verify permissions before performing any operation.
Block unauthorized actions early so unsafe code never runs.
Real-World Example
Before allowing a user to create an Invoice__c, confirm they can create the object
and that they’re allowed to write to each required field.
public with sharing class InvoiceGuard {
public static void assertCreatePermissions() {
if (!Schema.sObjectType.Invoice__c.isCreateable())
throw new AuraHandledException('You do not have permission to create invoices.');
// Field-level checks (sample)
if (!Schema.sObjectType.Invoice__c.fields.Amount__c.isCreateable())
throw new AuraHandledException('You cannot set Amount on invoices.');
if (!Schema.sObjectType.Invoice__c.fields.Status__c.isCreateable())
throw new AuraHandledException('You cannot set Status on invoices.');
}
}
Security.stripInaccessible — Sanitize SObjects for READ/CREATE/UPDATE
When you need to dynamically remove fields users shouldn’t read or write,Security.stripInaccessible() is the right tool.
Core Concept
Security.stripInaccessible() automatically strips out fields the running user doesn’t have permission for and tells you which ones were removed.
Use it as follows:
-
READ – before serializing or returning query results to the UI or API.
-
CREATE – before insert operations.
-
UPDATE – before updates.
Real-World Example
Here’s how to sanitize user input before DML, and strip unreadable fields before sending data back to a component or API.
public with sharing class InvoiceService {
// Create flow: sanitize writes
public static Id createInvoiceSecure(Invoice__c inputFromClient) {
InvoiceGuard.assertCreatePermissions();
// Strip fields the user can't set on create
Security.StripInaccessibleResult sr =
Security.stripInaccessible(AccessType.CREATABLE, new List<SObject>{ inputFromClient });
List<SObject> cleaned = sr.getRecords();
// Optional: log sr.getRemovedFields() for telemetry
insert cleaned;
return ((Invoice__c) cleaned[0]).Id;
}
// Read flow: sanitize reads
public static List<Invoice__c> listMyInvoices() {
List<Invoice__c> rows = [
SELECT Id, Name, Amount__c, Status__c, SensitiveNotes__c
FROM Invoice__c
WHERE OwnerId = :UserInfo.getUserId()
ORDER BY LastModifiedDate DESC
LIMIT 100
];
Security.StripInaccessibleResult sr =
Security.stripInaccessible(AccessType.READABLE, rows);
return (List<Invoice__c>) sr.getRecords(); // Sensitive fields removed if not readable
}
}
Use
AccessType.UPDATABLEfor update scenarios.
End-to-End Pattern — Sharing + CRUD/FLS + stripInaccessible
The strongest security model combines all three layers:
record-level sharing, object/field-level checks, and field stripping before DML or responses.
Core Concept
Together, these ensure that every Apex action runs in the correct context,
respects permissions, and only exposes fields that the user is allowed to see or edit.
Real-World Example
Here’s how an invoice update API could apply all three concepts in one flow.
public inherited sharing class InvoiceUpdateApi {
@AuraEnabled
public static Invoice__c updateInvoice(Invoice__c patch) {
// Object update permission
if (!Schema.sObjectType.Invoice__c.isUpdateable())
throw new AuraHandledException('You cannot update invoices.');
// FLS check: ensure user can edit fields they sent (optional: whitelist set)
Set<String> editable = new Set<String>{
'Amount__c', 'Status__c', 'DueDate__c'
};
for (String f : patch.getPopulatedFieldsAsMap().keySet()) {
if (!editable.contains(f) || !Schema.sObjectType.Invoice__c.fields.getMap().get(f).getDescribe().isUpdateable())
throw new AuraHandledException('Not allowed to modify field: ' + f);
}
// Merge with current row (avoid mass overwrite) then strip for UPDATE
Invoice__c current = [SELECT Id, Amount__c, Status__c, DueDate__c FROM Invoice__c WHERE Id = :patch.Id LIMIT 1];
if (patch.Amount__c != null) current.Amount__c = patch.Amount__c;
if (patch.Status__c != null) current.Status__c = patch.Status__c;
if (patch.DueDate__c != null) current.DueDate__c = patch.DueDate__c;
List<SObject> cleaned = Security.stripInaccessible(AccessType.UPDATABLE, new List<SObject>{ current }).getRecords();
update cleaned;
// Sanitize response for READ
return (Invoice__c) Security.stripInaccessible(AccessType.READABLE, new List<SObject>{ current }).getRecords()[0];
}
}
Testing Sharing & FLS — Building Confidence
Security logic should always be tested to prove it works as intended.
Core Concept
Use @IsTest(runAs = …) to execute Apex as a different user and confirm record access.
Also, use permission sets or profiles in your tests to verify CRUD and FLS enforcement.
Real-World Example
A user without “Edit Amount” permission shouldn’t be able to change the invoice’s amount field.
Security Layers Diagram

Security Layers Diagram — Summary
Secure Apex = Least Privilege.
That means:
-
Default to
with sharingorinherited sharingfor record visibility. -
Explicitly check CRUD and FLS using
Schema.Describebefore DML or data exposure. -
Sanitize SObjects with
Security.stripInaccessiblefor READ, CREATE, and UPDATE scenarios. -
Use
without sharingonly for narrow, audited admin operations. -
Write tests with
runAsand permission-based assertions to validate security.
By following these patterns, you’ll build Apex that’s not only functional but truly secure, compliant, and trustworthy.
