ESC

AI-powered search across all blog posts and tools

Architecture · February 11, 2026

Salesforce Security Best Practices - The Architect's Checklist

A comprehensive security checklist for Salesforce architects covering CRUD/FLS enforcement, Shield encryption, Event Monitoring, and OWASP-aligned Apex patterns.

☕ 10 min read 📅 February 11, 2026
  • WITH SECURITY_ENFORCED in SOQL throws a QueryException if the user lacks FLS access — use stripInaccessible for a gentler alternative
  • Shield Platform Encryption encrypts at rest at the database level — Native encryption only obscures, it does not protect against Salesforce admin access
  • Event Monitoring gives you login history, report exports, and API usage logs — essential for regulated industries

Security reviews catch issues that code reviews miss. I’ve sat on both sides of the Salesforce security review table — submitting apps for AppExchange review and conducting internal architecture reviews — and the same categories of vulnerabilities come up repeatedly. CRUD and FLS enforcement gaps, missing input sanitization, over-permissioned profiles, unmonitored data exports.

This is the checklist I work through on every implementation. It’s organized by security layer so you can address each domain systematically.


The Security Layers Model

Salesforce Security Layers — Defence in Depth
Salesforce Security LayersNetwork & Session (IP Ranges, Trusted IPs, MFA, Session Timeout)Org Security (Profiles, Permission Sets, Login Policies)Object/Field Security (CRUD, FLS, Record Sharing)Data Security (Encryption, Masking, Tokenization)Apex CodeCRUD/FLS EnforcementShieldPlatformEncryption

Layer 1: Network and Session Security

Checklist

  • IP whitelisting configured on all integration-connected profiles
  • Trusted IP ranges set per profile for admin/developer access
  • MFA enforced for all users (now required by Salesforce policy)
  • Session timeout set to 2 hours or less for non-service accounts
  • Login hours restricted on profiles where appropriate (e.g., no overnight logins for internal staff)
  • Single Sign-On (SSO) enforced via identity provider where possible — reduces password sprawl

Common Gap

⚠️ Session Timeout Risk

Session timeout is often left at the default 8 hours. For any org holding PII or financial data, drop it to 2 hours or less and ensure “Force relogin after Login-As-User” is enabled.

The Problem

Scenario: A sales rep’s laptop is left unattended at a conference. Their Salesforce session is still active — the default 8-hour timeout hasn’t expired. Someone walks up, opens the browser, and has full access to the org including customer contact details, deal values, and the ability to export reports.

The Solution

Set session timeout to 2 hours (or less) for all non-service-account profiles. Enable “Force relogin after Login-As-User” and “Lock sessions to the IP address from which they originated” for admin profiles. For any org handling PII or financial data, also enable the “Require HttpOnly attribute” and “Require Secure Connections (HTTPS)” session settings.


Layer 2: Org-Level Access Control

Checklist

  • Minimum Permission Set principle applied — no “grant all” profiles
  • System Administrator profile reserved for dedicated admins only, not developers
  • Modify All Data and View All Data permissions explicitly justified and documented
  • API Enabled removed from end-user profiles that don’t need API access
  • Custom Permissions used for feature access rather than profile hacks
  • Muting Permission Sets used to reduce over-permissioning in managed packages

The Permission Set Audit Query

Run this SOQL to surface all users with Modify All Data:

SELECT PermissionSet.Label, Assignee.Name, Assignee.Profile.Name
FROM PermissionSetAssignment
WHERE PermissionSet.PermissionsModifyAllData = true
ORDER BY Assignee.Name

If this returns more than 5-10 records in a typical org, you have a problem.

💡 Pro Tip

Schedule this SOQL query to run monthly via a lightweight Apex batch that emails the results to your security team. When the count suddenly jumps — say, from 6 to 14 records — you get an alert before that over-permissioned access causes a problem. Pair this with a similar query for PermissionsViewAllData and PermissionsManageUsers.


Layer 3: CRUD and FLS Enforcement in Apex

This is the layer most developers under-invest in. Apex runs in system context by default — it bypasses CRUD and FLS. That means a user who can invoke your Apex (via a Flow, LWC, or API) gets whatever data access your code gives them, regardless of their profile.

WITH SECURITY_ENFORCED

// Throws QueryException if user lacks FLS read on any queried field
List<Account> accounts = [
    SELECT Id, Name, AnnualRevenue, BillingStreet
    FROM Account
    WHERE OwnerId = :UserInfo.getUserId()
    WITH SECURITY_ENFORCED
];
⚠️ Warning

Limitation: If ANY field in the SELECT is inaccessible, the entire query throws. This can be overly aggressive in read scenarios. Use stripInaccessible instead when you want graceful degradation.

stripInaccessible — The Gentler Alternative

Read Operations

// Query runs, then platform strips fields the user can't see
SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.READABLE,
    [SELECT Id, Name, AnnualRevenue, SSN__c FROM Account WHERE Id = :accountId]
);

List<Account> safeAccounts = (List<Account>) decision.getRecords();
// SSN__c will be null if user lacks FLS read — no exception thrown

Write Operations

// Strip fields the user can't write before DML
SObjectAccessDecision writeDecision = Security.stripInaccessible(
    AccessType.UPDATABLE,
    accountsToUpdate
);
update writeDecision.getRecords();

Manual CRUD Checks

Manual CRUD/FLS check patterns

For cases where WITH SECURITY_ENFORCED and stripInaccessible don’t fit the pattern, check CRUD manually:

// Check object-level read access
if (!Schema.sObjectType.Account.isQueryable()) {
    throw new AuraHandledException('Insufficient permissions to read Accounts');
}

// Check field-level access
Schema.DescribeFieldResult fieldDescribe =
    Schema.sObjectType.Account.fields.AnnualRevenue;

if (!fieldDescribe.isAccessible()) {
    throw new AuraHandledException('Field access denied: AnnualRevenue');
}

// Check before DML
if (!Schema.sObjectType.Account.isUpdateable()) {
    throw new AuraHandledException('Insufficient permissions to update Accounts');
}
The Problem

Scenario: A community portal lets external users submit service requests. The underlying Apex controller queries Account fields including AnnualRevenue and BillingStreet without CRUD/FLS checks. A determined user discovers they can call the Apex method directly via the API and retrieve financial data for any Account in the org — data their profile explicitly restricts.

The Solution

Replace the bare SOQL query with Security.stripInaccessible(AccessType.READABLE, …). This runs the query in system context (preserving performance) but strips any fields the calling user’s profile cannot access before the results are returned to the caller. Sensitive fields like AnnualRevenue will be null for users without FLS read — without throwing an exception that breaks the UI.

The OWASP Salesforce Top Issues

The OWASP Salesforce Security Cheat Sheet highlights:

1. SOQL Injection — Never concatenate user input into SOQL strings:

Vulnerable

String query = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';
List<Account> results = Database.query(query);

Safe

List<Account> results = [SELECT Id FROM Account WHERE Name = :userInput];

2. Cross-Site Scripting (XSS) — In Aura components, always use {!v.value} bound expressions, never {!v.value + someString}. In LWC, use textContent not innerHTML.

3. Open Redirect — Validate URLs before redirecting:

// Check the URL is within your known safe domains before redirect
if (!url.startsWith('https://yourorg.salesforce.com') &&
    !url.startsWith('https://yourdomain.com')) {
    throw new AuraHandledException('Invalid redirect URL');
}
💡 Pro Tip

Use the Salesforce Code Analyzer (formerly PMD for Apex) in your CI pipeline to automatically flag CRUD/FLS violations, SOQL injection risks, and other security issues on every pull request. Configure it to fail the build on any “Critical” or “High” severity findings.


Layer 4: Data Encryption

Native Encryption vs Shield Platform Encryption

Native Encrypted FieldsShield Platform Encryption
Encrypts at restYes (AES-128)Yes (AES-256)
SearchableNoYes (with deterministic encryption)
Encryption key managed bySalesforceCustomer (Bring Your Own Key)
Works with reports/formulasNoYes
Additional costNoYes (Shield add-on)
🚨 Important

Classic Encrypted Fields use AES-128 encryption at rest — this is real encryption, not mere masking. However, the encryption key is managed entirely by Salesforce, meaning Salesforce staff can technically access the decrypted values. If you’re handling PII, PHI, or PCI data and need customer-controlled key management and AES-256 encryption, you need Shield Platform Encryption.

Shield Platform Encryption Checklist

  • Tenant Secret generated and backed up securely (losing it means losing the data)
  • Encryption policy defined per field — not all fields need encryption, only PII/PCI/PHI
  • Search indexes planned — deterministic encryption required for searchable fields
  • Formula fields audited — formulas referencing encrypted fields return masked values
  • Apex code reviewed — String.valueOf() on encrypted fields returns the unmasked value, check who can call those methods
Key Management Best Practice

Store your Shield tenant secret export in a secure offline location (hardware key or enterprise password manager). If the key is lost and the org is ever restored from backup, the encrypted data is irrecoverable.


Layer 5: Event Monitoring

Event Monitoring (available with Event Monitoring add-on or Shield) gives you detailed logs for:

  • Login events (including failed logins and geolocation)
  • Report exports (who exported what, and how many records)
  • REST API usage (which integrations are doing what)
  • UI tracking events

Key Queries for Security Review

Failed Logins

SELECT EventDate, Username, SourceIp, LoginType
FROM LoginEvent
WHERE LoginType != 'Application'
  AND EventDate = LAST_N_DAYS:30
ORDER BY EventDate DESC

Large Exports

SELECT EventDate, Username, ReportName, NumberOfColumns, RowsProcessed
FROM ReportEvent
WHERE RowsProcessed > 5000
  AND EventDate = LAST_N_DAYS:7
ORDER BY RowsProcessed DESC

Unexpected API Access

SELECT EventDate, Username, SourceIp, ApiType
FROM ApiEvent
WHERE SourceIp NOT IN ('10.0.0.1', '10.0.0.2')
  AND EventDate = LAST_N_DAYS:1

Real-Time Event Monitoring with Platform Events

For immediate response to security events, subscribe to LoginEventStream in a Platform Event trigger:

trigger LoginEventTrigger on LoginEventStream (before insert) {
    for (LoginEventStream event : Trigger.new) {
        if (event.LoginType == 'Application' && event.Country != 'US') {
            // Alert security team via email/Slack
        }
    }
}
The Problem

Scenario: A former employee’s credentials are used to export 50,000 customer records via a report three days after their offboarding. The access was deactivated in HR systems but the Salesforce user was not deactivated. The breach isn’t discovered until a routine audit two weeks later.

The Solution

Configure an Event Monitoring alert on ReportEvent where RowsProcessed > 5000 that sends an immediate email to the security team. Additionally, integrate your offboarding process with Salesforce user deactivation — either via an HR system integration or a manual checklist item. A LoginEventStream trigger that fires on logins from unexpected countries provides a second layer of detection for compromised credentials.


Layer 6: Connected App and Integration Security

Checklist

  • Named Credentials used for all outbound callouts — no credentials stored in Custom Settings or code
  • Connected App scopes limited to minimum required (avoid full scope)
  • Client Credential OAuth flow used for server-to-server integrations, not password flow
  • Refresh token rotation enabled on OAuth Connected Apps
  • IP enforcement enabled on integration-specific profiles
  • REST API access restricted via Permission Set (not profile) for integration users

Named Credentials in Apex

// Always use Named Credentials — never hardcode tokens
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_Integration_NC/api/v1/accounts');
req.setMethod('GET');
req.setHeader('Content-Type', 'application/json');

Http http = new Http();
HttpResponse res = http.send(req);
💡 Pro Tip

Run the Salesforce Health Check from Setup regularly and aim for a score above 90. Health Check automatically flags insecure session settings, password policies, and network access configurations against Salesforce’s Baseline Standard. Set a recurring calendar reminder to review it quarterly.


The Pre-Go-Live Security Checklist

Print this and work through it before any production launch:

  • Health Check score is above 90
  • Guest user profile has no object access (unless explicitly needed)
  • No active users with “Manage Users” who are not administrators
  • All Apex classes use CRUD/FLS enforcement or have documented justification
  • No SOQL queries built with string concatenation
  • Session timeout is 2 hours or less
  • MFA is enforced for all users
  • No Named Credentials with stored passwords (use OAuth)
  • Event Monitoring alerts configured for failed logins and large exports
  • Encryption policy defined for any PII fields
  • Guest user sharing rules reviewed (especially for Community/Experience Cloud sites)

Security is never “done” — it’s a continuous practice. Salesforce’s Trust site publishes regular security advisories, and the platform evolves. Schedule a quarterly security review in your calendar and work through this checklist each time.


What’s the security gap you see most often in Salesforce implementations? I’ve found CRUD/FLS enforcement in Apex to be the single biggest blind spot — developers simply don’t think about it until a security review forces the issue. Drop your observations in the comments, especially if you’ve caught something unusual in a review that isn’t on this checklist.


Test Your Knowledge

What is the difference between WITH SECURITY_ENFORCED and stripInaccessible?
Why should you use Named Credentials instead of storing API keys in Custom Settings?

How did this article make you feel?

Comments

Salesforce Tip

🎉

You finished this article!

What to read next

Contents