Programmatic Security: Apex Enforcement & Secure Coding
Programmatic security covers how developers enforce security in Apex code, prevent common vulnerabilities, and securely integrate with external systems. This is CTA Objective 2.4 — and the board expects you to know when declarative security is insufficient and how to enforce security correctly in code.
The Sharing Keyword Spectrum
Every Apex class runs in one of three sharing contexts. Choosing the wrong one is a common security vulnerability.
with sharing / without sharing / inherited sharing
| Keyword | Record Access | Use Case |
|---|---|---|
with sharing | Enforces the running user’s sharing rules (OWD, role hierarchy, sharing rules) | Default for most classes — user-facing logic |
without sharing | Ignores all sharing rules — sees all records | System operations, integrations, admin utilities |
inherited sharing | Inherits the sharing context of the calling class | Utility classes called from multiple contexts |
| (no keyword) | Inherits sharing context from calling class; defaults to without sharing only when used as the entry point of an Apex transaction | Avoid — explicit is always better |
flowchart TD
Q1{"Is this class user-facing\n(controller, service for UI)?"}
Q1 -->|Yes| WithSharing["Use 'with sharing'\nEnforce user's record access"]
Q1 -->|No| Q2{"Is this a utility/helper\ncalled from multiple contexts?"}
Q2 -->|Yes| Inherited["Use 'inherited sharing'\nInherit caller's context"]
Q2 -->|No| Q3{"Does it NEED to see\nall records regardless\nof user access?"}
Q3 -->|Yes| WithoutSharing["Use 'without sharing'\nDocument WHY"]
Q3 -->|No| WithSharing2["Use 'with sharing'\nDefault to restrictive"]
style WithSharing fill:#2e7d32,color:#fff
style WithSharing2 fill:#2e7d32,color:#fff
style WithoutSharing fill:#c62828,color:#fff
style Inherited fill:#1565c0,color:#fff
The “without sharing” Risk
Every class marked without sharing is a potential security bypass. The CTA board will question any architecture that uses without sharing extensively. Always document WHY a class needs to bypass sharing, and keep the scope as narrow as possible — ideally a single inner class method that does the elevated query, not the entire outer class.
Sharing Context Interaction
flowchart LR
subgraph "Call Chain"
A["Controller\n(with sharing)"] --> B["Service\n(with sharing)"]
B --> C["Utility\n(inherited sharing)"]
C --> D["Data Access\n(without sharing)"]
end
subgraph "Effective Sharing"
EA["with sharing"] -.-> EB["with sharing"]
EB -.-> EC["with sharing\n(inherited from B)"]
EC -.-> ED["without sharing\n(overrides)"]
end
style A fill:#2e7d32,color:#fff
style B fill:#2e7d32,color:#fff
style C fill:#1565c0,color:#fff
style D fill:#c62828,color:#fff
inherited sharing Behavior
inherited sharing in the top-level context (e.g., a trigger or anonymous Apex) runs as with sharing. It only inherits a different context when called from another class. This makes it safe as a default for utility classes.
CRUD and FLS Enforcement
Sharing controls which records a user sees. CRUD and FLS control what operations they can perform and which fields they can access. Apex does NOT automatically enforce CRUD or FLS — developers must enforce it explicitly.
Enforcement Methods Comparison
| Method | Enforces | Available Since | Where It Works |
|---|---|---|---|
WITH SECURITY_ENFORCED | Object CRUD + FLS (Read only) | Spring ‘20 (GA) | SOQL queries |
WITH USER_MODE | CRUD + FLS + Sharing | Spring ‘23 | SOQL + DML |
stripInaccessible() | FLS | Spring ‘20 (GA) | Apex (before DML or after query) |
Schema.describe | Manual check | Always | Apex (programmatic checks) |
Security.stripInaccessible() | FLS per operation type | Spring ‘20 (GA) | Apex |
WITH SECURITY_ENFORCED
Appended to a SOQL query to enforce FLS on all referenced fields. If the running user lacks Read access to any field in the query, a System.QueryException is thrown.
// Enforces FLS -- throws exception if user can't read any fieldList<Account> accounts = [ SELECT Name, Phone, AnnualRevenue FROM Account WHERE Industry = 'Technology' WITH SECURITY_ENFORCED];Limitations:
- Read-only enforcement (does not apply to DML)
- Throws an exception rather than stripping fields — all or nothing
- Enforces both object-level CRUD and field-level security on SOQL SELECT queries
- Does not enforce sharing (use
with sharingfor that)
WITH USER_MODE / WITH SYSTEM_MODE
The modern, comprehensive approach. Available on both SOQL and DML.
// USER_MODE: Enforces CRUD + FLS + sharingList<Account> accounts = [ SELECT Name, Phone, AnnualRevenue FROM Account WHERE Industry = 'Technology' WITH USER_MODE];
// USER_MODE on DML: Enforces CRUD + FLSDatabase.insert(newAccounts, AccessLevel.USER_MODE);Database.update(existingAccounts, AccessLevel.USER_MODE);// SYSTEM_MODE: Bypasses CRUD + FLS + sharing (explicit bypass)List<Account> allAccounts = [ SELECT Name, Phone, AnnualRevenue FROM Account WITH SYSTEM_MODE];Advantages over WITH SECURITY_ENFORCED:
- Works on both SOQL and DML (SECURITY_ENFORCED is SOQL-only)
- Inaccessible fields are silently stripped (no exception)
- Enforces sharing rules automatically
- Single consistent API for all security enforcement
CTA Recommendation
For new development, recommend USER_MODE as the default for all SOQL and DML operations. It provides the most comprehensive security enforcement with the least code. Use SYSTEM_MODE only for system operations that explicitly need to bypass user permissions.
stripInaccessible()
Strips fields the running user cannot access before DML or after a query. Provides granular control over which operation type to check.
// Strip fields user can't read from query resultsList<Account> accounts = [SELECT Name, Phone, AnnualRevenue FROM Account];SObjectAccessDecision decision = Security.stripInaccessible( AccessType.READABLE, accounts);List<Account> sanitized = decision.getRecords();// sanitized records have inaccessible fields removed
// Strip fields user can't create before insertList<Account> newAccounts = new List<Account>();// ... populate accountsSObjectAccessDecision insertDecision = Security.stripInaccessible( AccessType.CREATABLE, newAccounts);insert insertDecision.getRecords();AccessType options: READABLE, CREATABLE, UPDATABLE, UPSERTABLE
Enforcement Selection Guide
flowchart TD
Start["Need to enforce\nCRUD/FLS in Apex"] --> Q1{"SOQL query\nor DML?"}
Q1 -->|"SOQL"| Q2{"Need CRUD + FLS\nor just FLS?"}
Q2 -->|"CRUD + FLS"| UserMode["WITH USER_MODE\n(Recommended)"]
Q2 -->|"CRUD + FLS (SOQL only)"| Q3{"Want exception\nor strip fields?"}
Q3 -->|"Exception"| SecEnf["WITH SECURITY_ENFORCED"]
Q3 -->|"Strip silently"| Strip1["Query + stripInaccessible()"]
Q1 -->|"DML"| Q4{"Want comprehensive\nenforcement?"}
Q4 -->|"Yes"| UserModeDML["Database.op(records, USER_MODE)\n(Recommended)"]
Q4 -->|"Granular control"| Strip2["stripInaccessible()\nbefore DML"]
style UserMode fill:#2e7d32,color:#fff
style UserModeDML fill:#2e7d32,color:#fff
style SecEnf fill:#1565c0,color:#fff
Named Credentials
Named Credentials store callout endpoint URLs and authentication details, keeping secrets out of Apex code. They are essential for secure integrations.
Why Named Credentials Matter
| Without Named Credentials | With Named Credentials |
|---|---|
| Credentials hardcoded or in Custom Settings | Credentials managed declaratively |
| Endpoint URLs in code | Endpoint URL configured in Setup |
| OAuth token management in Apex | Platform handles token lifecycle |
| Secrets in version control | Secrets stored securely by platform |
| Each developer manages auth | Auth configured once, used everywhere |
Named Credential Usage in Apex
// Callout using Named Credential -- no auth code neededHttpRequest req = new HttpRequest();req.setEndpoint('callout:My_ERP_System/api/orders'); // Named Credential prefixreq.setMethod('GET');Http http = new Http();HttpResponse res = http.send(req);// Platform automatically adds authentication headersNamed Credential vs Remote Site Setting
| Feature | Named Credential | Remote Site Setting |
|---|---|---|
| Stores auth details | Yes | No |
| Token management | Automatic (OAuth) | Manual (in code) |
| CSRF protection | Built-in | Manual |
| Per-user vs Named Principal | Both | N/A |
| Use case | Authenticated callouts | Unauthenticated callouts only |
CTA Architecture Rule
Always recommend Named Credentials for callouts. If a CTA scenario shows Apex code with hardcoded credentials or manual OAuth token management, flag it as a security risk and recommend Named Credentials as the fix. The board values this.
Apex Crypto
The Crypto class provides cryptographic functions for signing, encrypting, hashing, and generating MACs.
Common Crypto Use Cases
| Operation | Method | Use Case |
|---|---|---|
| Hashing | Crypto.generateDigest('SHA-256', data) | Data integrity verification |
| HMAC | Crypto.generateMac('HmacSHA256', data, key) | Webhook signature validation |
| Encryption | Crypto.encrypt('AES256', key, iv, data) | Encrypt sensitive data in transit |
| Decryption | Crypto.decrypt('AES256', key, iv, data) | Decrypt received data |
| Digital Signature | Crypto.sign('RSA-SHA256', data, privateKey) | Sign outbound messages |
| Random | Crypto.getRandomInteger() / Crypto.generateAesKey(256) | Generate tokens, keys |
Crypto Key Management
Never hardcode encryption keys in Apex. Use Custom Settings (Protected), Named Credentials, or Platform Encryption’s key management. Keys in code are visible to anyone with “Author Apex” permission and are stored in version control.
Secure Coding Practices
SOQL Injection Prevention
SOQL injection occurs when user input is concatenated directly into SOQL queries.
// VULNERABLE -- user input directly in query stringString userInput = ApexPages.currentPage().getParameters().get('name');String query = 'SELECT Id, Name FROM Account WHERE Name = \'' + userInput + '\'';List<Account> accounts = Database.query(query); // SOQL INJECTION RISK
// SAFE -- use bind variablesString userInput = ApexPages.currentPage().getParameters().get('name');List<Account> accounts = [SELECT Id, Name FROM Account WHERE Name = :userInput];
// SAFE -- use String.escapeSingleQuotes() for dynamic SOQLString userInput = ApexPages.currentPage().getParameters().get('name');String safe = String.escapeSingleQuotes(userInput);String query = 'SELECT Id, Name FROM Account WHERE Name = \'' + safe + '\'';List<Account> accounts = Database.query(query);SOQL Injection
Always use bind variables for static SOQL. For dynamic SOQL, always use String.escapeSingleQuotes(). This is a top Salesforce security review finding and will be flagged in any architecture review.
Cross-Site Scripting (XSS) Prevention
XSS occurs when user-supplied data is rendered in HTML without encoding.
| Context | Prevention | Visualforce | LWC |
|---|---|---|---|
| HTML body | HTML encode | {!HTMLENCODE(value)} or <apex:outputText escape="true"/> | Automatic (template auto-escapes) |
| JavaScript | JavaScript encode | {!JSENCODE(value)} | Use properties, not innerHTML |
| URL parameters | URL encode | {!URLENCODE(value)} | Use NavigationMixin |
| CSS | CSS encode | Avoid dynamic CSS values | Avoid dynamic CSS values |
Additional Secure Coding Practices
| Vulnerability | Prevention |
|---|---|
| CSRF | Use Salesforce anti-CSRF tokens (built into VF/LWC); validate state parameter in OAuth |
| Open redirect | Validate redirect URLs against allowlist; never use user input directly |
| Insecure direct object reference | Always check CRUD/FLS/sharing before returning records |
| Sensitive data exposure | Use Shield Encryption; mask fields in UI; avoid logging sensitive data |
| Insufficient logging | Log security events; use Event Monitoring; implement Transaction Security |
Session-Based Permission Sets
Session-based Permission Sets activate only when specific conditions are met during a user’s session. They provide conditional, elevated access.
How Session-Based Permission Sets Work
flowchart LR
Login["User Logs In"] --> Session["Standard Session\n(Base permissions)"]
Session --> Trigger["Trigger Event\n(Flow, Auth Provider, Custom)"]
Trigger --> Activate["Session-Based PS Activated\n(Elevated permissions)"]
Activate --> Duration["Active until:\n- Session ends\n- Explicitly deactivated\n- Timeout (if configured)"]
style Session fill:#1565c0,color:#fff
style Activate fill:#2e7d32,color:#fff
Use Cases for Session-Based Permission Sets
| Scenario | Implementation |
|---|---|
| Step-up authentication | User re-authenticates to access sensitive data; session PS grants access to encrypted fields |
| Time-limited elevated access | Approval workflow activates session PS for data export during audit period |
| Location-based access | Flow checks IP range; activates session PS for internal network users |
| Role-based escalation | Manager approves temporary access; Flow activates session PS for the requesting user |
Activating Session-Based Permission Sets
| Method | How | Best For |
|---|---|---|
| Flow | SessionPermissionSetActivation element | Most scenarios — declarative |
| Auth Provider | Custom Auth Provider registration handler | SSO-triggered activation |
| Apex | SessionPermissionSetActivation record insert | Complex logic or API-driven |
| Connected App | Session-based PS assigned to Connected App | Integration-specific access |
Secure Integration Callout Architecture
When designing integrations, the CTA board expects you to articulate the full security stack for callouts — not just “use Named Credentials.”
flowchart TD
subgraph "Apex Code Layer"
Controller["Apex Controller\n(with sharing)"]
Controller --> FLS["Enforce FLS\n(USER_MODE)"]
FLS --> Callout["HttpRequest via\ncallout:Named_Credential"]
end
subgraph "Platform Security Layer"
NC["Named Credential\n(endpoint URL)"]
EC["External Credential\n(auth config)"]
PSAccess["Permission Set\n(controls who can\nuse this credential)"]
end
subgraph "Wire Security"
TLS["TLS 1.2+\n(in transit)"]
Cert["Mutual TLS\n(certificate auth)"]
end
Callout --> NC --> EC
EC --> PSAccess
NC --> TLS
EC --> Cert
style Controller fill:#2e7d32,color:#fff
style NC fill:#1565c0,color:#fff
style EC fill:#1565c0,color:#fff
style TLS fill:#e65100,color:#fff
Enhanced External Credentials Model
The modern architecture separates concerns: the Named Credential stores the endpoint URL, the External Credential stores the authentication configuration, and a Permission Set controls which users/integrations can use that credential. Multiple Named Credentials can share one External Credential, reducing duplication.
Security in LWC vs Visualforce vs Aura
| Security Aspect | LWC | Aura | Visualforce |
|---|---|---|---|
| XSS protection | Auto-escaped templates | Semi-auto ({!v.value} is safe, $A.util.format is not) | Manual (HTMLENCODE, JSENCODE) |
| CSRF protection | Built-in | Built-in | Built-in (anti-CSRF token) |
| Data access | Via Apex (enforce in Apex) | Via Apex (enforce in Apex) | Direct binding (enforce FLS in controller) |
| Namespace isolation | Shadow DOM | Locker Service | None |
| CSP enforcement | Strict | Moderate | Relaxed |
Related Topics
- Sharing Model —
with sharingenforces the sharing model in code - Field & Object Security — CRUD/FLS that Apex must enforce
- Shield Encryption — encrypted field access in Apex
- Identity & SSO — Named Credentials and OAuth in integrations
- Security Best Practices — secure coding patterns and anti-patterns
- Integration — Named Credentials and callout security
Sources
- Salesforce Help: Apex Security
- Salesforce Help: stripInaccessible
- Salesforce Help: SOQL Security Modes
- Salesforce Help: Named Credentials
- Salesforce Help: Session-Based Permission Sets
- Salesforce Developers: Secure Coding Guidelines
- OWASP: SOQL Injection Prevention