How to Enforce Apex Security in Salesforce A Developer’s Guide

🔐 How to Enforce Apex Security in Salesforce: A Developer’s Guide

,

Introduction

Ensuring data security in Apex is critical for protecting sensitive business information. Salesforce offers tools like profiles and permission sets. However, Apex code must explicitly enforce Field-Level Security (FLS) and Object-Level Security (OLS). This is essential when accessing or modifying records programmatically.

This blog introduces and compares the key tools Salesforce provides to enforce security in Apex:

  • WITH SECURITY_ENFORCED
  • WITH USER_MODE
  • Security.stripInaccessible()
  • Manual FLS checks with Schema methods

We’ll cover what each tool does, their limitations, best use cases, and how to combine them effectively in real-world development.key differences, use cases, advantages, and pitfalls of each—along with hands-on examples.

1. WITH SECURITY_ENFORCED: Secure Your SOQL Queries

Apex traditionally ignores user permissions during SOQL execution unless explicitly enforced. This means that fields the user doesn’t have access to can still be queried—exposing sensitive data. If you query fields the user doesn’t have access to, the query still be able to query those fields. So before, WITH SECURITY_ENFORCED became available, developers had to check access manually using the Schema class before running queries.

🚫 Problem: Apex Ignores FLS by Default

Consider the below scenario:

  • The following Apex code is executed in user context (e.g., from LWC or Visualforce):
  • A user does not have read access to the AnnualRevenue field on the Account object.
public with sharing class SampleClass {
    public static void someMethod() {
        List<Account> accounts;
        accounts = [SELECT Id, Name, AnnualRevenue, Active__c FROM Account WHERE CreatedDate = TODAY];
        System.debug('accounts+++' + accounts);
    }
}

Output:

USER_DEBUG|[5]|DEBUG|accounts+++(Account:{Id=001..., Name=Test Corp, AnnualRevenue=500000})

Despite the user lacking access to AnnualRevenue, the query returns data. This is because Apex does not enforce field-level permissions by default — even in with sharing classes.

Old Approach: Manual FLS Checks

To protect sensitive data, developers previously had to manually validate object and field access using Schema Class methods:

public with sharing class SampleClass {
    public static void someMethod() {
        List<Account> accounts;
        if (Schema.SObjectType.Account.isAccessible() &&
            Schema.SObjectType.Account.fields.AnnualRevenue.isAccessible() &&
            Schema.SObjectType.Account.fields.Active__c.isAccessible()) 
        {
            accounts = [SELECT Id, Name, AnnualRevenue, Active__c FROM Account];
        }
        System.debug('accounts+++' + accounts);
    }
}

Output:

USER_DEBUG|[10]|DEBUG|accounts+++null

There is no data returned, due to the FLS checks.

⚠️Limitations of the Manual Approach
  • Clunky and repetitive: You must check every field manually before using it in a SOQL query.
  • High chance of developer oversight: It’s easy to forget a field, especially in queries with many fields.
  • Hard to maintain: Any time the object model changes (e.g., new fields), you may need to update the FLS logic.

✅ Solution: WITH SECURITY_ENFORCED

Updated Example Using WITH SECURITY_ENFORCED
public with sharing class SampleClass {
    public static void someMethod() {
        try {
            List<Account> accounts = [SELECT Id, Name, AnnualRevenue FROM Account WITH SECURITY_ENFORCED];
            for (Account acc : accounts) {
                System.debug('Account: ' + acc);
            }
        } catch (QueryException qe) {
            System.debug('Security Exception: ' + qe);
        }
    }
}

Output:

USER_DEBUG|[9]|DEBUG|Security Exception: System.QueryException: Insufficient permissions: secure query included inaccessible field

If the user lacks access to AnnualRevenue, the SOQL query fails securely with a clear error – “Security Exception: Insufficient permissions: secure query included inaccessible field”

🟢 Pros
  1. Automatically enforces FLS and OLS at runtime
  2. Eliminates need for manual field checks using Schema class
  3. Keeps Apex code clean and declarative
  4. Safer for UI-bound queries (LWC, VF, APIs)
🔴 Cons
  1. Only works with SOQL
    • Only Works for SELECT Queries, not DML operations like insert, update, or delete.
  2. Does Not Enforce WHERE/ORDER BY Fields
    • Fields used in the WHERE or ORDER BY clauses are not checked. This can cause security blind spots.
  3. Limited Support for Relationship Fields
    • Polymorphic fields such as WhatId and WhoId are not supported.
    • Only system fields like OwnerId, CreatedById, and LastModifiedById are allowed.
  4. Partial Error Reporting
    • If multiple fields are inaccessible, only the first violation is reported.
  5. Throws a runtime exception if access is denied, which must be handled using try/catch.
✅ When to Use
  • When running SOQL queries and you want strict enforcement of field/object access.
  • Ideal for read-only access scenarios like displaying data in LWC, Visualforce, or REST APIs.

Reference – WITH SECURITY_ENFORCED SFDC Documentation

2. WITH USER_MODE: Enforcing Security in DML

As discussed earlier, Apex traditionally runs in system mode—which means it ignores the current user’s permissions unless explicitly coded otherwise. If you insert a record with fields the user wasn’t allowed to modify, the operation would still succeed. This poses a serious security risk.

Before Salesforce introduced WITH USER_MODE in API v56.0., enforcing Field-Level Security (FLS) in Apex DML operations needed manual checks. Developers had to use the Schema class to verify whether the user had access to perform DML.

🚫 Problem: Apex DML Ignores FLS by Default

Consider the below scenario:

  • The following Apex code is executed in user context (e.g., from LWC or Visualforce):
  • A user does not have write access to the AnnualRevenue and Active field on the Account object.
public with sharing class SampleClass {
    public static void someMethod() {
        Account acc = new Account(Name = 'ABC Corp', AnnualRevenue = 500000, Active__c = 'Yes');
        insert acc;
        System.debug('acc+++' + acc);
    }
}

Output:

USER_DEBUG|[5]|DEBUG|acc+++Account:{Name=ABC Corp, AnnualRevenue=500000, Active__c=Yes, Id=001NS00001ZuiWLYAZ}

Despite the user lacking access to the AnnualRevenue and Active field, the code inserts the account without any errors and sets the value.

Old Approach: Manual FLS Checks

To prevent restricted access, developers had to use the Schema class. They needed to verify whether the user had access to perform DML using schema class methods.

if (Schema.SObjectType.Account.isCreateable() &&
    Schema.SObjectType.Account.fields.AnnualRevenue.isCreateable()) {
    
    insert new Account(Name = 'ABC Corp', AnnualRevenue = 500000);
}

Output:

USER_DEBUG|[11]|DEBUG|acc+++Account:{}

There is no account record inserted, due to the FLS checks.

⚠️Limitations of the Manual Approach
  • Requires verbose, repetitive code, especially for complex objects with many fields.
  • Easy to forget checks, leading to potential security gaps.
  • Doesn’t scale well in large, dynamic forms or data flows.

✅ Solution: WITH USER_MODE

Example 1: Secure SOQL Query
public with sharing class SampleClass {
    public static void someMethod() {
        try {
            List<Account> accounts = [SELECT Id, Name, AnnualRevenue FROM Account WITH USER_MODE];
            for (Account acc : accounts) {
                System.debug('Account: ' + acc);
            }
        } catch (QueryException  qe) {
            System.debug('Security Exception: ' + qe);
        }
    }
}

Output:

If the user doesn’t have access to the AnnualRevenue field, the query will throw below exception.

USER_DEBUG|[9]|DEBUG|Security Exception: System.QueryException: No such column 'AnnualRevenue' on entity 'Account'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.

Note – WITH SECURITY_ENFORCED you only get the first field error in an exception. However, WITH USER_MODE, you can find all FLS errors in your SOQL query using the getInaccessibleFields() method. Refer below example –

try {
    List<Account> accounts = [SELECT Id, Name, AnnualRevenue, Active__c FROM Account WITH USER_MODE ];
    for (Account acc : accounts) {
        System.debug('Account: ' + acc);
    }
} catch (QueryException  qe) {
    System.debug('Security Exception: ' + qe);
    Map<String, Set<String>> inaccessibleFields = qe.getInaccessibleFields();
    System.debug('inaccessibleFields: ' + inaccessibleFields);
}

Output:

The getInaccessibleFields() method returns a map of object names. It identifies the set of fields that were inaccessible to the current user. getInaccessibleFields give you the field names Active__c and AnnualRevenue , which user doesn’t have access.

USER_DEBUG|[11]|DEBUG|inaccessibleFields: {Account={Active__c, AnnualRevenue}}
Example 2: Secure DML Operation
public with sharing class SampleClass {
    public static void someMethod() {
        try {
            Account acc = new Account(Name = 'ABC Corp', AnnualRevenue = 500000, Active__c = 'Yes');
            Database.insert(acc, AccessLevel.USER_MODE);
        } catch (DmlException dmlEx) {
            System.debug('Security DML Exception: ' + dmlEx);
            System.debug('inaccessibleFields: ' + dmlEx.getDmlFieldNames(0));
        }
    }
}

Output:

If the user does not have create access to the AnnualRevenue and Active field, the insert will fail securely. It will trigger a DML exception. There is no need for manual field access checks.

USER_DEBUG|[7]|DEBUG|Security DML Exception: System.DmlException: Operation failed due to fields being inaccessible on Sobject Account, check errors on Exception or Result!

USER_DEBUG|[8]|DEBUG|inaccessibleFields: (AnnualRevenue, Active__c)
🟢 Pros
  • Works with all DML operations: insert, update, upsert, and delete.
  • Validates fields used in WHERE clauses of queries if paired with Database.query() in user mode.
  • Reports all inaccessible fields using getInaccessibleFields()
  • Helps maintain data security in Experience Cloud sites and custom Apex APIs.
🔴 Cons
  • Requires API v56.0 or above.
  • Not available for anonymous execution in Developer Console.
  • Requires extra error handling and logic for partial DML failures.
✅ When to Use
  • When inserting or updating records on behalf of users.
  • In Experience Cloud sites , Apex REST APIs or LWC where you want to respect the user’s access rights.
  • In Managed Packages to offer a more secure DML experience for customers.

Reference – WITH USER_MODE SFDC Documentation

3. Security.stripInaccessible(): Clean DML for All API Versions

Before Salesforce introduced WITH USER_MODE in API v56.0, developers relied on Security.stripInaccessible() to enforce FLS when performing DML or reading data in Apex. This method ensures that your code respects the user’s field-level and object-level permissions without requiring manual checks.

Unlike WITH USER_MODE, this method doesn’t throw exceptions when encountering inaccessible fields. Instead, it automatically removes the values (nulls out) the user cannot access. This feature is especially useful for DML operations in API versions below 56.0, batch jobs, and utility classes.

🚫 Problem: Apex DML Operates in System Mode by Default

Apex runs in system mode. This means it can perform operations on fields the user is not allowed to see. It can also update those fields. If not handled properly, this could result in inserting or updating restricted data silently, creating potential security and compliance risks.

✅ Solution: Use Security.stripInaccessible() to Enforce FLS

This method cleans up records before DML or processing. It removes values the current user does not have access to. The removal is based on the specified access type (READABLE, CREATABLE, UPDATABLE).

Example 1: Securing SOQL Results with StripInaccessible

If a user does not have read access to AnnualRevenue, the query still runs without error, but the restricted field is stripped from the result set:

public with sharing class SampleClass {
    public static void someMethod() {
        List<Account> accounts = [SELECT Id, Name, AnnualRevenue, Active__c FROM Account];       
        SObjectAccessDecision decision = Security.stripInaccessible(
            AccessType.READABLE,
            accounts
        );
		System.debug('Fields removed by stripInaccessible: '+decision.getRemovedFields());        
        for (SObject acc : decision.getRecords()) {
            System.debug('Account: ' + acc);
        }
    }
}

Output:

USER_DEBUG|[9]|DEBUG|Fields removed by stripInaccessible: {Account={Active__c, AnnualRevenue}}
USER_DEBUG|[7]|DEBUG|Account: Account:{Id=001..., Name=ABC Corp, Active__c=Yes}

Here, AnnualRevenue is excluded from the output because the user lacks read access.

Example 2: Securing DML Insert Operation

If a user doesn’t have create access to fields like AnnualRevenue, this approach strips them before DML, preventing security exceptions:

Account acc = new Account(Name = 'ABC Corp', AnnualRevenue = 500000, Active__c = 'Yes');

SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.CREATABLE,
    new List<Account>{ acc }
);

Database.insert(decision.getRecords());

Output:

USER_DEBUG|[7]|DEBUG|Account inserted without inaccessible fields (e.g., AnnualRevenue)

This way, the DML proceeds safely and only includes fields the user is allowed to set.

🟢 Pros
  • Works across all API versions
  • Supports SOQL and DML with FLS enforcement
  • Prevents DML failures by cleaning up restricted fields
  • Useful in batch jobs, triggers, and helper classes
  • No exception handling required for FLS violations
🔴 Cons
  • Doesn’t throw errors for restricted fields—silent removal lead to confusionRequires extra logic if you need to log or report which fields were stripped
  • Requires extra logic if you need to log or report which fields were stripped
  • Slightly more verbose compared to WITH USER_MODE
  • Doesn’t support AggregateResult SObject. If the source records are of AggregateResult SObject type, an exception is thrown.
✅ When to Use
  • When working with API versions < 56.0
  • In bulk processing, triggers, schedulers, or background jobs
  • When you want to safely skip inaccessible fields without breaking code
  • When building reusable services that must run safely in different user contexts

Reference – Security.stripInaccessible() SFDC Documentation

Side-by-Side Comparison: Apex Security Enforcement Options

Feature / CapabilityWITH SECURITY_ENFORCEDWITH USER_MODESecurity.stripInaccessible()
Applies ToSOQL (SELECT only)SOQL + DML (Insert, Update, Delete, Upsert)SOQL + DML
EnforcesField-Level Security (FLS) + Object-Level Security (OLS)FLS + OLSFLS + OLS
Execution ContextSystem Mode (except SELECT clause enforcement)User Mode (for SOQL and DML when enabled)System Mode (manual enforcement)
Behavior on Inaccessible FieldsThrows a QueryExceptionFails with QueryException or DmlException with detailsSilently strips inaccessible fields
Error DetailsOnly reports the first inaccessible fieldReports all inaccessible fields via getInaccessibleFields()No exception, but requires additional logic to log stripped fields
WHERE/ORDER BY Enforcement❌ Not enforced✅ Enforced (when using Database.query with user mode)❌ Not enforced
Supports DML Operations❌ No✅ Yes✅ Yes
Supports AggregateResult✅ Yes✅ Yes❌ No (throws exception on AggregateResult)
Partial Success in Bulk DML❌ Not applicable✅ Yes✅ Yes
Available in Anonymous Apex (Dev Console)✅ Yes❌ No✅ Yes
Minimum API VersionAPI v45.0+API v56.0+API v41.0+ (safe for older orgs)
Modifies Records Automatically❌ No❌ No✅ Yes (removes values from inaccessible fields)
Best forUI queries, REST responsesSecure DML and queries in user-driven flowsBackground jobs, reusable services, older APIs

When to Use Which

ScenarioRecommended Option
SOQL SELECT queries for LWC/VisualforceWITH SECURITY_ENFORCED
SOQL + DML in Experience Cloud or REST APIWITH USER_MODE
Insert/update logic in batch jobs or schedulersSecurity.stripInaccessible()
DML on fields with conditional access per userWITH USER_MODE or stripInaccessible()
Queries in lower API versions (<56.0)WITH SECURITY_ENFORCED or stripInaccessible()
Need for partial success in bulk DMLWITH USER_MODE or stripInaccessible()
Complex SOQL logic with relationships or polymorphic fieldsPrefer stripInaccessible() (due to limited support in WITH SECURITY_ENFORCED)

Practical Use Cases

📌 Use Case 1: Securing Queries in LWC
@AuraEnabled(cacheable=true)
public static List<Contact> getContacts() {
    return [SELECT Id, FirstName, LastName, Email FROM Contact WHERE Email != null WITH SECURITY_ENFORCED];
}

Use this when exposing data to Lightning Components where users may have limited field access.

📌 Use Case 2: Enforcing DML Security in REST API
@RestResource(urlMapping='/api/account')
global with sharing class AccountAPI {
    @HttpPost
    global static String createAccount(String name, Decimal revenue) {
        Account acc = new Account(Name = name, AnnualRevenue = revenue);

        try {
            Database.DMLOptions dmlOpts = new Database.DMLOptions();
            dmlOpts.UseUserMode = true;
            Database.insert(acc, dmlOpts);
            return 'Success';
        } catch (Exception e) {
            return 'Error: ' + e.getMessage();
        }
    }
}

Use this to respect field-level access when users call APIs to create records.

Best Practices

  • Always use WITH SECURITY_ENFORCED for any query that depends on user-accessible fields, especially for UI and exposed data.
  • Use WITH USER_MODE for DML where data is being entered or changed by the user to enforce proper permissions.
  • Use Security.stripInaccessible() for DML in environments where API version < 56.0 or bulk-safe logic is needed.
  • Don’t blindly use these in system processes or batch jobs—they can break if running user lacks access.

Final Thoughts

Both WITH SECURITY_ENFORCED and WITH USER_MODE are essential tools in the modern Salesforce developer’s toolkit. They encourage secure-by-default coding practices and reduce the risk of data leakage or unauthorized access.

If you found this guide helpful, consider bookmarking it or sharing with your Salesforce dev team. Happy coding! ⚡

Start using them today and elevate the trust and security of your Salesforce applications!

One response to “🔐 How to Enforce Apex Security in Salesforce: A Developer’s Guide”

  1. This is an excellent breakdown—practical, concise, and immediately usable. The way you’ve laid out the recommended options against different scenarios makes it easy to decide what’s right depending on the context. Loved how you backed it up with real code examples, especially for LWC and REST API use cases.

    The reminder about not blindly using these in system contexts is spot on—that nuance often gets missed. Definitely sharing this with my team. Thanks for putting this together!

    Like

Leave a reply to shantanu kumar Cancel reply