Skip to main content

AppExchange Security Review readiness checklist

Catch the CRUD/FLS gaps that fail AppExchange Security Review submissions, offline, before you spend on a submission.

Salesforce does not publish a first-pass failure rate. The figure partners cite most often is about half, and it is an industry estimate rather than an official number. A paid listing pays a fee per submission attempt, and a failed round costs two to three weeks of reviewer turnaround. What is not in dispute is the top cause: object and field permission enforcement (CRUD/FLS).

The cheapest fix is to walk the checklist on your own laptop first. vulkro-sf runs the same classes of check the reviewer applies, entirely offline: your package source never leaves the machine.

Why CRUD/FLS is the top failure cause

Below API version 67, Apex runs in system context by default. Unless you explicitly enforce the running user's object and field permissions before a DML or SOQL operation, your package will happily read and write data the user should never be able to touch.

At API version 67 (Summer '26) Salesforce changed the default. SOQL, SOSL, DML and Database methods run in user mode, and a class with no sharing declaration defaults to with sharing. The same release removed WITH SECURITY_ENFORCED, which no longer compiles. Most packages in review still carry classes on an older API version, so both worlds are usually in scope for the same submission.

Reviewers test enforcement aggressively because it is the most common real-world data exposure in managed packages.

"Enforce" means one of these, on every DML and SOQL path, ranked by what to reach for first:

  1. WITH USER_MODE on the query and AccessLevel.USER_MODE on the DML. This is the current answer and the one to write in new code.
  2. Security.stripInaccessible(...) on the records, where the query has to stay broad and you filter after the fact.
  3. Explicit Schema.sObjectType.X.isAccessible() / isCreateable() / isUpdateable() / isDeletable() checks, for the cases the platform modes do not cover.
  4. A vetted access-check helper class, applied on every path rather than on some of them.

WITH SECURITY_ENFORCED is the legacy clause. It is removed at API version 67 and will not compile there, so treat it as something to migrate off rather than something to add.

Partial enforcement (checking isAccessible but not isUpdateable, or enforcing on one method but not the helper it delegates to) is the trap that fails experienced teams. A pattern-matching checker cannot see the enforcement that happens one method call away. vulkro-sf builds the intra-class and cross-class call graph and follows the delegation, so a real gap surfaces and a false alarm does not.

The readiness checklist

Walk these before you submit. vulkro-sf appexchange-report renders the same list as an HTML report grouped section by section, pinned to the checklist version on the day you run it.

1. Object and field permissions (CRUD/FLS)

  • Every SOQL query enforces FLS (WITH USER_MODE, stripInaccessible, or explicit checks).
  • Every DML operation enforces CRUD for the running user (AccessLevel.USER_MODE or explicit checks).
  • No WITH SECURITY_ENFORCED clauses left in any class you are moving to API version 67 or later.
  • Enforcement holds across method and class boundaries, not just in the entry method.
  • @AuraEnabled and @RestResource methods enforce before reaching any DML.

2. Sharing model

  • No without sharing on classes that handle record data without an explicit ownership check.
  • inherited sharing used where the caller's context should decide.
  • Every class states its sharing intent explicitly, so the behaviour does not change when the API version moves to 67.

3. Injection

  • No dynamic SOQL built by string-concatenating request input (use bind variables).
  • No SOQL injection across method boundaries (tainted input passed into a query-building helper).
  • No Apex or JavaScript injection via dynamic Type.forName, eval, or unescaped merge fields.

4. External integrations and secrets

  • No hardcoded passwords or API tokens in Apex literals, named credentials, or custom settings.
  • Named credentials have IP restrictions and use HTTPS endpoints.
  • OAuth scopes on connected apps are no broader than the integration needs (no Full where a narrower scope works).

5. Lightning (LWC + Aura)

  • No lwc:dom="manual" paired with innerHTML on untrusted data.
  • No secrets written to localStorage or sessionStorage.
  • @wire returns are treated as untrusted in the DOM.

6. Visualforce

  • No escape="false" on a reflected merge field without a JSENCODE / $Resource / $Label wrap.
  • No <apex:includeScript> with a dynamic URL.

7. Flow

  • No runInMode = SystemModeWithoutSharing where user context is required.
  • No hardcoded org IDs in <stringValue> elements.
  • No system-context DML that bypasses the running user's permissions.

8. Profiles and permission sets

  • No View All Data, Modify All Data, Customize Application, or Author Apex granted to non-admin profiles the package ships.

9. Connected apps

  • OAuth scope is the minimum the integration requires.

10. Insecure deserialization and mass assignment

  • No JSON.deserialize of caller-controlled input straight into an SObject that is later written via DML.

Run it before you submit

The scan and the readiness checklist on screen are Free, with no account. vulkro-sf appexchange-report, the report you hand to the Security Review team, requires Pro. The 14-day trial of the full product covers a submission cycle, and licenses are issued directly by our team at license@vulkro.com; prices are on the pricing page.

# Render the reviewer-grouped readiness report, offline:
vulkro-sf appexchange-report force-app -o readiness.html
  1. Open readiness.html and walk all 10 sections.
  2. Fix what is flagged. CRUD/FLS first: it is the most common fail.
  3. Re-run until the report reads READY.
  4. Only then submit to Salesforce.
  5. If Salesforce flags something new, re-run with the reviewer's notes to find the same pattern elsewhere in the package.

The whole loop runs on your laptop. Air-gap it with VULKRO_OFFLINE=1 to enforce zero network at the process boundary.

Ready to walk the list?

Run vulkro-sf locally before you spend on a submission. The methodology maps every detector to its checklist section, and licenses are issued directly by our team at license@vulkro.com.

See Vulkro for Salesforce


See also: AppExchange readiness docs, Vulkro for Salesforce, methodology.