.cursor/skills/sf-security/SKILL.md
Use when implementing Salesforce Apex security — CRUD/FLS enforcement, sharing keywords, SOQL injection prevention, AppExchange review prep. Do NOT use for general Apex or LWC patterns.
npx skillsauth add jiten-singh-shahi/salesforce-claude-code sf-securityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Salesforce has a layered security model. Each layer must be respected in Apex code, SOQL queries, and UI components.
@../_reference/SECURITY_PATTERNS.md @../_reference/SHARING_MODEL.md
with sharing or using unexplained without sharingThe modern standard for CRUD + FLS enforcement. This replaces explicit CRUD checks and Security.stripInaccessible for most use cases.
// SOQL enforces both object CRUD and FLS for the running user
List<Account> accounts = [SELECT Id, Name FROM Account WITH USER_MODE];
// For DML, use Database methods with AccessLevel
Database.insert(records, false, AccessLevel.USER_MODE);
Database.update(records, false, AccessLevel.USER_MODE);
Database.delete(records, false, AccessLevel.USER_MODE);
| Context | Check CRUD? | |---|---| | Apex called from LWC/Aura/VF page | Yes | | Service layer called by user-facing code | Yes | | Internal utility called by system batch | Usually no — runs in system context | | Apex REST/SOAP API endpoint | Yes | | Scheduled/Batch internal processing | Usually no — document justification |
stripInaccessible() silently removes fields the user cannot access. The returned records look normal but stripped fields are null. Check getRemovedFields() before passing records downstream.
public List<Account> getAccountsForDisplay() {
List<Account> accounts = [
SELECT Id, Name, AnnualRevenue, SSN__c, Internal_Notes__c
FROM Account WHERE Type = 'Customer'
];
SObjectAccessDecision decision = Security.stripInaccessible(
AccessType.READABLE, accounts
);
Map<String, Set<String>> removed = decision.getRemovedFields();
if (removed.containsKey('Account')) {
System.debug(LoggingLevel.WARN, 'Stripped fields: ' + removed.get('Account'));
}
return decision.getRecords();
}
WITH USER_MODE throws a QueryException if the running user lacks field-level access to any field in the SELECT or WHERE clause. Use Security.stripInaccessible() when you need to silently remove inaccessible fields instead of throwing.
// Defensive approach: check accessibility first
List<String> selectFields = new List<String>{ 'Id', 'Name' };
if (Schema.SObjectType.Account.Fields.AnnualRevenue.getDescribe().isAccessible()) {
selectFields.add('AnnualRevenue');
}
// ... build and execute dynamic SOQL with validated fields
public with sharing class AccountsSelector {
public List<Account> selectAll() {
return [SELECT Id, Name FROM Account]; // Only returns records user can see
}
}
/**
* Batch processor for automated data enrichment.
* Uses without sharing because:
* 1. Runs as a system user (scheduled job)
* 2. Needs to process ALL accounts regardless of ownership
* 3. Sharing is irrelevant for automated system processing
*/
public without sharing class AccountEnrichmentBatch
implements Database.Batchable<SObject> {
// ...
}
// Adopts the sharing mode of the calling class
public inherited sharing class QueryHelper {
public List<Account> getAccountsForCaller() {
return [SELECT Id, Name FROM Account];
// If called by "with sharing" class: enforces sharing
// If called by "without sharing" class: bypasses sharing
// If top-level: defaults to "with sharing"
}
}
User-facing (LWC, VF, Aura, REST API)? → with sharing
Utility that respects caller's context? → inherited sharing
Scheduled/batch as system user? → without sharing (documented)
Permission elevation utility? → without sharing (narrow scope, documented)
Unsure? → with sharing
Sharing context does NOT propagate to called classes. A
with sharingclass calling awithout sharingclass runs the called method without sharing.
public List<Account> searchAccounts(String nameFilter) {
return [SELECT Id, Name FROM Account WHERE Name = :nameFilter WITH USER_MODE];
}
public List<Account> searchAccounts(String nameFilter) {
Map<String, Object> binds = new Map<String, Object>{
'nameFilter' => nameFilter
};
return Database.queryWithBinds(
'SELECT Id, Name FROM Account WHERE Name = :nameFilter WITH USER_MODE',
binds, AccessLevel.USER_MODE
);
}
public class SafeDynamicQueryBuilder {
private static final Set<String> ALLOWED_SORT_FIELDS = new Set<String>{
'Name', 'CreatedDate', 'AnnualRevenue', 'Type'
};
private static final Set<String> ALLOWED_DIRECTIONS = new Set<String>{
'ASC', 'DESC'
};
public List<Account> getAccountsSorted(String sortField, String sortDirection) {
if (!ALLOWED_SORT_FIELDS.contains(sortField)) {
sortField = 'Name';
}
if (!ALLOWED_DIRECTIONS.contains(sortDirection?.toUpperCase())) {
sortDirection = 'ASC';
}
String query = 'SELECT Id, Name, AnnualRevenue, Type '
+ 'FROM Account WITH USER_MODE '
+ 'ORDER BY ' + sortField + ' ' + sortDirection
+ ' LIMIT 200';
return Database.query(query);
}
}
<!-- HTML body → HTMLENCODE -->
<div>{!HTMLENCODE(accountDescription)}</div>
<!-- JS string → JSENCODE -->
<script>var accountName = '{!JSENCODE(account.Name)}';</script>
<!-- JS in HTML attribute → JSINHTMLENCODE -->
<div onclick="handleClick('{!JSINHTMLENCODE(account.Name)}')">Click</div>
<!-- URL parameter → URLENCODE -->
<a href="/mypage?name={!URLENCODE(account.Name)}">View</a>
<!-- Safe by default -->
<apex:outputField value="{!account.Name}" />
<apex:outputText value="{!account.Description}" escape="true" />
LWC templates auto-encode HTML. Avoid innerHTML:
// Safe — LWC encodes the value as text content
this.accountName = '<script>alert(1)</script>'; // Rendered as literal text
// Use textContent, not innerHTML
this.template.querySelector('.description').textContent = userProvidedContent;
// For rich text, use lightning-formatted-rich-text (sanitizes automatically)
public class ERPIntegrationService {
public HttpResponse callERPEndpoint(String path, String method, String body) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:ERP_System' + path);
req.setMethod(method);
req.setHeader('Content-Type', 'application/json');
if (String.isNotBlank(body)) {
req.setBody(body);
}
return new Http().send(req);
}
}
// No SOQL limits, deployable config
Map<String, Service_Config__mdt> configs = new Map<String, Service_Config__mdt>();
for (Service_Config__mdt config : Service_Config__mdt.getAll().values()) {
configs.put(config.DeveloperName, config);
}
| Encryption Type | Query Support | Use Case | |---|---|---| | Deterministic | Exact match only in WHERE | SSN lookup, email search | | Probabilistic | No SOQL filtering | Health data, financial records |
Encrypted fields cannot be used in ORDER BY, GROUP BY, or LIKE queries (probabilistic). Code must handle null returns when user lacks decryption permission.
Real-time threat detection: block large data exports, enforce MFA for sensitive operations, block restricted locations.
| Failure | Fix |
|---|---|
| CRUD not checked before DML | Use AccessLevel.USER_MODE |
| FLS not checked before field access | Use WITH USER_MODE or stripInaccessible |
| Dynamic SOQL with string concatenation | Use bind variables or queryWithBinds() |
| without sharing on user-facing class | Switch to with sharing |
| Hardcoded credentials | Use Named/External Credentials |
| Sensitive data in debug logs | Remove System.debug of PII |
| Unrestricted SOQL rows | Add WHERE + LIMIT |
| innerHTML with user input | Use textContent or sanitized components |
Security guidance is distributed across multiple skills. Use this index to find the right skill:
| Skill | Focus | Type |
|---|---|---|
| sf-security (this skill) | CRUD/FLS, sharing, injection, Named Credentials, AppExchange prep | Implementation guide |
| sf-security-constraints | Hard never/always rules for security compliance | Constraint (auto-loaded) |
| sf-apex-constraints | Apex-specific security: with sharing, bind variables, without sharing justification | Constraint (auto-loaded) |
| sf-soql-constraints | SOQL injection prevention, selective queries, bind variables | Constraint (auto-loaded) |
| sf-lwc-constraints | LWC XSS prevention, lwc:dom="manual" restrictions, CSP | Constraint (auto-loaded) |
sf-review-agent — For interactive, in-depth guidancesf-security-constraints — Hard rules for security compliancedevelopment
Update Salesforce platform reference docs with latest release features and deprecation announcements. Use when SessionStart hook warns docs are outdated or a new Salesforce release has shipped. Do NOT use for Apex or LWC development.
development
Use when syncing documentation after Salesforce Apex code changes. Update README, API docs, and deploy metadata references to match the current org codebase.
development
Use when managing context during long Salesforce Apex development sessions. Suggests manual compaction at logical intervals to preserve deploy and org context across phases.
tools
Visualforce development — pages, controllers, extensions, ViewState, JS Remoting, LWC migration. Use when maintaining VF pages, building PDFs, or planning VF-to-LWC migration. Do NOT use for LWC, Aura, or Flow.