Apex Security Essentials — with/without sharing and CRUD/FLS with Security.stripInaccessible

Share

In Salesforce development, secure Apex means two things:

  1. Enforcing record access through with sharing, without sharing, or inherited sharing.

  2. 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.UPDATABLE for 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.

@IsTest
private class InvoiceSecurityTest {
    @IsTest static void user_cannot_edit_amount() {
        // Test data
        User u = TestUtils.createUserWithProfile('Standard User'); // helper creates a user
        Invoice__c inv = new Invoice__c(Name='T1', Amount__c=100, Status__c='Open');
        insert inv;

        System.runAs(u) {
            try {
                Invoice__c patch = new Invoice__c(Id=inv.Id, Amount__c=200);
                InvoiceUpdateApi.updateInvoice(patch);
                System.assert(false, 'Expected security exception');
            } catch (Exception e) {
                System.assert(e.getMessage().contains('Not allowed'));
            }
        }
    }
}

Security Layers Diagram

Diagram that shows how a user's request flows through both layers of Apex security


Security Layers Diagram — Summary

Secure Apex = Least Privilege.
That means:

  • Default to with sharing or inherited sharing for record visibility.

  • Explicitly check CRUD and FLS using Schema.Describe before DML or data exposure.

  • Sanitize SObjects with Security.stripInaccessible for READ, CREATE, and UPDATE scenarios.

  • Use without sharing only for narrow, audited admin operations.

  • Write tests with runAs and 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.

  • October 18, 2025