SfSkills Domains Docs GitHub

Home / apex / apex-security-patterns

apex-security-patterns

Use when designing, reviewing, or debugging Apex execution context, sharing keywords, CRUD/FLS enforcement, system-vs-user mode behavior, or secure write patterns. Triggers: 'with sharing', 'inherited sharing', 'AuraEnabled security', 'what execution context does this class run in', 'system mode vs user mode'. NOT for remediating a CRUD/FLS finding — use apex/apex-stripinaccessible-and-fls-enforcement for stripInaccessible, AccessType, SObjectAccessDecision and WITH USER_MODE mechanics. NOT for SOQL injection review alone — use apex/soql-security.

Open the skill folder on GitHub


Use this skill when Apex security needs to be explicit rather than assumed. The purpose is to choose the right sharing model, enforce CRUD and FLS deliberately on reads and writes, and prevent user-facing entry points from silently operating in broader system context than intended.

Before Starting

Questions to Ask Before Configuring

Ask these before writing or reviewing an Apex security boundary. Each one traces to a behaviour in references/gotchas.md that a keyword choice alone cannot recover from afterwards.

AskWhy it mattersWhat a good answer adds
"Is the risk row visibility, object/field permissions, or both?"with sharing only addresses rows; CRUD and FLS need a separate enforcement path (with sharing Is Not A Full Security Model)A split design: sharing keyword for visibility, WITH USER_MODE / stripInaccessible / describe checks for CRUD/FLS
"Which class in the call chain, if any, is allowed to widen record access?"A safe controller still leaks when a downstream helper is without sharing (without sharing Deep In The Call Stack Still Matters)A named elevation boundary with a documented reason, kept narrow and purpose-built
"Does this path only read, or does it also write — and how is each half enforced?"Securing SOQL does not sanitize DML (Secure Reads And Secure Writes Need Different Controls)An explicit write pattern (stripInaccessible, AccessLevel.USER_MODE, or describe checks) beside the read pattern
"What is the class or trigger's .cls-meta.xml / .trigger-meta.xml apiVersion?"At 67.0+ undeclared classes default to with sharing and SOQL/DML default to user mode; below 67.0 both defaults differ (The Default Sharing Mode Changed In API 67.0; The Default Access Mode Flipped In API 67.0)The version row before any "is this secure?" claim, plus an inventory of operations that still need WITH SYSTEM_MODE / AccessLevel.SYSTEM_MODE with a // reason:
"For a trigger, are you judging the sharing context or the access mode of each operation?"Triggers are always implicitly without sharing, but body operations follow the version-gated access-mode default (A Trigger's without sharing Context Says Nothing About Its Access Mode)Per-operation elevation only where needed, with accurate comments — not a blanket "triggers run in system mode" claim
"Will a comment mention WITH SECURITY_ENFORCED or system mode without the keyword appearing in code?"Scanners that read comments as code raise false CRITICALs; stale system-mode comments mislead reviewers (A Checker That Reads Comments As Code Flags Safe Classes)Comments that either omit removed clauses, negate old defaults, or sit next to real SYSTEM_MODE / insertAsSystem elevation

What a proper configuration adds over just picking keywords: every public class declares its sharing intent, every elevation carries a reason tied to the statement that elevates, CRUD/FLS is enforced on the write path as deliberately as on the read path, and comments match the apiVersion the metadata actually pins.


Core Concepts

Sharing Keywords Set The Record-Access Boundary

with sharing, without sharing, and inherited sharing are design choices, not style preferences. with sharing enforces row-level sharing rules for the class. without sharing explicitly widens record visibility and must be justified — for record-visibility purposes it lets the code see records as if the running user had Modify All Data. inherited sharing makes the class adopt the caller’s sharing model and is often the safest default for reusable service layers that should not surprise reviewers.

Do not lean on the default. In API version 67.0 and later, a class with no explicit sharing declaration runs in with sharing mode. That is a safer default than older versions, but it also means a class that genuinely needs elevated visibility can silently start enforcing sharing after an API-version bump, and older code being uplifted may change behavior. Declare the intended mode explicitly rather than depending on the version default.

Sharing Mode Resolves By Definition, Not By Call Site

Two inheritance and call-chain rules trip up reviewers who assume sharing "flows down" like a normal variable:

Triggers Split The Two Axes — Sharing Is Fixed, Access Mode Is Not

Apex triggers cannot carry an explicit sharing keyword and always run implicitly in a without sharing context. That context is fixed at every API version — you cannot declare it away. What it does not fix is whether any given operation actually bypasses the running user's sharing rules, because the access mode is a separate axis and it overrides the context. Per the Apex Developer Guide, "database operations within trigger bodies, including SOQL queries, SOSL queries, DML statements, and Database methods, run in user mode unless system mode is explicitly specified. User mode overrides the trigger's without sharing context and effectively enforces a with sharing context in the trigger body."

That default is version-gated exactly like a class's, on the apiVersion in the trigger's own .trigger-meta.xml: user mode at 67.0 and later, system mode at 66.0 and earlier. So a bare SOQL in a 67.0 trigger body returns only the rows the running user can see — do not assume a trigger sees everything. Individual operations inside the body can still opt out with WITH SYSTEM_MODE, as system, or AccessLevel.SYSTEM_MODE; such an operation bypasses object- and field-level permissions and falls back to the trigger's own without sharing context for record visibility, so it genuinely does see every row. Review the two axes separately, and keep security-sensitive logic in a handler class where the sharing axis is yours to choose too.

The Default Access Mode Is Set By The Class, Not By The Org

with sharing affects row visibility; it does not by itself decide object permissions or field-level security. What decides those is the apiVersion in the class's .cls-meta.xml. At API 67.0 and later, SOQL, SOSL, DML, and Database methods run in user mode by default, so CRUD, FLS, and sharing are enforced with no keyword at all. At 66.0 and earlier the default is system mode and every bit of enforcement is opt-in. A Summer '26 org runs both kinds of class side by side, so never assert either behaviour flatly — read the apiVersion, then read the row that applies in Apex security idiom by API version. The version-independent point still holds: Security.stripInaccessible remains the tool for graceful degradation, because user mode fails the whole operation instead of trimming it.

User-Facing Entry Points Need Explicit Security

Aura-enabled controllers, REST resources, and other externally callable Apex can easily run with broader access than intended if the class declaration and data-access code are vague. Secure Apex guidance emphasizes making sharing intent explicit and enforcing access in the data path, not assuming the platform will infer the right boundary.

Secure Writes Need As Much Attention As Secure Reads

Teams often secure queries and then perform unsafe DML on fields the user should not edit. Security.stripInaccessible is a strong pattern for mutating records safely while preserving a clear list of removed fields for auditing or logging.

Common Patterns

inherited sharing Service Layer

When to use: Reusable services are called from multiple entry points and should respect the caller’s sharing model.

How it works: Declare the service inherited sharing, keep high-risk elevation isolated to narrow helper classes, and document every justified without sharing boundary.

Why not the alternative: Omitting a sharing keyword leaves intent ambiguous and makes reviews harder.

Read With User Context, Write With stripInaccessible

When to use: Code both queries and updates data on behalf of a user.

How it works: Use WITH USER_MODE or another explicit read-enforcement strategy for queries, then sanitize outbound records with Security.stripInaccessible before DML.

Allowlist Dynamic Access

When to use: The code allows a caller to choose fields, sort orders, or objects dynamically.

How it works: Validate object and field names against Schema describe metadata and allowlists before using them.

Decision Guidance

SituationRecommended ApproachReason
Reusable service should respect the caller’s sharing boundaryinherited sharingClear and least surprising behavior
User-facing code reads data for the current userExplicit user-context read pattern such as WITH USER_MODESharing alone is not enough
User-facing code updates recordsSecurity.stripInaccessible before DMLPrevents unauthorized field writes
Documented admin or maintenance process truly needs elevated accessNarrow without sharing helper with explicit justificationKeeps privilege elevation contained
Integration, batch, or platform-utility code on a 67.0+ class must see all rows and fieldsExplicit WITH SYSTEM_MODE / AccessLevel.SYSTEM_MODE plus a // reason: commentElevation is now the opt-in half; silence means user mode and silently fewer rows

Step-by-step instructions for an AI agent or practitioner activating this skill:

  1. Gather context — confirm the org edition, relevant objects, and current configuration state
  2. Review official sources — check the references in this skill's well-architected.md before making changes
  3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
  4. Validate — run the skill's checker script and verify against the Review Checklist below
  5. Document — record any deviations from standard patterns and update the template if needed

Review Checklist

Checker Rules

scripts/check_apex_security_patterns.py scans .cls / .trigger trees. Code-facing rules run on text with // comments, /* … */ comments, and single-quoted literals blanked (same length) so explanatory comments cannot raise CRITICAL/HIGH/MEDIUM.

SeverityRuleWhen it fires
CRITICALwith-security-enforced-removedWITH SECURITY_ENFORCED in code at apiVersion ≥ 67.0
MEDIUMwith-security-enforced-legacyWITH SECURITY_ENFORCED in code below 67.0
HIGH / MEDIUMundeclared-sharingpublic/global class with no sharing keyword (HIGH below 67.0, MEDIUM at 67.0+)
HIGHwithout-sharingwithout sharing present
HIGHentry-point-no-sharing@AuraEnabled / @InvocableMethod / @RestResource without sharing declaration
HIGHentry-point-no-read-enforcemententry point lacks obvious read enforcement
HIGHentry-point-no-write-enforcemententry point DML lacks obvious write enforcement
HIGHentry-point-system-modeentry point opts out to system mode
REVIEWtrigger-system-modetrigger-body operation opts out to system mode
REVIEWelevation-without-reasonWITH SYSTEM_MODE / AccessLevel.SYSTEM_MODE at ≥ 67.0 with no nearby // reason:
REVIEWstale-system-mode-claimcomment at ≥ 67.0 asserts system-mode default without negating the claim, and with no nearby explicit elevation (SYSTEM_MODE / insertAsSystem / as system)

Salesforce-Specific Gotchas

  1. with sharing does not enforce CRUD or FLS — it only addresses row visibility.
  2. Aura-enabled Apex can still expose too much data if the query or DML path is not explicitly secured — the class declaration alone is not enough.
  3. without sharing in the wrong layer silently widens access for everything below it — security reviews must trace the call chain, not just the top-level controller.
  4. Secure read patterns and secure write patterns are different — a class can query safely and still perform unsafe DML if writes are not sanitized.
  5. Relying on the sharing default is fragile — in API 67.0+ an undeclared class runs with sharing, so uplifting older code or bumping the API version can silently flip enforcement; declare the mode explicitly.
  6. Inner classes do not inherit the outer class's sharing — a with sharing outer class does not make its inner classes safe; each inner class needs its own declaration.
  7. A trigger's without sharing context does not decide its access mode — the context is fixed and undeclarable, but it only governs operations that run in system mode; database operations in the body follow the version-gated default (user mode at 67.0+, system mode at ≤66.0), and a user-mode operation overrides the without sharing context and enforces sharing for that operation.
  8. WITH SECURITY_ENFORCED stops compiling at API 67.0 — it was removed from SOQL SELECT in Apex; WITH USER_MODE is the replacement for both SOQL and SOSL.
  9. The user-mode default is gated on the class, not the org — a Summer '26 org still runs a class pinned to 58.0 in system mode, so read the apiVersion before calling code secure or insecure.

Output Artifacts

ArtifactDescription
Apex security reviewFindings on sharing intent, CRUD/FLS enforcement, and system-context risk
Security decision treeGuidance for with sharing, without sharing, inherited sharing, and data-access enforcement
Secure code patternRead/write pattern using explicit query enforcement and stripInaccessible

Package files