Skip to main content

AppExchange Security Review readiness checklist

Catch the CRUD/FLS gaps that fail about half of all AppExchange Security Reviews, offline, before you spend on a submission.

Roughly half of first-time AppExchange Security Review submissions fail. Each attempt carries a per-submission fee, and a failed review round-trips through two to three weeks of vendor feedback per revision. The single most common reason a package fails is 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: nothing about your unreleased managed package leaves your machine.

Why CRUD/FLS is the number-one failure cause

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. Reviewers test for this 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:

  • Schema.sObjectType.X.isAccessible() / isCreateable() / isUpdateable() / isDeletable() checks before the operation
  • WITH SECURITY_ENFORCED on the SOQL query
  • Security.stripInaccessible(...) on the records
  • as user mode on the operation (Database.queryWithBinds, user-mode DML)
  • A vetted helper such as fflib_SecurityUtils or CanTheUser

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 linter 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 SECURITY_ENFORCED, stripInaccessible, as user, or explicit checks).
  • Every DML operation enforces CRUD for the running user.
  • 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.

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 / 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 / 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

# 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 pay the submission fee and 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 AppExchange Submission Ready Pack bundles 90 days of Pro for one submission cycle, and the methodology maps every detector to its checklist section.

See Vulkro for Salesforce


See also: AppExchange submission prep for ISVs, Vulkro for Salesforce vs CodeScan, Vulkro for Salesforce vs DigitSec, AppExchange readiness docs.