Salesforce

Security in Apex: User Mode vs System Mode

How Apex Runs by Default

Salesforce Apex runs in system mode by default. This means the executing code can see and modify every record in the org, regardless of the current user's sharing rules, field-level security (FLS), or object-level CRUD permissions. The platform behaves as if an all-powerful system administrator is making every query and DML call.

This design exists for legitimate reasons. Integration code, data migration utilities, and admin-triggered batch jobs often need to operate on records the running user cannot normally see. If a nightly batch job that consolidates billing data had to respect every sales rep's sharing model, it would silently miss half the records it needs.

The danger appears the moment you call that same code from a Lightning component, a public-facing Experience Cloud page, or any context where an unprivileged user triggers it. Without explicit security enforcement, a community user or low-permission internal user can read and write data far beyond their access level simply by invoking your Apex.

With Sharing, Without Sharing, and Inherited Sharing

Apex controls record-level visibility through three class-level keywords. These keywords determine which records are visible to SOQL queries inside the class — they are about row access, not field access.

with sharing

Declaring a class with sharing tells Apex to enforce the current user's sharing rules when executing SOQL. If a user cannot see an Account record because no sharing rule grants them access, a query inside this class will not return that record.

public with sharing class AccountService {
    public List<Account> getMyAccounts() {
        // Only returns accounts the running user has access to via
        // ownership, sharing rules, manual shares, or role hierarchy.
        return [SELECT Id, Name, Industry FROM Account];
    }
}

Use with sharing for every class that handles user-facing data operations. This is the safe default for any code triggered by a user interaction.

without sharing

Declaring a class without sharing explicitly bypasses the current user's sharing model. Every record the class queries or modifies is accessible regardless of who triggered the code.

public without sharing class BillingAggregator {
    public static Map<Id, Decimal> computeOrgWideTotals() {
        Map<Id, Decimal> totals = new Map<Id, Decimal>();
        for (Opportunity opp : [SELECT AccountId, Amount FROM Opportunity]) {
            Decimal current = totals.containsKey(opp.AccountId)
                ? totals.get(opp.AccountId) : 0;
            totals.put(opp.AccountId, current + (opp.Amount ?? 0));
        }
        return totals;
    }
}

Use without sharing only for back-end utility code: scheduled jobs, batch classes, integration adapters, and admin tools that must see all data regardless of user context. Never expose a without sharing method directly to a user-facing controller.

inherited sharing

Declaring a class inherited sharing tells it to adopt the sharing mode of whatever called it. If a with sharing controller calls an inherited sharing service, the service enforces sharing. If a without sharing batch class calls the same service, the service does not enforce sharing.

public inherited sharing class RecordFetcher {
    public static List<Case> getOpenCases() {
        return [SELECT Id, Subject, Status FROM Case WHERE Status != 'Closed'];
    }
}

This is the right keyword for shared utility or service classes that do not own the security decision — the decision belongs to the caller. A controller that knows it is serving a user applies with sharing; it calls RecordFetcher, which inherits that restriction automatically.

The hidden danger: no keyword declared

A class with no sharing keyword does not inherit anything — it runs in system mode for record visibility. This surprises many developers because it looks like inherited sharing but behaves differently.

// DANGEROUS: no sharing keyword means system mode for record visibility
public class LeadService {
    public static List<Lead> getAllLeads() {
        // Returns ALL leads in the org, ignoring the user's sharing model.
        // If this is called from a Lightning component, any user can see
        // every lead regardless of their territory or role.
        return [SELECT Id, Name, Email, Company FROM Lead];
    }
}

The fix is always to declare an explicit keyword. For new user-facing classes: with sharing. For classes whose behavior should adapt to their context: inherited sharing.

WITH USER_MODE and WITH SYSTEM_MODE in SOQL

Introduced in API version 50.0, the WITH USER_MODE and WITH SYSTEM_MODE clauses apply security enforcement at the individual query level, independent of the class's sharing declaration. This is a more granular and explicit mechanism than class-level keywords.

WITH USER_MODE enforces three things simultaneously for that query:

  • The current user's sharing rules (row-level visibility)
  • Field-level security — fields the user cannot read are excluded from results
  • Object-level CRUD — if the user cannot query the object, the query throws an exception
public with sharing class ContactController {
    public static List<Contact> getUserContacts() {
        // Enforces sharing, FLS, and CRUD at query time.
        // Fields the running user cannot see are stripped automatically.
        return [SELECT Id, FirstName, LastName, Email, Phone
                FROM Contact
                WITH USER_MODE];
    }
}

WITH SYSTEM_MODE does the opposite — it explicitly bypasses all three checks for that query, even inside a with sharing class. Use it when one specific query in an otherwise secure class needs to read data the user cannot directly see, such as a configuration record or a cross-object lookup.

public with sharing class DashboardController {
    public static List<Account> getUserAccounts() {
        // Normal user-mode query: respects sharing + FLS
        return [SELECT Id, Name FROM Account WITH USER_MODE];
    }

    public static List<OrgConfig__c> getConfiguration() {
        // Configuration records are admin-managed; bypass sharing for this query only
        return [SELECT Id, FeatureFlag__c, MaxBatchSize__c
                FROM OrgConfig__c
                WITH SYSTEM_MODE];
    }
}

Query-level USER_MODE vs class-level with sharing

These two mechanisms are complementary, not interchangeable. with sharing on a class enforces sharing rules for all SOQL in that class but does not enforce FLS or CRUD. WITH USER_MODE on a query enforces all three — sharing, FLS, and CRUD — for that specific query only.

For complete security, you need both: the class declared with sharing and queries using WITH USER_MODE, or you rely on WITH USER_MODE alone (which is sufficient at the query level).

Database.query() with AccessLevel

Dynamic SOQL through Database.query() supports the same access levels via the AccessLevel enum, available from API v57.0 with Database.queryWithBinds() or passed as a third argument to Database.query().

public with sharing class DynamicSearchService {
    public static List<SObject> searchRecords(String objectApiName, String searchTerm) {
        String soql = 'SELECT Id, Name FROM ' + String.escapeSingleQuotes(objectApiName)
                    + ' WHERE Name LIKE :searchTerm';
        Map<String, Object> binds = new Map<String, Object>{ 'searchTerm' => '%' + searchTerm + '%' };

        // USER_MODE enforces sharing, FLS, and CRUD for this dynamic query
        return Database.queryWithBinds(soql, binds, AccessLevel.USER_MODE);
    }

    public static List<SObject> adminSearchRecords(String objectApiName, String searchTerm) {
        String soql = 'SELECT Id, Name FROM ' + String.escapeSingleQuotes(objectApiName)
                    + ' WHERE Name LIKE :searchTerm';
        Map<String, Object> binds = new Map<String, Object>{ 'searchTerm' => '%' + searchTerm + '%' };

        // SYSTEM_MODE for admin-facing tools that must see all records
        return Database.queryWithBinds(soql, binds, AccessLevel.SYSTEM_MODE);
    }
}

Manual FLS Enforcement

When you cannot use WITH USER_MODE — for instance, in code targeting API versions below 50.0, or when working with DML operations — you must check FLS manually using the Schema.DescribeFieldResult methods.

Checking field accessibility

public with sharing class AccountFieldChecker {
    public static void verifyReadAccess() {
        // Check individual fields before reading them
        if (!Schema.SObjectType.Account.fields.Phone.isAccessible()) {
            throw new System.NoAccessException();
        }
        if (!Schema.SObjectType.Account.fields.AnnualRevenue.isAccessible()) {
            throw new System.NoAccessException();
        }

        // Safe to query now
        List<Account> accounts = [SELECT Id, Phone, AnnualRevenue FROM Account];
    }

    public static void verifyWriteAccess() {
        // Check before update DML
        if (!Schema.SObjectType.Account.fields.Phone.isUpdateable()) {
            throw new System.NoAccessException();
        }
        // Check before insert DML
        if (!Schema.SObjectType.Account.fields.Phone.isCreateable()) {
            throw new System.NoAccessException();
        }
    }
}

stripInaccessible() — the practical alternative

Manual field-by-field checking is verbose and easy to miss. Security.stripInaccessible() automates this: it takes a list of records and removes any field values the current user is not allowed to access, based on FLS. The method returns an SObjectAccessDecision object containing the cleaned records.

public with sharing class SafeAccountReader {
    public static List<Account> getAccountsStripped() {
        // Query all fields of interest (some may not be accessible to all users)
        List<Account> rawAccounts = [
            SELECT Id, Name, Phone, AnnualRevenue, Rating, Fax
            FROM Account
            LIMIT 200
        ];

        // Strip any fields the current user cannot read.
        // AccessType.READABLE checks read FLS.
        SObjectAccessDecision decision = Security.stripInaccessible(
            AccessType.READABLE,
            rawAccounts
        );

        // getRemovedFields() returns a map of object name → set of stripped field names,
        // useful for logging what was removed.
        Map<String, Set<String>> removed = decision.getRemovedFields();
        if (!removed.isEmpty()) {
            System.debug('Stripped inaccessible fields: ' + removed);
        }

        return decision.getRecords();
    }
}

The four AccessType values correspond to DML operations:

  • AccessType.READABLE — strips fields the user cannot read
  • AccessType.CREATABLE — strips fields the user cannot set on insert
  • AccessType.UPDATABLE — strips fields the user cannot modify
  • AccessType.UPSERTABLE — strips fields not creatable or updatable

CRUD Enforcement

FLS governs individual fields. CRUD governs the entire object. A user might have read access to every field on Opportunity but no permission to create new Opportunity records. You must check object-level permissions separately.

public with sharing class CrudGuard {
    public static void assertCreateable(SObjectType objType) {
        if (!objType.getDescribe().isCreateable()) {
            throw new System.NoAccessException();
        }
    }

    public static void assertUpdateable(SObjectType objType) {
        if (!objType.getDescribe().isUpdateable()) {
            throw new System.NoAccessException();
        }
    }

    public static void assertDeletable(SObjectType objType) {
        if (!objType.getDescribe().isDeletable()) {
            throw new System.NoAccessException();
        }
    }

    public static void assertQueryable(SObjectType objType) {
        if (!objType.getDescribe().isQueryable()) {
            throw new System.NoAccessException();
        }
    }
}

Call these guards immediately before DML in any user-facing operation:

public with sharing class OpportunityWriter {
    public static void createOpportunity(String name, Id accountId, Date closeDate, String stage) {
        // CRUD check before insert
        if (!Schema.SObjectType.Opportunity.isCreateable()) {
            throw new AuraHandledException('You do not have permission to create Opportunities.');
        }
        // FLS checks for the fields being set
        if (!Schema.SObjectType.Opportunity.fields.StageName.isCreateable()) {
            throw new AuraHandledException('You do not have permission to set the Stage field.');
        }

        Opportunity opp = new Opportunity(
            Name = name,
            AccountId = accountId,
            CloseDate = closeDate,
            StageName = stage
        );
        insert opp;
    }

    public static void deleteOpportunity(Id oppId) {
        // CRUD check before delete
        if (!Schema.SObjectType.Opportunity.isDeletable()) {
            throw new AuraHandledException('You do not have permission to delete Opportunities.');
        }
        delete new Opportunity(Id = oppId);
    }
}

Real-World Pattern: Secure User-Facing Utility Class

The following class combines every security layer discussed above into a single, production-ready pattern. It handles a realistic scenario: a Lightning component calls a method to fetch and update Account records on behalf of the current user.

public with sharing class AccountManager {

    /**
     * Returns accounts visible to the current user, with inaccessible fields stripped.
     * Throws if the user cannot query Accounts at all.
     */
    public static List<Account> getAccessibleAccounts(String industryFilter) {
        // 1. CRUD: can this user query Accounts?
        if (!Schema.SObjectType.Account.isQueryable()) {
            throw new AuraHandledException('You do not have permission to view Accounts.');
        }

        // 2. Query with USER_MODE: enforces sharing + FLS + CRUD at query level.
        //    The CRUD check above is redundant when using USER_MODE but is kept
        //    here as a fast-fail with a user-friendly message before the query fires.
        List<Account> accounts;
        if (String.isNotBlank(industryFilter)) {
            accounts = Database.queryWithBinds(
                'SELECT Id, Name, Phone, AnnualRevenue, Rating, Industry FROM Account WHERE Industry = :ind',
                new Map<String, Object>{ 'ind' => industryFilter },
                AccessLevel.USER_MODE
            );
        } else {
            accounts = [SELECT Id, Name, Phone, AnnualRevenue, Rating, Industry
                        FROM Account
                        WITH USER_MODE];
        }

        // 3. stripInaccessible as a defence-in-depth layer for any fields
        //    not already excluded by USER_MODE (edge cases with legacy permissions).
        SObjectAccessDecision decision = Security.stripInaccessible(
            AccessType.READABLE,
            accounts
        );
        return decision.getRecords();
    }

    /**
     * Updates the Phone and Rating fields on an Account.
     * Enforces CRUD and FLS before performing any DML.
     */
    public static void updateAccountContact(Id accountId, String phone, String rating) {
        // 1. CRUD: can this user update Accounts?
        if (!Schema.SObjectType.Account.isUpdateable()) {
            throw new AuraHandledException('You do not have permission to update Accounts.');
        }

        // 2. FLS: can this user write to these specific fields?
        List<String> blockedFields = new List<String>();
        if (!Schema.SObjectType.Account.fields.Phone.isUpdateable()) {
            blockedFields.add('Phone');
        }
        if (!Schema.SObjectType.Account.fields.Rating.isUpdateable()) {
            blockedFields.add('Rating');
        }
        if (!blockedFields.isEmpty()) {
            throw new AuraHandledException(
                'You do not have permission to edit: ' + String.join(blockedFields, ', ')
            );
        }

        // 3. Verify the record is accessible to this user before updating it.
        //    USER_MODE on the read ensures we don't blindly update a record the
        //    user couldn't see — which would be a privilege-escalation vector.
        List<Account> existing = [
            SELECT Id FROM Account WHERE Id = :accountId WITH USER_MODE LIMIT 1
        ];
        if (existing.isEmpty()) {
            throw new AuraHandledException('Account not found or you do not have access.');
        }

        // 4. Build a minimal update record — never query-then-update full object
        //    to avoid accidentally overwriting fields with stale values.
        Account toUpdate = new Account(
            Id = accountId,
            Phone = phone,
            Rating = rating
        );

        // 5. stripInaccessible on UPDATABLE before DML as a final safety net.
        SObjectAccessDecision decision = Security.stripInaccessible(
            AccessType.UPDATABLE,
            new List<Account>{ toUpdate }
        );

        update decision.getRecords();
    }

    /**
     * Inserts a new Account. Enforces CRUD and FLS on every field being set.
     */
    public static Account createAccount(String name, String industry, String phone) {
        // 1. CRUD
        if (!Schema.SObjectType.Account.isCreateable()) {
            throw new AuraHandledException('You do not have permission to create Accounts.');
        }

        // 2. FLS on fields being written
        Map<String, Schema.SObjectField> fieldMap = Schema.SObjectType.Account.fields.getMap();
        List<String> fieldsToWrite = new List<String>{ 'Name', 'Industry', 'Phone' };
        for (String fieldName : fieldsToWrite) {
            Schema.DescribeFieldResult dfr = fieldMap.get(fieldName.toLowerCase()).getDescribe();
            if (!dfr.isCreateable()) {
                throw new AuraHandledException('You do not have permission to set: ' + fieldName);
            }
        }

        Account newAccount = new Account(
            Name = name,
            Industry = industry,
            Phone = phone
        );

        // 3. stripInaccessible on CREATABLE before insert
        SObjectAccessDecision decision = Security.stripInaccessible(
            AccessType.CREATABLE,
            new List<Account>{ newAccount }
        );

        List<SObject> cleanRecords = decision.getRecords();
        insert cleanRecords;
        return (Account) cleanRecords[0];
    }
}

Test class skeleton

Apex security tests require a test user with restricted permissions to verify that enforcement is actually happening — testing as a System Administrator proves nothing because admins bypass most FLS checks.

@IsTest
private class AccountManagerTest {

    @TestSetup
    static void makeData() {
        // Create a test user with a profile that has limited Account field access.
        Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];
        User restrictedUser = new User(
            Alias = 'tstUsr',
            Email = '[email protected]',
            EmailEncodingKey = 'UTF-8',
            LastName = 'TestUser',
            LanguageLocaleKey = 'en_US',
            LocaleSidKey = 'en_US',
            ProfileId = p.Id,
            TimeZoneSidKey = 'America/Los_Angeles',
            UserName = '[email protected]'
        );
        insert restrictedUser;

        Account acc = new Account(Name = 'Test Corp', Industry = 'Technology');
        insert acc;
    }

    @IsTest
    static void testGetAccessibleAccounts_asRestrictedUser() {
        User restrictedUser = [SELECT Id FROM User WHERE UserName = '[email protected]'];
        System.runAs(restrictedUser) {
            Test.startTest();
            List<Account> results = AccountManager.getAccessibleAccounts('Technology');
            Test.stopTest();
            // Verify results contain only visible records
            Assert.isFalse(results.isEmpty(), 'Restricted user should see at least their own accounts');
        }
    }

    @IsTest
    static void testCreateAccount_withoutPermission() {
        // To fully test CRUD enforcement, use a profile that cannot create Accounts.
        // This skeleton shows the pattern; tailor it to a profile in your org.
        User restrictedUser = [SELECT Id FROM User WHERE UserName = '[email protected]'];
        System.runAs(restrictedUser) {
            Test.startTest();
            try {
                AccountManager.createAccount('New Corp', 'Finance', '5551234567');
                // If the user genuinely lacks create on Account, an exception is expected.
                // If the profile has create access, adjust the test to verify successful creation.
            } catch (AuraHandledException e) {
                Assert.isTrue(e.getMessage().contains('permission'), 'Expected a permission error');
            }
            Test.stopTest();
        }
    }
}

Decision Guide: Which Security Mechanism to Use

  • New user-facing class, all queries, all DML: declare with sharing on the class and add WITH USER_MODE to every SOQL statement. This is the most complete and concise approach.
  • Shared service or utility called by both user-facing and system code: declare inherited sharing so the caller decides.
  • Back-end batch, scheduled job, or integration: declare without sharing explicitly so the intent is documented.
  • Classes with no keyword: treat them as a bug. Audit and add a keyword.
  • DML without USER_MODE available: use Schema.SObjectType CRUD checks before DML and Security.stripInaccessible() before insert/update.
  • Need to verify a field before read or write without full strip: use .isAccessible(), .isCreateable(), .isUpdateable() on the DescribeFieldResult.

Apex security is not one mechanism but a layered system. Class-level sharing keywords govern row visibility. WITH USER_MODE adds FLS and CRUD enforcement at the query level. Manual describe checks and stripInaccessible() cover DML. Use all three layers together in user-facing code: a class declared with sharing, queries using WITH USER_MODE or Database.queryWithBinds(..., AccessLevel.USER_MODE), and Security.stripInaccessible() as a final gate before DML. The most dangerous pattern in Salesforce development is a class with no sharing keyword called from a user-facing controller — it silently exposes every record in the org. Always declare sharing intent explicitly.

← All articles

Frequently Asked Questions

Do with sharing or without sharing keywords also enforce field-level security and CRUD permissions?

No — the sharing keywords only control row-level visibility, meaning which records a SOQL query returns. Field-level security (FLS) and object-level CRUD permissions are separate concerns that must be enforced through other means, such as using WITH SECURITY_ENFORCED in SOQL or calling Schema.sObjectType describe methods to check permissions before DML.

What does inherited sharing actually do differently from the other two keywords?

A class declared with inherited sharing adopts the sharing context of whatever called it — if the caller is running with sharing, so does the inherited class; if the caller is running without sharing, the inherited class does too. This makes it useful for utility or service classes that should not impose their own sharing model but instead defer to the security posture of the entry point. Without this keyword, an unspecified class defaults to without sharing behavior, which is the unsafe implicit choice.

If Apex defaults to system mode, why not just always declare with sharing to be safe?

Some legitimate use cases — scheduled batch jobs, integration adapters, and cross-user data aggregations — genuinely need access to records the running user cannot see, so forcing with sharing on those classes would cause silent data gaps or broken functionality. The correct approach is to use with sharing as the default for all user-facing classes while deliberately and explicitly using without sharing only for back-end infrastructure code that operates outside a specific user's context.

Can a with sharing class call a without sharing class and inadvertently expose restricted records to the user?

Yes, and this is a common security gap. When a with sharing class invokes a method on a without sharing class, the called class executes with full system access regardless of the caller's sharing context. Any data returned from that without sharing class can then surface to the user through the calling class. To prevent this, without sharing classes should never return raw record collections to user-facing layers — they should return only computed or aggregated values that don't expose underlying record data the user shouldn't see.