---
name: salesforce-security
description: >
  Designs and hardens security on the Salesforce Platform — enforcing sharing and field-level security in Apex with USER_MODE, stripInaccessible and `with sharing`, guest user and Experience Cloud site hardening, org posture through Health Check, MFA and session settings, Shield Platform Encryption and Event Monitoring, Trusted URLs and CSP for Lightning Web Security, named credentials over stored secrets, and the Code Analyzer v5 security engines in CI. Use this whenever the user writes SOQL or DML that must respect the running user's access, sees WITH SECURITY_ENFORCED in inherited code, exposes data to unauthenticated guest users, hardens an Experience Cloud site, configures MFA, session or IP restrictions, evaluates Shield or Event Monitoring, adds a third-party script or endpoint that needs a Trusted URL, or prepares an app for AppExchange listing. For Apex idioms use `apex-development`.
metadata:
  technology: Salesforce
  type: security
---

# Salesforce Security
> **Targets:** Salesforce Spring '26 · Code Analyzer v5 · Lightning Web Security · **Verified:** 2026-09 against https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_keywords_sharing.htm and https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/guide/

Salesforce gives you a strong permission model and then lets Apex run right past it. Apex executes in system mode by default: sharing rules, object permissions and field-level security are all ignored unless you say otherwise. So platform security is mostly a discipline of enforcing, in code, the access the admin already configured — and of noticing the places (guest users, integration users, Experience Cloud sites) where "the admin configured it" means something much more permissive than you assumed.

## 1. Decide first

| Question | Default | Change when |
|---|---|---|
| Class sharing declaration | `with sharing`, declared explicitly on **every** class | `without sharing` only with a comment justifying it. The org standard uses these two keywords only |
| Static SOQL access | `WITH USER_MODE` | System mode only for a deliberate, documented elevation |
| Dynamic SOQL | `Database.queryWithBinds(q, binds, AccessLevel.USER_MODE)` | Never string-concatenate user input into the query |
| DML access | `Database.insert(records, AccessLevel.USER_MODE)` | System mode only where the operation is genuinely privileged |
| `WITH SECURITY_ENFORCED` in existing code | Replace it with `WITH USER_MODE` | Never combine the two |
| System-mode query returned to a user | `Security.stripInaccessible` before returning | Not needed when the query already ran in user mode |
| Guest user access | Assume it is public internet; grant nothing by default | Every guest-accessible object is a decision to defend |
| Secrets for callouts | Named Credential (+ External Credential) | The org also sanctions `Credential__c` (encrypted field, FLS restricted to the integration user, kept out of change sets); never a custom setting, custom metadata, static resource or hard-coded string |
| Static analysis | Code Analyzer v5 in CI with the security engines enabled | `sf scanner run` (v4) was retired in August 2025 |

## 2. Enforce access in Apex

Apex runs in system mode. Three separate things must be enforced, and `with sharing` only covers the first:

- **Record access (sharing)** — `with sharing` on the class.
- **Object and field permissions (CRUD/FLS)** — `WITH USER_MODE` on the query, `AccessLevel.USER_MODE` on the DML.
- **What you hand back** — a system-mode query returning records to a user needs `Security.stripInaccessible`.

```apex
public with sharing class OrderService {
    public List<Order> recentOrders(Id accountId) {
        return [
            SELECT Id, Name, Amount, Status
            FROM Order
            WHERE AccountId = :accountId
            WITH USER_MODE
            ORDER BY CreatedDate DESC
            LIMIT 50
        ];
    }
}
```

`WITH USER_MODE` supersedes `WITH SECURITY_ENFORCED`: it covers polymorphic fields and gives better errors. When you find the old clause, **replace** it — never write both on one query.

The full enforcement matrix, including which construct covers what and where each one silently does not apply: `references/sharing-and-crud-fls.md`.

## 3. Guest users and Experience Cloud

The guest user is the unauthenticated internet, and it is where Salesforce data leaks happen.

- Start from zero: no object permissions, no record access, guest sharing rules only where genuinely required.
- Guest user record access is capped at read; anything a guest "creates" happens through Apex you control — so that Apex is a public API and must validate everything.
- An `@AuraEnabled` method reachable by a guest is an unauthenticated endpoint. Check `with sharing`, check CRUD/FLS, validate every input, and do not trust an id parameter to be one the caller may see.
- Review what the site's standard components expose, not just your custom ones.

## 4. Org posture

- **Health Check** gives a baseline score against Salesforce's standard: session settings, password policy, certificate and sharing settings. Run it, fix what it flags, re-run after every release.
- **MFA** is required; enforce it through the org's session settings rather than relying on users. Add IP ranges and session timeouts appropriate to the profile rather than one blanket policy.
- **Shield Platform Encryption** protects data at rest from platform-level exposure, with real functional costs — encrypted fields lose some filtering, sorting and indexing. Decide field by field, and confirm each one does not break a report or a SOQL filter the business depends on.
- **Event Monitoring** supplies the audit trail: logins, API calls, report exports, Apex executions. Without it, "did anyone export that object" has no answer.

Health Check items, Shield trade-offs, Event Monitoring event types and a guest-user hardening checklist: `references/org-hardening.md`.

## 5. Client-side and integrations

- **Lightning Web Security** is the runtime for components (default for orgs created since Winter '23). It isolates component JavaScript; code that reached into the DOM or global objects under Locker needs revisiting when migrating.
- Third-party scripts load from a **static resource**, not a CDN, and any external endpoint the page contacts needs a **Trusted URL** with the right CSP directives.
- Outbound callouts authenticate through a **Named Credential** with an External Credential. Secrets in custom metadata or custom settings are readable by anyone who can view setup, and they end up in change sets.
- Inbound integrations get their own integration user with a minimal permission set — not a system administrator licence, and not a departing employee's account.

## 6. Static analysis in CI

Code Analyzer v5 replaced the retired v4 (`sf scanner run`) in August 2025, and runs PMD, ESLint, RetireJS, the Flow scanner, CPD, regex rules and the Salesforce Graph Engine in one result set:

```bash
sf plugins install code-analyzer
sf code-analyzer run --workspace force-app --rule-selector Security --view detail
```

Enable the Graph Engine for CRUD/FLS analysis — it is the engine that finds access violations across call paths, which PMD alone cannot. Configure engines and severities in `code-analyzer.yml` and fail the build on the severities you have agreed to treat as blocking.

## Deliverable format

```
## Exposure — what data, which audiences (internal, community, guest, integration)
## Apex enforcement — class sharing, query and DML access level, per entry point
## Guest and community — object permissions, sharing, and every guest-reachable Apex entry point
## Org posture — Health Check findings, MFA, session and IP policy
## Data protection — Shield decisions per field, Event Monitoring events retained
## Integrations — named credentials, integration users and their permission sets
## Client-side — Trusted URLs, CSP, static resources, LWS migration notes
## CI — Code Analyzer configuration and the failing severities
## Open questions
```

## Checklist

- [ ] Every class declares `with sharing`, or `without sharing` with a stated reason — none omits the keyword.
- [ ] Every user-facing query uses `WITH USER_MODE`; DML uses `AccessLevel.USER_MODE`.
- [ ] No `WITH SECURITY_ENFORCED` remains, and it is never combined with `USER_MODE`.
- [ ] System-mode results returned to users pass through `Security.stripInaccessible`.
- [ ] Dynamic SOQL uses bind variables — no string concatenation of user input.
- [ ] Guest user profiles grant the minimum, and every guest-reachable `@AuraEnabled` method validates its inputs.
- [ ] Health Check has been run this release and its findings triaged.
- [ ] MFA, session timeout and IP policy are set per profile, not one blanket rule.
- [ ] Shield field choices were checked against the reports and filters that use them.
- [ ] Every callout secret is in a Named Credential, or in `Credential__c` with the field encrypted and FLS restricted; none in custom settings or metadata.
- [ ] Integration users have minimal permission sets.
- [ ] Third-party scripts are static resources, with Trusted URLs and CSP configured.
- [ ] Code Analyzer v5 runs in CI with the Graph Engine enabled and a failing severity agreed.

## Anti-patterns

| Anti-pattern | Why it hurts | Fix |
|---|---|---|
| `without sharing` with no comment | Silently returns records the user may not see | `with sharing`, or `without sharing` with a stated reason |
| `with sharing` treated as full enforcement | It covers record access only — fields and objects are still system mode | Add `WITH USER_MODE` and `AccessLevel.USER_MODE` |
| Adding `WITH SECURITY_ENFORCED` to new code | Superseded; weaker on polymorphic fields | `WITH USER_MODE` |
| Both clauses on one query | Confusing and redundant | Keep `USER_MODE` only |
| String-concatenated dynamic SOQL | SOQL injection | `Database.queryWithBinds` with `AccessLevel.USER_MODE` |
| Guest user granted object permissions "to make it work" | Publishes data to the internet | Grant nothing; expose through controlled Apex with validation |
| API key in custom metadata | Visible to anyone who can view setup; travels in change sets | Named Credential with External Credential, or `Credential__c` encrypted and FLS-restricted |
| Integration running as a system administrator | One compromised integration owns the org | Dedicated integration user with a minimal permission set |
| `sf scanner run` in the pipeline | Retired August 2025 | `sf code-analyzer run` with `code-analyzer.yml` |
| Third-party script from a CDN | Blocked by CSP, and unreviewed code in your page | Static resource plus a Trusted URL |

## Go deeper

- `references/sharing-and-crud-fls.md` — the enforcement matrix, what each construct does and does not cover, `stripInaccessible` patterns, and migrating `WITH SECURITY_ENFORCED`.
- `references/org-hardening.md` — Health Check items, MFA and session policy, Shield trade-offs per field type, Event Monitoring events, guest-user hardening, and the checks an AppExchange listing must pass.
- Sibling skills: `apex-development` for Apex idioms and bulkification; `lwc-development` for component patterns under LWS; `salesforce-integration` for authentication flows; `salesforce-org-conventions` for the org's naming and mandated patterns.
