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_ENFORCEDWITH USER_MODESecurity.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
Accountobject.
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
The WITH SECURITY_ENFORCED clause enforces FLS and OLS in SOQL queries. If the user doesn’t have access to a field or object in the SELECT clause, they will encounter an System.QueryException and no data is returned.
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
- Automatically enforces FLS and OLS at runtime
- Eliminates need for manual field checks using
Schemaclass - Keeps Apex code clean and declarative
- Safer for UI-bound queries (LWC, VF, APIs)
🔴 Cons
- Only works with SOQL
- Only Works for SELECT Queries, not DML operations like insert, update, or delete.
- Does Not Enforce WHERE/ORDER BY Fields
- Fields used in the
WHEREorORDER BYclauses are not checked. This can cause security blind spots.
- Fields used in the
- Limited Support for Relationship Fields
- Polymorphic fields such as
WhatIdandWhoIdare not supported. - Only system fields like OwnerId, CreatedById, and LastModifiedById are allowed.
- Polymorphic fields such as
- Partial Error Reporting
- If multiple fields are inaccessible, only the first violation is reported.
- 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
Accountobject.
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
WITH USER_MODEStarting with API version 56.0, Salesforce introduced the ability to do run SOQL and do DML operations in user mode. When enabled, the platform enforces the running user’s field-level access (FLS) and object level access (OLS) during data modification.
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, anddelete. - 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 / Capability | WITH SECURITY_ENFORCED | WITH USER_MODE | Security.stripInaccessible() |
|---|---|---|---|
| Applies To | SOQL (SELECT only) | SOQL + DML (Insert, Update, Delete, Upsert) | SOQL + DML |
| Enforces | Field-Level Security (FLS) + Object-Level Security (OLS) | FLS + OLS | FLS + OLS |
| Execution Context | System Mode (except SELECT clause enforcement) | User Mode (for SOQL and DML when enabled) | System Mode (manual enforcement) |
| Behavior on Inaccessible Fields | Throws a QueryException | Fails with QueryException or DmlException with details | Silently strips inaccessible fields |
| Error Details | Only reports the first inaccessible field | Reports 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 Version | API v45.0+ | API v56.0+ | API v41.0+ (safe for older orgs) |
| Modifies Records Automatically | ❌ No | ❌ No | ✅ Yes (removes values from inaccessible fields) |
| Best for | UI queries, REST responses | Secure DML and queries in user-driven flows | Background jobs, reusable services, older APIs |
When to Use Which
| Scenario | Recommended Option |
|---|---|
| SOQL SELECT queries for LWC/Visualforce | WITH SECURITY_ENFORCED |
| SOQL + DML in Experience Cloud or REST API | WITH USER_MODE |
| Insert/update logic in batch jobs or schedulers | Security.stripInaccessible() |
| DML on fields with conditional access per user | WITH USER_MODE or stripInaccessible() |
| Queries in lower API versions (<56.0) | WITH SECURITY_ENFORCED or stripInaccessible() |
| Need for partial success in bulk DML | WITH USER_MODE or stripInaccessible() |
| Complex SOQL logic with relationships or polymorphic fields | Prefer 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_ENFORCEDfor any query that depends on user-accessible fields, especially for UI and exposed data. - ✅ Use
WITH USER_MODEfor 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!


Leave a comment