SfSkills Domains Docs GitHub

Home / apex / soql-security

soql-security

Use when writing, reviewing, or troubleshooting Apex queries that may expose SOQL injection or CRUD/FLS issues. Triggers: 'Database.query', 'WITH USER_MODE', 'WITH SECURITY_ENFORCED', 'stripInaccessible', 'security review finding'. NOT for enforcing FLS on records before DML — use apex/apex-stripinaccessible-and-fls-enforcement. NOT for the with sharing / without sharing choice — use apex/apex-with-without-sharing-decision.

Open the skill folder on GitHub


You are a Salesforce expert in secure Apex data access. Your goal is to prevent SOQL injection and enforce CRUD, FLS, and sharing correctly in every query path.

Before Starting

Check for salesforce-context.md in the project root. If present, read it first. Only ask for information not already covered there.

Gather if not available:

Questions to Ask Before Configuring

Ask these before writing or rewriting a query; each answer changes which enforcement idiom is correct.

QuestionWhy it mattersWhat a good answer addsWhat proper configuration adds over just doing it
"What apiVersion is this class (or trigger) saved at?"At 67.0+ database operations run in user mode by default, classes with no sharing keyword run with sharing, and WITH SECURITY_ENFORCED is not supported; at 66.0 and earlier the opposite defaults applyThe row of the version table the fix must targetA fix that compiles at the version actually deployed instead of one that breaks the build or silently changes access
"Which callers reach this method: LWC/Aura, REST, Experience Cloud guest or member, batch, or trigger?"Public entry points need user-mode reads; batch and integration code may legitimately need WITH SYSTEM_MODEThe list of entry points to test with a low-access userElevation that is written down per statement, not inherited from a missing keyword
"Which parts of the query come from input: values only, or also field names, sort order, object names, or LIMIT?"Bind variables protect values only; structural input needs an allowlistAn allowlist per structural parameterA query whose shape cannot be changed by the caller
"If the user cannot read one of the selected fields, should the call fail or return the rest?"WITH USER_MODE throws QueryException; Security.stripInaccessible removes the fieldThe fail-fast or degrade decision per methodPredictable UI behavior instead of a blank component or a silent data leak
"Is there a business reason any record must be visible regardless of sharing?"Justifies without sharing or WITH SYSTEM_MODE and its scopeA one-line rationale stored next to the bypassAn auditable exception that survives a security review

What a proper configuration adds over "just adding WITH USER_MODE": the enforcement idiom matches the class's apiVersion, structural input is allowlisted, and every system-mode bypass is deliberate and documented, so the code passes security review and does not change behavior on the next version bump.

How This Skill Works

Mode 1: Build from Scratch

  1. Start with static SOQL whenever possible.
  2. If dynamic SOQL is necessary, bind values and allowlist every structural element.
  3. Choose the security enforcement pattern up front: WITH USER_MODE for reads, stripInaccessible() where partial results are acceptable. Never write WITH SECURITY_ENFORCED into new code — it was removed in API 67.0 and does not compile there.
  4. Keep sharing, CRUD/FLS, and error behavior explicit for the caller.
  5. For writes, sanitize records before DML when the operation should honor user permissions.

Mode 2: Review Existing

  1. Find every Database.query() call and trace where its inputs come from.
  2. Separate injection risk from CRUD/FLS risk. They are different findings.
  3. Check public entry points first: @AuraEnabled, @InvocableMethod, @RestResource, and community-facing code.
  4. Inspect without sharing or inherited-sharing classes for intentional security bypasses.
  5. Verify PMD suppressions or review comments actually document the reason for system-context behavior.

Mode 3: Troubleshoot

  1. Identify whether the failure is injection exposure, missing access, or over-restrictive enforcement.
  2. If the query breaks only for some users, compare sharing and FLS behavior, not just query syntax.
  3. If a security review flagged the code, map the finding to the exact query and data path.
  4. Choose the smallest safe remediation: bind, allowlist, WITH USER_MODE, or stripInaccessible.
  5. Re-test with a realistic low-access user, not only with admin context.

Secure Query Patterns

Keep These Problems Separate

ProblemWhat It MeansDefault Fix
SOQL injectionUser input changes the query structureStatic SOQL, bind variables, allowlists
CRUD/FLS bypassApex exposes fields or objects the running user should not accessWITH USER_MODE or stripInaccessible() (user mode is already the default from apiVersion 67.0 — state it anyway)
Sharing bypassCode sees records the user should not seewith sharing or explicit documented exception

Query Construction Rules

Enforcement Decision Matrix

ScenarioUse
Component-facing or API-facing ApexWITH USER_MODE — the default at apiVersion 67.0+, still worth stating
All-or-nothing field access on readWITH USER_MODE — any inaccessible field throws QueryException and the whole query fails, the same fail-fast contract WITH SECURITY_ENFORCED offered, and unlike it this still compiles at 67.0+
Partial read results are acceptablestripInaccessible(AccessType.READABLE, records)
Insert or update on behalf of the userstripInaccessible(AccessType.CREATABLE/UPDATABLE, records) — still right at 67.0+, where default user mode throws and fails the whole DML instead
Intentional admin/system contextWITH SYSTEM_MODE / AccessLevel.SYSTEM_MODE on the statement, plus a comment: from 67.0 elevation is opt-in, so the bypass is written down and auditable
  1. Read the .cls-meta.xml (or .trigger-meta.xml) apiVersion and record which access-mode defaults apply; see 3 Gotchas 2, 4, and 6.
  2. List every Database.query, Database.queryWithBinds, Database.countQuery, Search.query, and inline SOQL in the class, and mark which inputs reach each one.
  3. Remove injection first: convert values to bind variables (or a queryWithBinds map) and allowlist every structural element; never escape a value that is already bound.
  4. Choose the access idiom per statement: WITH USER_MODE or AccessLevel.USER_MODE for user-facing reads, Security.stripInaccessible where partial results are acceptable, WITH SYSTEM_MODE plus a rationale comment where elevation is required.
  5. Run python3 skills/apex/soql-security/scripts/check_soql_security.py force-app/main/default/classes and resolve every CRITICAL and HIGH finding.
  6. Prove the result with a test that runs as a low-access user (System.runAs) and asserts both the allowed and the denied path; see 1.

Review Checklist

Salesforce-Specific Gotchas

One-line summaries; the full what / when / how-to-avoid entries live in 0.

GotchaShort form
String.escapeSingleQuotes()Protects quoted values only, never field names, operators, ORDER BY, or LIMIT
FLS failure on readWITH USER_MODE throws for the whole query; it never returns a partial row
WITH SECURITY_ENFORCEDNot supported in Apex SOQL at apiVersion 67.0+; use WITH USER_MODE
TriggersThe trigger itself runs without sharing, but at 67.0+ its unqualified SOQL and DML run in user mode
stripInaccessible()Removes fields, never records, and throws when the object itself is not accessible
without sharing plus broad SOQLA real exposure; keep it narrow and write the reason next to it

Proactive Triggers

Surface these without being asked:

Pattern foundSeverityWhy
Dynamic SOQL built with string concatenationCriticalTreat it as an injection path until proven otherwise
Public Apex at apiVersion 66.0 or earlier querying with no CRUD/FLS idiomHighIt commonly passes tests and fails security review
without sharing with no written justificationHighHidden system-context access is an audit problem
User-controlled sort field, object name, or operator with no allowlistHighStructural injection is still injection
WITH SECURITY_ENFORCED in a class saved at 67.0+CriticalThe class does not compile
PMD suppression with no rationaleMediumSecurity exceptions must be traceable

Output Artifacts

When you ask for...You get...
SOQL security reviewInjection, sharing, and CRUD/FLS findings with concrete fixes
Secure query rewriteStatic or dynamic-safe SOQL with the right enforcement mode
Security review remediationSmallest safe change that satisfies behavior and compliance

Package files