Skip to main content

AgentExchange (formerly AppExchange) Security Review readiness checklist

Catch the CRUD/FLS gaps that fail 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. On a paid solution Salesforce charges a $999 fee for the initial submission and for every later attempt, and a solution typically takes four to five weeks to get through review (Salesforce). The review is now called the AgentExchange Security Review (formerly AppExchange); the checklist is the same, and the cheapest place to pass it is during development. 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​

vulkro-sf appexchange-report requires Pro. The 14-day Pro trial covers a submission cycle. Start it at checkout in the customer portal; the plans 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.

Next steps​

Run Vulkro for Salesforce locally before you spend on a submission. The methodology maps every detector to its checklist section, and Pro is self-serve in the customer portal, starting with a 14-day trial.

See Vulkro for Salesforce


See also: AgentExchange (formerly AppExchange) readiness docs, Vulkro for Salesforce, methodology.