
Sf Security
- 42 installs
- 12 repo stars
- Updated July 14, 2026
- clientell-ai/salesforce-skills
Apply Salesforce CRUD/FLS, USER_MODE SOQL/DML, stripInaccessible, and with-sharing class patterns in Apex you are writing or reviewing.
About
sf-security is a Salesforce security patterns reference skill for solo builders and small teams shipping on Platform who need correct CRUD and field-level security in Apex without memorizing every API variant. It documents USER_MODE for SOQL and dynamic queries, stripInaccessible before insert, update, and returning records to users, and Database DML operations with AccessLevel.USER_MODE, plus class-level with sharing versus without sharing guidance. Use it while implementing integrations and services in Build, and again in Ship when hardening code paths before production or partner review. The skill is procedural knowledge packaged as copy-pasteable Apex patterns—not a scanner—so you still run org security reviews and checkstyle rules separately. It pairs naturally with broader Salesforce skills in the same repo when you move from feature code to compliance-safe data access.
- SOQL and dynamic query examples with WITH USER_MODE and Database.query AccessLevel.USER_MODE
- stripInaccessible for CREATABLE, UPDATABLE, and READABLE flows before DML and API responses
- Database.insert/update/upsert with AccessLevel.USER_MODE
- Guidance to default to public with sharing and reserve without sharing for explicit system-level cases
- Reference-style snippets for sharing model and field-level security enforcement
Sf Security by the numbers
- 42 all-time installs (skills.sh)
- Ranked #1,393 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/clientell-ai/salesforce-skills --skill sf-securityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 42 |
|---|---|
| repo stars | ★ 12 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 14, 2026 |
| Repository | clientell-ai/salesforce-skills ↗ |
What it does
Apply Salesforce CRUD/FLS, USER_MODE SOQL/DML, stripInaccessible, and with-sharing class patterns in Apex you are writing or reviewing.
Files
Salesforce Security Auditor
You are a Salesforce security specialist. Audit code for the vulnerabilities that cause AppExchange security review failures.
Critical Violations to Detect
1. Missing CRUD/FLS Enforcement
Scan for DML operations without Security.stripInaccessible():
// VIOLATION
insert records;
// COMPLIANT
SObjectAccessDecision decision = Security.stripInaccessible(AccessType.CREATABLE, records);
insert decision.getRecords();Search patterns:
insert/update/delete/upsertwithout precedingstripInaccessibleDatabase.insert/Database.updatewithoutAccessLevel.USER_MODE
2. Missing WITH USER_MODE in SOQL
Scan for SOQL queries without WITH USER_MODE:
// VIOLATION
[SELECT Id FROM Account WHERE Name = :name]
// COMPLIANT
[SELECT Id FROM Account WHERE Name = :name WITH USER_MODE]3. Missing with sharing
All classes should declare sharing model explicitly:
// VIOLATION
public class MyClass { }
// COMPLIANT
public with sharing class MyClass { }Only use without sharing when explicitly needed (e.g., running aggregate queries for dashboard data) and document the reason.
4. SOQL Injection
Scan for string concatenation in dynamic SOQL:
// VIOLATION — injection risk
String query = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';
// COMPLIANT — use bind variable
String query = 'SELECT Id FROM Account WHERE Name = :userInput';
// COMPLIANT — use escapeSingleQuotes for truly dynamic queries
String safeName = String.escapeSingleQuotes(userInput);5. PII/Sensitive Data in Debug Logs
Scan for debug statements that might expose sensitive data:
// VIOLATION
System.debug('User SSN: ' + contact.SSN__c);
System.debug('Credit Card: ' + payment.CardNumber__c);
System.debug(JSON.serialize(sensitiveRecord));
// COMPLIANT — debug ID only
System.debug('Processing contact: ' + contact.Id);6. Hardcoded Credentials
Scan for:
- Hardcoded URLs, API keys, passwords, tokens
- Credentials in string literals instead of Named Credentials or Custom Metadata
7. Cross-Site Scripting (XSS) in Visualforce
Scan .page files for unescaped output:
{!variable}withoutJSENCODE,HTMLENCODE, orURLENCODE<apex:outputText escape="false">
8. FLS Schema API Checks
Pre-check permissions before CRUD using Schema Describe:
if (!Schema.sObjectType.Account.isAccessible()) {
throw new SecurityException('No read access to Account');
}
if (!Schema.sObjectType.Account.fields.Name.getDescribe().isUpdateable()) {
throw new SecurityException('Cannot update Account.Name');
}9. Sharing Model
- Organization-Wide Defaults (OWD): Private, Public Read Only, Public Read/Write, Controlled by Parent
- Role Hierarchy: Users see records owned by subordinates
- Sharing Rules: Owner-based and criteria-based rules extend access
- Apex Managed Sharing: Programmatic sharing via
AccountShare,OpportunityShare, etc. - Check sharing with
Schema.sObjectType.Account.isAccessible()at object level
10. Custom Permission Checks
if (FeatureManagement.checkPermission('MyCustomPermission')) {
// User has the custom permission
}11. WITH SECURITY_ENFORCED vs WITH USER_MODE
| Feature | SECURITY_ENFORCED | USER_MODE |
|---|---|---|
| On FLS violation | Throws exception | Silently strips fields |
| WHERE clause | Not enforced | Enforced |
| Recommendation | Legacy | Preferred |
Audit Workflow
1. Scan all Apex classes:
Glob: force-app/**/*.cls2. Check each file for violations using Grep patterns:
- Classes without
with sharing:^public\s+(virtual\s+|abstract\s+|global\s+)?class - SOQL without USER_MODE:
\[SELECT.*FROM.*(?!WITH USER_MODE)\] - DML without stripInaccessible:
(insert|update|delete|upsert)\s+\w+; - String concat in SOQL:
'SELECT.*'\s*\+ - Debug with sensitive fields:
System\.debug.*\.(SSN|Password|Secret|Token|CardNumber)
3. Generate report with:
- File path and line number for each violation
- Severity (Critical / High / Medium / Low)
- Recommended fix
- Code snippet showing the fix
4. Severity Classification:
- Critical: SOQL injection, missing CRUD/FLS on DML, hardcoded credentials
- High: Missing
with sharing, missing USER_MODE, XSS in Visualforce - Medium: PII in debug logs, overly permissive sharing
- Low: Missing null checks, non-bulkified patterns
Gotchas
WITH SECURITY_ENFORCEDthrows an exception on FLS violation —WITH USER_MODEsilently strips inaccessible fields- Apex runs in system mode by default — security is NOT enforced unless you explicitly add it
- Custom permission checks are cached — recent permission set changes may not reflect immediately
without sharingcode ignores ALL sharing rules — records visible regardless of OWD- Debug logs are accessible to anyone with View Setup permission — never log sensitive data
Security.stripInaccessible()returns a NEW list — the original list is unchanged- String concatenation in dynamic SOQL bypasses bind variable protection even with
USER_MODE
Output Format
## Security Audit Report
### Critical Issues (X found)
| # | File | Line | Issue | Fix |
|---|------|------|-------|-----|
| 1 | AccountService.cls | 45 | DML without CRUD check | Add Security.stripInaccessible() |
### High Issues (X found)
...
### Summary
- Total files scanned: X
- Critical: X | High: X | Medium: X | Low: X
- Recommendation: [PASS/FAIL for AppExchange review]References
- Security Patterns — CRUD/FLS enforcement, sharing model, SOQL injection prevention, XSS, managed sharing, custom permissions
- Security Reference — FLS Schema APIs, sharing deep dive, Shield encryption, OAuth, event monitoring, CSRF, compliance, AppExchange checklist
- Governor Limits — per-transaction limits reference
Scripts
- Security Scan — quick automated scan for common Apex vulnerabilities
Salesforce Security Patterns Reference
CRUD/FLS Enforcement
SOQL — User Mode
// Enforces both CRUD and FLS automatically
List<Account> accounts = [
SELECT Id, Name, Industry
FROM Account
WHERE Industry = :filter
WITH USER_MODE
];
// Dynamic SOQL with user mode
List<Account> accounts = Database.query(
'SELECT Id, Name FROM Account',
AccessLevel.USER_MODE
);DML — stripInaccessible
// Before INSERT
SObjectAccessDecision decision = Security.stripInaccessible(
AccessType.CREATABLE, records
);
insert decision.getRecords();
// Before UPDATE
SObjectAccessDecision decision = Security.stripInaccessible(
AccessType.UPDATABLE, records
);
update decision.getRecords();
// Before returning data to user
SObjectAccessDecision decision = Security.stripInaccessible(
AccessType.READABLE, records
);
return decision.getRecords();
// Check which fields were stripped
Set<String> strippedFields = decision.getRemovedFields().get('Account');Database Operations with AccessLevel
// Insert with user mode
Database.insert(records, AccessLevel.USER_MODE);
// Update with user mode
Database.update(records, AccessLevel.USER_MODE);
// Upsert with user mode
Database.upsert(records, ExternalId__c, AccessLevel.USER_MODE);Sharing Model
Class Declarations
// DEFAULT — always use this
public with sharing class MyService { }
// Only when system-level access is explicitly needed
public without sharing class SystemDataService { }
// Inherits from caller
public inherited sharing class UtilityClass { }When to Use Without Sharing
- Aggregate reporting queries that span ownership
- System-level operations in batch jobs
- Platform event handlers that need cross-user access
- Always document the reason in a comment
SOQL Injection Prevention
Bind Variables (Preferred)
String nameFilter = userInput;
List<Account> results = [
SELECT Id, Name FROM Account
WHERE Name = :nameFilter
WITH USER_MODE
];escapeSingleQuotes (Dynamic SOQL)
String safeName = String.escapeSingleQuotes(userInput);
String query = 'SELECT Id FROM Account WHERE Name = \'' + safeName + '\'';Never Do This
// VULNERABLE — direct concatenation
String query = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';Visualforce XSS Prevention
Output Encoding
<!-- Auto-escaped (safe) -->
<apex:outputText value="{!accountName}"/>
<!-- Manual encoding when needed -->
<script>
var name = '{!JSENCODE(accountName)}';
var url = '{!URLENCODE(accountName)}';
</script>
<!-- DANGEROUS — never use -->
<apex:outputText value="{!accountName}" escape="false"/>Named Credentials (No Hardcoded Secrets)
// GOOD — uses Named Credential
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_Named_Credential/api/resource');
req.setMethod('GET');
// BAD — hardcoded
req.setEndpoint('https://api.example.com/resource');
req.setHeader('Authorization', 'Bearer ' + hardcodedToken);---
Schema Describe FLS Checks
isAccessible Pattern (Read Check)
public class SecureQueryService {
public static List<Account> getAccounts(Set<Id> ids) {
// Object-level read check
if (!Schema.sObjectType.Account.isAccessible()) {
throw new AuraHandledException('Insufficient access to Account');
}
// Field-level read checks
Map<String, Schema.SObjectField> fieldMap =
Schema.sObjectType.Account.fields.getMap();
List<String> queryFields = new List<String>{'Id'};
for (String fieldName : new List<String>{'Name', 'Industry', 'AnnualRevenue', 'Phone'}) {
if (fieldMap.get(fieldName).getDescribe().isAccessible()) {
queryFields.add(fieldName);
}
}
String soql = 'SELECT ' + String.join(queryFields, ', ') +
' FROM Account WHERE Id IN :ids';
return Database.query(soql);
}
}isCreateable Pattern (Insert Check)
public class SecureInsertService {
public static Id createAccount(String name, String industry, Decimal revenue) {
// Object-level create check
if (!Schema.sObjectType.Account.isCreateable()) {
throw new AuraHandledException('Cannot create Account records');
}
Account acct = new Account();
// Field-level create checks
if (Schema.sObjectType.Account.fields.Name.getDescribe().isCreateable()) {
acct.Name = name;
}
if (Schema.sObjectType.Account.fields.Industry.getDescribe().isCreateable()) {
acct.Industry = industry;
}
if (Schema.sObjectType.Account.fields.AnnualRevenue.getDescribe().isCreateable()) {
acct.AnnualRevenue = revenue;
}
insert acct;
return acct.Id;
}
}isUpdateable / isDeletable Checks
// Update check
if (Schema.sObjectType.Account.isUpdateable() &&
Schema.sObjectType.Account.fields.Industry.getDescribe().isUpdateable()) {
acct.Industry = 'Technology';
update acct;
}
// Delete check
if (Schema.sObjectType.Account.isDeletable()) {
delete acct;
}---
Apex Managed Sharing Example
AccountShare — Grant Edit Access to a User
public class AccountSharingService {
/**
* Share an Account record with a specific user.
* Requires the Account OWD to be Private or Public Read Only.
*/
public static void shareAccountWithUser(Id accountId, Id userId, String accessLevel) {
AccountShare share = new AccountShare();
share.AccountId = accountId;
share.UserOrGroupId = userId;
share.AccountAccessLevel = accessLevel; // 'Read' or 'Edit'
share.OpportunityAccessLevel = 'Read'; // Required for AccountShare
share.RowCause = Schema.AccountShare.RowCause.Manual;
Database.SaveResult sr = Database.insert(share, false);
if (!sr.isSuccess()) {
for (Database.Error err : sr.getErrors()) {
System.debug(LoggingLevel.ERROR, 'AccountShare error: ' + err.getMessage());
}
}
}
/**
* Revoke manually shared access for a user on an Account.
*/
public static void revokeAccountShare(Id accountId, Id userId) {
List<AccountShare> shares = [
SELECT Id FROM AccountShare
WHERE AccountId = :accountId
AND UserOrGroupId = :userId
AND RowCause = :Schema.AccountShare.RowCause.Manual
];
if (!shares.isEmpty()) {
delete shares;
}
}
/**
* Bulk share accounts with a public group.
*/
public static void shareAccountsWithGroup(List<Id> accountIds, Id groupId) {
List<AccountShare> shares = new List<AccountShare>();
for (Id acctId : accountIds) {
AccountShare share = new AccountShare();
share.AccountId = acctId;
share.UserOrGroupId = groupId;
share.AccountAccessLevel = 'Read';
share.OpportunityAccessLevel = 'None';
share.RowCause = Schema.AccountShare.RowCause.Manual;
shares.add(share);
}
List<Database.SaveResult> results = Database.insert(shares, false);
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
System.debug('Failed to share account: ' + accountIds[i]);
}
}
}
}---
FeatureManagement.checkPermission() Example
Custom Permission Check in Apex
public class FeatureGateService {
/**
* Check if the running user has a specific custom permission.
* Custom Permissions are defined in Setup and assigned via Permission Sets.
*/
public static Boolean hasFeatureAccess(String customPermissionApiName) {
return FeatureManagement.checkPermission(customPermissionApiName);
}
/**
* Guard a sensitive operation behind a custom permission.
*/
public static void executeSensitiveOperation() {
if (!FeatureManagement.checkPermission('Allow_Mass_Delete')) {
throw new AuraHandledException(
'You do not have the Allow_Mass_Delete permission.'
);
}
// Proceed with mass delete logic
performMassDelete();
}
/**
* Use custom permissions for feature toggling.
*/
public static Map<String, Object> getFeatureFlags() {
return new Map<String, Object>{
'betaDashboard' => FeatureManagement.checkPermission('Beta_Dashboard_Access'),
'advancedReporting' => FeatureManagement.checkPermission('Advanced_Reporting'),
'bulkOperations' => FeatureManagement.checkPermission('Bulk_Operations_Access')
};
}
private static void performMassDelete() {
// Implementation
}
}In LWC (via Apex or Custom Permission Import)
import hasAdvancedReporting from '@salesforce/customPermission/Advanced_Reporting';
export default class MyComponent extends LightningElement {
get showAdvancedTab() {
return hasAdvancedReporting;
}
}In Flow
Decision Element:
Condition: $Permission.Advanced_Reporting = true
True Path: Show advanced features
False Path: Show standard features---
Transaction Security Policy Pattern
Overview
Transaction Security evaluates events in real time and takes automated actions (block, alert, require MFA, end session).
Policy Configuration via Setup
Setup > Transaction Security Policies > New:
1. Select Event Type:
- ApiEvent (API calls)
- LoginEvent (logins)
- ReportEvent (report runs/exports)
- ListViewEvent (list view access)
- BulkApiResultEvent (bulk data downloads)
2. Define Conditions (Condition Builder or Apex):
- Field-based: e.g., QueriedEntities CONTAINS 'Account'
- Threshold-based: e.g., RowsProcessed > 10000
- User-based: e.g., User.Profile != 'System Administrator'
3. Select Action:
- Block: Prevent the operation entirely
- Multi-Factor Authentication: Require MFA challenge
- Notify: Send email notification to admin
- Notify + Block: Alert and prevent
4. Notification Recipients:
- Specific users or groups
- Email template for notificationApex Policy Implementation
global class LargeDataExportPolicy implements TxnSecurity.PolicyCondition {
/**
* Evaluate whether a report event should be blocked.
* Returns true to trigger the configured action (block/notify).
*/
public boolean evaluate(TxnSecurity.Event e) {
// Get event attributes
Integer rowCount = (Integer) e.getAttribute('NumberOfRecords');
String userId = (String) e.getAttribute('UserId');
// Allow system administrators
User u = [SELECT Profile.Name FROM User WHERE Id = :userId];
if (u.Profile.Name == 'System Administrator') {
return false; // Do not trigger action
}
// Block exports with more than 10,000 rows
if (rowCount != null && rowCount > 10000) {
return true; // Trigger the action
}
return false;
}
}Common Policy Patterns
| Event Type | Condition | Action |
|---|---|---|
| LoginEvent | Login from unknown IP / country | Block + Notify |
| LoginEvent | Login outside business hours | Require MFA |
| ReportEvent | Export > 10,000 rows | Block + Notify |
| ApiEvent | Bulk query on sensitive objects | Notify |
| BulkApiResultEvent | Download results > 50,000 records | Block + Notify |
| ListViewEvent | Access to sensitive object list views | Notify |
Salesforce Enterprise Security Reference
1. FLS Schema API Checks
Object-Level Checks
// Check object accessibility before query
if (Schema.sObjectType.Account.isAccessible()) {
List<Account> accounts = [SELECT Id, Name FROM Account];
}
// Check object createability before insert
if (Schema.sObjectType.Account.isCreateable()) {
insert new Account(Name = 'Test');
}
// Check object updateability before update
if (Schema.sObjectType.Account.isUpdateable()) {
update accountRecord;
}
// Check object deletability before delete
if (Schema.sObjectType.Account.isDeletable()) {
delete accountRecord;
}Field-Level Checks
// Check individual field accessibility
if (Schema.sObjectType.Account.fields.Name.getDescribe().isAccessible()) {
// Safe to read Name field
}
if (Schema.sObjectType.Account.fields.AnnualRevenue.getDescribe().isUpdateable()) {
// Safe to update AnnualRevenue field
}
if (Schema.sObjectType.Account.fields.Industry.getDescribe().isCreateable()) {
// Safe to set Industry on insert
}Pre-Check Pattern Before CRUD Operations
public class SecureAccountService {
public static List<Account> getAccounts(Set<Id> accountIds) {
// Object-level check
if (!Schema.sObjectType.Account.isAccessible()) {
throw new SecurityException('No read access to Account');
}
// Field-level checks
List<String> accessibleFields = new List<String>();
Map<String, Schema.SObjectField> fieldMap =
Schema.sObjectType.Account.fields.getMap();
for (String fieldName : new List<String>{'Name', 'Industry', 'AnnualRevenue', 'Phone'}) {
Schema.DescribeFieldResult dfr = fieldMap.get(fieldName).getDescribe();
if (dfr.isAccessible()) {
accessibleFields.add(fieldName);
}
}
String query = 'SELECT Id, ' + String.join(accessibleFields, ', ') +
' FROM Account WHERE Id IN :accountIds';
return Database.query(query);
}
public static void updateAccounts(List<Account> accounts) {
if (!Schema.sObjectType.Account.isUpdateable()) {
throw new SecurityException('No update access to Account');
}
// Strip inaccessible fields before DML
SObjectAccessDecision decision = Security.stripInaccessible(
AccessType.UPDATABLE, accounts
);
update decision.getRecords();
}
}Bulk Field Describe Pattern
Map<String, Schema.SObjectField> fieldMap = Schema.sObjectType.Contact.fields.getMap();
for (String fieldName : fieldMap.keySet()) {
Schema.DescribeFieldResult dfr = fieldMap.get(fieldName).getDescribe();
System.debug(fieldName + ' -> Accessible: ' + dfr.isAccessible()
+ ', Createable: ' + dfr.isCreateable()
+ ', Updateable: ' + dfr.isUpdateable());
}---
2. Sharing Model Deep Dive
Organization-Wide Defaults (OWD)
| OWD Setting | Description |
|---|---|
| Private | Only record owner and users above in role hierarchy can access |
| Public Read Only | All users can read, but only owner/hierarchy can edit |
| Public Read/Write | All users can read and edit all records |
| Controlled by Parent | Access determined by parent record (detail in master-detail) |
Role Hierarchy
- Opens access upward (managers see subordinates' records)
- Does NOT restrict access
- Can be disabled for custom objects via "Grant Access Using Hierarchies" checkbox
- Standard objects always respect role hierarchy
Criteria-Based Sharing Rules
Rule: Share Accounts where Industry = 'Technology'
Share with: Role = Sales Manager
Access Level: Read/Write- Evaluated when record is created or edited
- Based on field values, not ownership
- Supports formula-based criteria
Owner-Based Sharing Rules
Rule: Share records owned by Role = Eastern Sales
Share with: Role = Western Sales
Access Level: Read Only- Based on record ownership (user, role, group)
Territory Management
- Enterprise Territory Management (ETM) for account-based territories
- Territory types, territory models, assignment rules
- Supports multiple territory hierarchies simultaneously
- Territory-based sharing rules
---
3. Apex Managed Sharing
Share Object Structure
Every standard/custom object with Private OWD has a corresponding Share object:
AccountShare,OpportunityShare,CaseShare,LeadShare- Custom objects:
MyObject__Share
Share Record Fields
| Field | Description |
|---|---|
ParentId | ID of the record being shared (e.g., AccountId) |
UserOrGroupId | User, Role, or Public Group receiving access |
AccessLevel | Read, Edit, or All |
RowCause | Reason for sharing (e.g., Manual, custom reason) |
AccountShare Example
AccountShare share = new AccountShare();
share.AccountId = accountId;
share.UserOrGroupId = userId;
share.AccountAccessLevel = 'Edit';
share.OpportunityAccessLevel = 'Read';
share.RowCause = Schema.AccountShare.RowCause.Manual;
Database.SaveResult sr = Database.insert(share, false);
if (!sr.isSuccess()) {
for (Database.Error err : sr.getErrors()) {
System.debug('Share insert error: ' + err.getMessage());
}
}Custom Object Share with Apex Sharing Reason
// Define sharing reason in custom object metadata first
// Then use in Apex:
MyObject__Share share = new MyObject__Share();
share.ParentId = recordId;
share.UserOrGroupId = userId;
share.AccessLevel = 'Edit';
share.RowCause = Schema.MyObject__Share.RowCause.Team_Member__c;
insert share;Bulk Sharing Pattern
public class BulkSharingService {
public static void shareRecordsWithTeam(List<Id> recordIds, Id groupId) {
List<MyObject__Share> shares = new List<MyObject__Share>();
for (Id recordId : recordIds) {
MyObject__Share share = new MyObject__Share();
share.ParentId = recordId;
share.UserOrGroupId = groupId;
share.AccessLevel = 'Edit';
share.RowCause = Schema.MyObject__Share.RowCause.Team_Member__c;
shares.add(share);
}
List<Database.SaveResult> results = Database.insert(shares, false);
for (Integer i = 0; i < results.size(); i++) {
if (!results[i].isSuccess()) {
System.debug('Failed to share record ' + recordIds[i]);
}
}
}
}Deleting Shares
List<AccountShare> sharesToDelete = [
SELECT Id FROM AccountShare
WHERE AccountId = :accountId
AND RowCause = :Schema.AccountShare.RowCause.Manual
AND UserOrGroupId = :userId
];
delete sharesToDelete;---
4. Shield Platform Encryption
Encrypted Field Types
- Text, Text Area, Long Text Area, Rich Text Area
- Email, Phone, URL
- Date, Date/Time
Deterministic vs Probabilistic
| Feature | Deterministic | Probabilistic |
|---|---|---|
| Filter in SOQL | Yes (exact match, case-insensitive) | No |
| Unique enforcement | Yes | No |
| Grouping | Yes (GROUP BY, DISTINCT) | No |
| Security strength | Strong | Strongest |
Bring Your Own Key (BYOK)
- Upload tenant secrets or key material
- Customer controls key lifecycle (rotation, destruction)
- Compatible with HSM-generated keys
- Key rotation does not require data re-encryption (envelope encryption)
Key Management
Setup > Platform Encryption > Key Management
- Generate tenant secret
- Upload customer-supplied key material
- Archive/destroy keys
- Key rotation (recommended every 12 months)Limitations
- Encrypted fields cannot be used in:
- SOQL WHERE, ORDER BY, GROUP BY (unless deterministic)
- Formula fields (cannot reference encrypted fields)
- Criteria-based sharing rules
- Standard/custom report filters
- SOSL searches
- Maximum encrypted fields per object varies by license
---
5. Event Monitoring
Key Event Types
| Event Type | Description | Use Case |
|---|---|---|
| Login | User login attempts | Detect anomalous logins |
| API | API calls | Monitor integrations |
| Report Export | Report exports/prints | Prevent data exfiltration |
| Bulk API | Bulk API operations | Large data movement detection |
| Data Export | Data Loader exports | Track mass downloads |
| URI | Page view events | Usage analytics |
| Lightning Page View | Lightning page loads | Adoption tracking |
Transaction Security Policies
Condition Builder or Apex Policy:
- Event: ReportEvent
- Condition: Report rows > 10000
- Action: Block + Notify Admin
- Event: LoginEvent
- Condition: LoginGeo.Country != 'United States'
- Action: Require MFA (raise session level)Apex Transaction Security Policy
global class DataExportPolicy implements TxnSecurity.PolicyCondition {
public boolean evaluate(TxnSecurity.Event e) {
// Block large data exports outside business hours
Integer hour = DateTime.now().hour();
if (hour < 6 || hour > 22) {
if (e.getAttribute('NumberOfRecords') > 5000) {
return true; // Trigger the action (block/alert)
}
}
return false;
}
}Real-Time vs Historical
- Real-time: Transaction Security policies evaluate as events occur
- Historical: EventLogFile objects stored for 30 days (1 day with add-on), queryable via SOQL/REST
---
6. OAuth Flows for Connected Apps
Web Server Flow (Authorization Code)
1. Redirect user to: https://login.salesforce.com/services/oauth2/authorize
?response_type=code
&client_id=CONSUMER_KEY
&redirect_uri=CALLBACK_URL
2. User authorizes, Salesforce redirects to callback with ?code=AUTH_CODE
3. Server exchanges code for tokens:
POST https://login.salesforce.com/services/oauth2/token
grant_type=authorization_code
&code=AUTH_CODE
&client_id=CONSUMER_KEY
&client_secret=CONSUMER_SECRET
&redirect_uri=CALLBACK_URL
4. Response includes access_token, refresh_token, instance_urlJWT Bearer Token Flow (Server-to-Server)
1. Create Connected App with digital certificate
2. Build JWT:
Header: {"alg": "RS256"}
Claims: {
"iss": "CONSUMER_KEY",
"sub": "user@example.com",
"aud": "https://login.salesforce.com",
"exp": <expiry_timestamp>
}
3. Sign JWT with private key
4. POST https://login.salesforce.com/services/oauth2/token
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=SIGNED_JWT
5. Response includes access_token, instance_url (no refresh_token)User-Agent Flow (Implicit)
1. Redirect to: https://login.salesforce.com/services/oauth2/authorize
?response_type=token
&client_id=CONSUMER_KEY
&redirect_uri=CALLBACK_URL
2. Token returned in URL fragment: #access_token=...&instance_url=...
(Less secure — token exposed in URL, no refresh token)Device Flow
1. POST https://login.salesforce.com/services/oauth2/token
grant_type=device_code
&client_id=CONSUMER_KEY
2. Response: device_code, user_code, verification_uri
3. User visits verification_uri, enters user_code
4. Poll token endpoint with device_code until authorizedRefresh Tokens
POST https://login.salesforce.com/services/oauth2/token
grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=CONSUMER_KEY
&client_secret=CONSUMER_SECRETCommon Scopes
api— Access REST/SOAP APIsrefresh_token/offline_access— Obtain refresh tokenfull— Full accessweb— Access web UIchatter_api— Chatter REST APIcustom_permissions— Custom permission access
---
7. Session Security
Session Timeout Settings
Setup > Session Settings:
- Timeout value: 15 min to 24 hours (default 2 hours)
- Force logout on session timeout: enabled/disabled
- Lock sessions to IP address from which they originated
- Lock sessions to domainLogin IP Ranges
- Defined per Profile
- Users outside range are completely blocked from login
- No email verification bypass
Trusted IP Ranges
- Defined in Network Access (org-wide)
- Users within range skip identity verification (email/SMS)
- Does NOT block access from other IPs
Session Security Levels
| Level | Granted When | Use Case |
|---|---|---|
| Standard | Username/password login | Normal access |
| High Assurance | MFA verified, Trusted IP + policy | Sensitive operations |
MFA Enforcement
Setup > Identity Verification:
- Require MFA for all users (org-wide)
- Require MFA for specific profiles
- Require High Assurance for Connected Apps
- Session policy: "High Assurance" requirement per Connected AppRaising Session Level in Apex
if (!Auth.SessionManagement.isIpAllowlisted()) {
Auth.SessionManagement.setSessionLevel(Auth.SessionLevel.HIGH_ASSURANCE);
}---
8. Certificate-Based Authentication
Digital Certificates in Salesforce
Setup > Certificate and Key Management:
- Create self-signed certificate
- Create CA-signed certificate request (CSR)
- Import certificate
- Export certificate (public key)Mutual TLS (mTLS)
Setup > Certificate and Key Management:
- Upload mutual authentication certificate
- Configure API client certificate
- Associate with Named Credential for outbound calls
Named Credential configuration:
- Authentication Protocol: Certificate
- Certificate: Select uploaded certJWT Certificate Signing
// Sign JWT with Salesforce certificate for outbound auth
Auth.JWT jwt = new Auth.JWT();
jwt.setSub('user@example.com');
jwt.setAud('https://target-system.com');
jwt.setIss('salesforce-org-id');
Auth.JWS jws = new Auth.JWS(jwt, 'My_Certificate_Name');
String token = jws.getCompactSerialization();
HttpRequest req = new HttpRequest();
req.setHeader('Authorization', 'Bearer ' + token);---
9. CSRF Protection
Salesforce Built-In CSRF Tokens
- Salesforce automatically includes anti-CSRF tokens in all standard pages
- Tokens are validated server-side on form submission
- Unique per user session
Visualforce Automatic Protection
<!-- apex:form automatically includes CSRF token -->
<apex:form>
<apex:commandButton action="{!save}" value="Save"/>
</apex:form>
<!-- CSRF token is embedded as hidden field: _CONFIRMATIONTOKEN -->REST API Considerations
- REST API uses OAuth tokens (not session cookies), inherently CSRF-resistant
- Custom REST endpoints should validate Origin/Referer headers for browser-based calls
- Lightning components use CSRF tokens automatically via Aura/LWC framework
Custom Visualforce CSRF Mitigation
// Verify that the request contains valid CSRF token
// This is automatic for apex:commandButton/apex:actionFunction
// For JavaScript remoting, tokens are handled by the framework
// DANGEROUS: Using onclick with JavaScript actions bypasses CSRF protection
// Avoid: <button onclick="window.location='...'">---
10. Clickjacking Protection
Salesforce Default Protections
Setup > Session Settings:
- Enable clickjack protection for setup pages (default: ON)
- Enable clickjack protection for non-setup pages (default: ON)
- Enable clickjack protection for customer Visualforce pages
with standard headers (default: ON)
- Enable clickjack protection for customer Visualforce pages
with headers disabled (default: OFF — enable this!)Headers Set by Salesforce
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'Visualforce Pages with showHeader="false"
<!-- Must explicitly enable clickjack protection -->
<apex:page showHeader="false">
<!-- Without the session setting enabled, this page can be iframed -->
</apex:page>Custom CSP for Lightning Components
// Lightning Locker / Lightning Web Security handles CSP automatically
// For external scripts, add to CSP Trusted Sites:
// Setup > CSP Trusted Sites > New
// Trusted Site URL: https://cdn.example.com
// Context: Lightning Components---
11. Data Classification & Compliance
GDPR Patterns
Right to Deletion (Right to be Forgotten)
public class GDPRDeletionService {
public static void processErasureRequest(String email) {
// Find all related records
List<Contact> contacts = [
SELECT Id FROM Contact WHERE Email = :email
];
List<Lead> leads = [
SELECT Id FROM Lead WHERE Email = :email
];
// Delete or anonymize records
// Consider: Cases, Activities, Campaign Members, etc.
delete contacts;
delete leads;
// Log the erasure for compliance
GDPR_Erasure_Log__c log = new GDPR_Erasure_Log__c();
log.Request_Date__c = Date.today();
log.Status__c = 'Completed';
log.Records_Processed__c = contacts.size() + leads.size();
insert log;
}
}Consent Management
// Individual object for consent tracking (standard Salesforce)
Individual ind = new Individual();
ind.FirstName = 'John';
ind.LastName = 'Doe';
ind.HasOptedOutProcessing = false;
ind.HasOptedOutSolicit = true;
ind.ShouldForget = false;
insert ind;
// Link to Contact
Contact c = [SELECT Id FROM Contact WHERE Id = :contactId];
c.IndividualId = ind.Id;
update c;Data Residency
- Hyperforce: choose data center region
- Data residency add-on for specific regions
- Cross-region replication considerations
Field Audit Trail (Shield)
- Retain field history data up to 10 years
- Define retention policies per object/field
- Archived data queryable via FieldHistoryArchive big object
// Query archived field history
List<FieldHistoryArchive> history = [
SELECT ParentId, FieldName, OldValue, NewValue, CreatedDate
FROM FieldHistoryArchive
WHERE ParentId = :accountId
AND FieldName = 'AnnualRevenue'
ORDER BY CreatedDate DESC
];---
12. Guest User Security
Guest User Profile Hardening
Setup > Sites > [Site Name] > Public Access Settings:
- Minimize object permissions (principle of least privilege)
- Remove ALL unnecessary object access
- Never grant Modify All or View All
- Restrict field-level security strictlySite-Level Sharing
- Guest users operate under a special sharing model
- "Secure guest user record access" (enforced since Spring '20)
- Records created by guest users default to owner = site guest user
- Must explicitly share records created by guest users with other usersUnauthenticated Access Hardening Checklist
- [ ] Review all Visualforce pages marked "Available for public"
- [ ] Audit all Apex classes with guest profile access
- [ ] Review Lightning components exposed to guest users
- [ ] Check all flows accessible to guest users
- [ ] Remove guest user access to any non-essential APIs
- [ ] Disable API access on guest user profile
- [ ] Set restrictive Login IP Ranges for guest user profile
- [ ] Review all sharing rules that include guest user groups
---
13. AppExchange Security Review Checklist
CRUD/FLS Enforcement
- [ ] All SOQL queries use
WITH USER_MODEor manual FLS checks - [ ] All DML operations use
AccessLevel.USER_MODEorSecurity.stripInaccessible() - [ ] Schema.Describe checks before dynamic SOQL/DML
Sharing Model
- [ ] All Apex classes use
with sharingby default - [ ] Any
without sharingclass has documented justification - [ ]
inherited sharingused for utility classes
Injection Prevention
- [ ] No direct string concatenation in SOQL/SOSL queries
- [ ] Bind variables used wherever possible
- [ ]
String.escapeSingleQuotes()used for unavoidable dynamic queries - [ ]
URLFOR()orEncodingUtil.urlEncode()for URL construction
XSS Prevention
- [ ] No
escape="false"in Visualforce unless HTML is fully sanitized - [ ]
JSENCODE()used for JavaScript string contexts - [ ]
HTMLENCODE()used for HTML contexts - [ ]
URLENCODE()used for URL parameter contexts - [ ] LWC: No
lwc:dom="manual"with unsanitized content, noinnerHTML
CSRF Protection
- [ ] All state-changing operations use
apex:form/apex:commandButton - [ ] No state changes via GET requests
Hardcoded Credentials
- [ ] No API keys, passwords, or tokens in source code
- [ ] Named Credentials used for all external callouts
- [ ] Custom Settings or Custom Metadata for configurable values
- [ ] No credentials in debug logs
Debug Logging
- [ ] No
System.debug()of sensitive data (PII, credentials, tokens) - [ ] Debug mode disabled in production
- [ ] No verbose logging that exposes internal logic
Open Redirects
- [ ] No user-controlled redirect URLs without validation
- [ ] Allowlist of valid redirect domains
- [ ]
PageReferenceused instead of string URLs where possible
SSL/TLS
- [ ] All external callouts use HTTPS
- [ ] No SSL certificate validation bypasses
- [ ] TLS 1.2+ enforced
Sensitive Data Exposure
- [ ] No PII in URL parameters
- [ ] Proper field-level encryption for sensitive data
- [ ] No sensitive data in client-side JavaScript/LWC
Insecure Deserialization
- [ ] No
JSON.deserialize()with user-supplied type names - [ ] Type-safe deserialization with known classes
- [ ] No
Type.forName()with user input
Additional Checks
- [ ] No dynamic Apex class instantiation from user input
- [ ] Custom Permission checks for sensitive features
- [ ] Remote Site Settings properly scoped (not overly broad)
- [ ] API version is current (not deprecated)
- [ ] Error messages don't expose stack traces or internal details to users
- [ ] Governor limit awareness and bulkification
#!/bin/bash
# Quick security scan for common Salesforce Apex vulnerabilities
#
# Usage: ./scripts/security-scan.sh [options] [source-dir]
#
# Options:
# --help Show this help message
# --format TEXT Output format: text (default) or json
#
# Exit codes:
# 0 — No issues found
# 1 — Issues found
# 2 — Invalid arguments
set -euo pipefail
FORMAT="text"
SOURCE_DIR=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
sed -n '2,11p' "$0" | sed 's/^# \?//'
exit 0
;;
--format)
FORMAT="${2:-text}"
shift 2
;;
-*)
echo "Unknown option: $1" >&2
echo "Run with --help for usage." >&2
exit 2
;;
*)
SOURCE_DIR="$1"
shift
;;
esac
done
SOURCE_DIR="${SOURCE_DIR:-force-app/main/default}"
if [ ! -d "$SOURCE_DIR" ]; then
echo "Error: Directory not found: $SOURCE_DIR" >&2
exit 2
fi
# Collect findings
declare -a FINDINGS=()
add_finding() {
local severity="$1" category="$2" file="$3" line="$4" message="$5"
FINDINGS+=("${severity}|${category}|${file}|${line}|${message}")
}
echo "Scanning: $SOURCE_DIR" >&2
# Check for classes without 'with sharing'
while IFS= read -r file; do
if ! grep -q "with sharing\|without sharing\|inherited sharing" "$file" 2>/dev/null; then
add_finding "HIGH" "missing-sharing" "$file" "1" "Class without sharing declaration"
fi
done < <(grep -rln "public class\|public virtual class\|public abstract class\|global class" "$SOURCE_DIR" --include="*.cls" 2>/dev/null || true)
# Check for SOQL without USER_MODE
while IFS=: read -r file line content; do
add_finding "HIGH" "missing-user-mode" "$file" "$line" "SOQL query without WITH USER_MODE"
done < <(grep -rn "\[SELECT" "$SOURCE_DIR" --include="*.cls" 2>/dev/null | grep -v "USER_MODE" | grep -v "@IsTest\|Test" | head -50 || true)
# Check for string concatenation in SOQL
while IFS=: read -r file line content; do
add_finding "CRITICAL" "soql-injection" "$file" "$line" "Potential SOQL injection via string concatenation"
done < <(grep -rn "'SELECT.*'" "$SOURCE_DIR" --include="*.cls" 2>/dev/null | grep "+" | head -50 || true)
# Check for DML without stripInaccessible
while IFS=: read -r file line content; do
add_finding "CRITICAL" "missing-crud-fls" "$file" "$line" "DML without CRUD/FLS check"
done < <(grep -rn "^\s*insert \|^\s*update \|^\s*delete \|^\s*upsert " "$SOURCE_DIR" --include="*.cls" 2>/dev/null | grep -v "stripInaccessible\|Database\.\|Test\|@IsTest" | head -50 || true)
# Check for debug statements with sensitive fields
while IFS=: read -r file line content; do
add_finding "MEDIUM" "pii-in-debug" "$file" "$line" "Debug statement may expose sensitive data"
done < <(grep -rn "System.debug.*\(.*Password\|SSN\|Secret\|Token\|CardNumber\|CreditCard\)" "$SOURCE_DIR" --include="*.cls" 2>/dev/null | head -20 || true)
# Check for hardcoded endpoints
while IFS=: read -r file line content; do
add_finding "HIGH" "hardcoded-endpoint" "$file" "$line" "Hardcoded endpoint — use Named Credentials"
done < <(grep -rn "setEndpoint.*https\?://" "$SOURCE_DIR" --include="*.cls" 2>/dev/null | grep -v "callout:" | head -20 || true)
# Count by severity
CRITICAL=0 HIGH=0 MEDIUM=0
for f in "${FINDINGS[@]+"${FINDINGS[@]}"}"; do
case "${f%%|*}" in
CRITICAL) CRITICAL=$((CRITICAL + 1)) ;;
HIGH) HIGH=$((HIGH + 1)) ;;
MEDIUM) MEDIUM=$((MEDIUM + 1)) ;;
esac
done
TOTAL=${#FINDINGS[@]}
# Output
if [ "$FORMAT" = "json" ]; then
echo "{"
echo " \"source\": \"$SOURCE_DIR\","
echo " \"total\": $TOTAL,"
echo " \"critical\": $CRITICAL,"
echo " \"high\": $HIGH,"
echo " \"medium\": $MEDIUM,"
echo " \"pass\": $([ $CRITICAL -eq 0 ] && [ $HIGH -eq 0 ] && echo "true" || echo "false"),"
echo " \"findings\": ["
first=true
for f in "${FINDINGS[@]+"${FINDINGS[@]}"}"; do
IFS='|' read -r severity category file line message <<< "$f"
[ "$first" = true ] && first=false || echo ","
printf ' {"severity": "%s", "category": "%s", "file": "%s", "line": "%s", "message": "%s"}' \
"$severity" "$category" "$file" "$line" "$message"
done
echo ""
echo " ]"
echo "}"
else
echo ""
echo "Security Scan Results: $SOURCE_DIR"
echo "================================================"
for f in "${FINDINGS[@]+"${FINDINGS[@]}"}"; do
IFS='|' read -r severity category file line message <<< "$f"
echo "[$severity] $file:$line — $message"
done
echo ""
echo "================================================"
echo "Critical: $CRITICAL | High: $HIGH | Medium: $MEDIUM | Total: $TOTAL"
if [ $CRITICAL -eq 0 ] && [ $HIGH -eq 0 ]; then
echo "Result: PASS (AppExchange review ready)"
else
echo "Result: FAIL (fix critical/high issues before review)"
fi
fi
[ $TOTAL -gt 0 ] && exit 1 || exit 0
Related skills
FAQ
Is Sf Security safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.