Security in Apex – with sharing, FLS & CRUD Checks

Writing secure Apex code is critical. This guide covers three essential security mechanisms: sharing, Field-Level Security (FLS), and CRUD checks.

1. Understanding "with sharing" vs "without sharing"

The with sharing keyword enforces Salesforce's sharing model. The without sharing keyword bypasses it.

❌ Unsafe - without sharing


public without sharing class AccountService {
    // This class can see ALL accounts regardless of user's sharing settings
    
    public static List<Account> getAllAccounts() {
        return [SELECT Id, Name FROM Account];  // User sees all accounts!
    }
}
  

✅ Secure - with sharing


public with sharing class AccountService {
    // This class respects the user's sharing settings
    
    public static List<Account> getMyAccounts() {
        // User only sees accounts they own or have been shared with
        return [SELECT Id, Name FROM Account];
    }
}
  

When to Use "without sharing"

Only use without sharing when you have a specific business reason and understand the security implications:


public without sharing class AdminUtility {
    // Admin utility that needs to process all records regardless of sharing
    
    public static void updateAllRecordsStatus() {
        // Carefully validate that this should bypass sharing
        List<Account> allAccounts = [SELECT Id FROM Account];
        // ... business logic
    }
}
  

2. Field-Level Security (FLS) Checks

Always check if a user has access to a field before reading or writing it.

❌ Unsafe - No FLS Check


public class AccountProcessor {
    public static void updateAccounts(List<Account> accounts) {
        for (Account acc : accounts) {
            acc.Sensitive_Data__c = 'secret value';  // No permission check!
            update acc;
        }
    }
}
  

✅ Secure - Check FLS


public with sharing class AccountProcessor {
    
    public static Boolean userCanUpdateField(String objectName, String fieldName) {
        // Check if field is accessible
        SObjectType objType = Schema.getGlobalDescribe().get(objectName);
        DescribeSObjectResult objDescribe = objType.getDescribe();
        DescribeFieldResult fieldDescribe = objDescribe.fields.getMap()
                                                       .get(fieldName)
                                                       .getDescribe();
        
        // Check create/update permission
        return fieldDescribe.isUpdateable();
    }
    
    public static void updateAccounts(List<Account> accounts) {
        // Check FLS before accessing field
        if (!userCanUpdateField('Account', 'Sensitive_Data__c')) {
            throw new SecurityException('You do not have permission to update this field');
        }
        
        for (Account acc : accounts) {
            acc.Sensitive_Data__c = 'secret value';
            update acc;
        }
    }
}

public class SecurityException extends Exception { }
  

3. CRUD Checks (Create, Read, Update, Delete)

Always check if a user can perform CRUD operations before doing DML.

❌ Unsafe - No CRUD Check


public class AccountService {
    public static void deleteAllAccounts(List<Id> accountIds) {
        List<Account> accounts = [SELECT Id FROM Account WHERE Id IN :accountIds];
        delete accounts;  // May fail if user doesn't have delete permission!
    }
}
  

✅ Secure - Check CRUD Permissions


public with sharing class AccountService {
    
    public static Boolean userCanDelete(String objectName) {
        SObjectType objType = Schema.getGlobalDescribe().get(objectName);
        DescribeSObjectResult objDescribe = objType.getDescribe();
        
        return objDescribe.isDeletable();
    }
    
    public static Boolean userCanCreate(String objectName) {
        SObjectType objType = Schema.getGlobalDescribe().get(objectName);
        DescribeSObjectResult objDescribe = objType.getDescribe();
        
        return objDescribe.isCreateable();
    }
    
    public static Boolean userCanUpdate(String objectName) {
        SObjectType objType = Schema.getGlobalDescribe().get(objectName);
        DescribeSObjectResult objDescribe = objType.getDescribe();
        
        return objDescribe.isUpdateable();
    }
    
    public static void deleteAccounts(List<Id> accountIds) {
        if (!userCanDelete('Account')) {
            throw new SecurityException('You do not have permission to delete accounts');
        }
        
        List<Account> accounts = [SELECT Id FROM Account WHERE Id IN :accountIds];
        delete accounts;
    }
    
    public static void createAccount(Account acc) {
        if (!userCanCreate('Account')) {
            throw new SecurityException('You do not have permission to create accounts');
        }
        
        insert acc;
    }
}

public class SecurityException extends Exception { }
  

4. Comprehensive Security Utility


public with sharing class SecurityUtil {
    
    // Check if user can read object
    public static Boolean canReadObject(String objectName) {
        try {
            SObjectType objType = Schema.getGlobalDescribe().get(objectName);
            return objType.getDescribe().isAccessible();
        } catch (Exception e) {
            return false;
        }
    }
    
    // Check if user can create object
    public static Boolean canCreateObject(String objectName) {
        try {
            SObjectType objType = Schema.getGlobalDescribe().get(objectName);
            return objType.getDescribe().isCreateable();
        } catch (Exception e) {
            return false;
        }
    }
    
    // Check if user can update object
    public static Boolean canUpdateObject(String objectName) {
        try {
            SObjectType objType = Schema.getGlobalDescribe().get(objectName);
            return objType.getDescribe().isUpdateable();
        } catch (Exception e) {
            return false;
        }
    }
    
    // Check if user can delete object
    public static Boolean canDeleteObject(String objectName) {
        try {
            SObjectType objType = Schema.getGlobalDescribe().get(objectName);
            return objType.getDescribe().isDeletable();
        } catch (Exception e) {
            return false;
        }
    }
    
    // Check if user can read field
    public static Boolean canReadField(String objectName, String fieldName) {
        try {
            SObjectType objType = Schema.getGlobalDescribe().get(objectName);
            DescribeSObjectResult objDescribe = objType.getDescribe();
            
            DescribeFieldResult fieldDescribe = objDescribe.fields.getMap()
                                                               .get(fieldName)
                                                               .getDescribe();
            return fieldDescribe.isAccessible();
        } catch (Exception e) {
            return false;
        }
    }
    
    // Check if user can write field
    public static Boolean canWriteField(String objectName, String fieldName) {
        try {
            SObjectType objType = Schema.getGlobalDescribe().get(objectName);
            DescribeSObjectResult objDescribe = objType.getDescribe();
            
            DescribeFieldResult fieldDescribe = objDescribe.fields.getMap()
                                                               .get(fieldName)
                                                               .getDescribe();
            return fieldDescribe.isUpdateable();
        } catch (Exception e) {
            return false;
        }
    }
}

// Usage in your code
public with sharing class SafeAccountService {
    
    public static void processAccounts(List<Account> accounts) {
        // Check object-level permission
        if (!SecurityUtil.canUpdateObject('Account')) {
            throw new SecurityException('No update permission on Account');
        }
        
        // Check field-level permission
        if (!SecurityUtil.canWriteField('Account', 'Description')) {
            throw new SecurityException('No permission to update Account.Description field');
        }
        
        for (Account acc : accounts) {
            acc.Description = 'Updated';
        }
        
        update accounts;
    }
}
  

5. Common Security Best Practices

Always Use Parameterized Queries


// ❌ Vulnerable to injection
String searchTerm = 'Acme'; // Could be malicious input
List<Account> accounts = [SELECT Id FROM Account WHERE Name = :searchTerm];

// ✅ Safe - uses bind variables
String searchTerm = 'Acme';
List<Account> accounts = [SELECT Id FROM Account WHERE Name = :searchTerm];
  

Validate User Input


public with sharing class InputValidator {
    public static String sanitizeInput(String input) {
        if (String.isBlank(input)) {
            return null;
        }
        
        // Remove potentially dangerous characters
        return input.replaceAll('[^a-zA-Z0-9 ]', '');
    }
    
    public static void createAccount(String name) {
        String safeName = sanitizeInput(name);
        
        if (String.isBlank(safeName)) {
            throw new ValidationException('Invalid account name');
        }
        
        Account acc = new Account(Name = safeName);
        insert acc;
    }
}

public class ValidationException extends Exception { }
  

6. Security Checklist

Key Takeaways