AgentExchange Security Review: what fails, and how to pass the first time
A failed AgentExchange (formerly AppExchange) Security Review rarely fails on something exotic. It fails on a query that skipped a field-level check, a class that forgot its sharing keyword, or a Visualforce page that printed a URL parameter as raw HTML. The fixes are usually small. The cost of finding them in the review instead of before it is not.
Three facts about that cost, stated carefully. Salesforce publishes no first-pass failure rate; about half is the figure partners cite most often for first submissions, and it is an industry estimate, not an official one. 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). Salesforce has since renamed it the AgentExchange Security Review. The weeks are spent in the queue; the way to save them is to find the issues while you develop.
What the reviewers actually find
Salesforce has published its own list. In 2023 its developer blog ranked the top 20 reasons partners fail the review, in order of prevalence. CRUD and FLS enforcement is first, "by a significant margin" in the post's own words. Then come insecure software versions (most often an old JavaScript library), sharing violations, insecure storage of sensitive data, and TLS configuration. Stored and reflected cross-site scripting is eighth, and insecure endpoints appear further down.
Almost everything on that list is visible in your source before you submit. Here is what each one looks like in a real package, and the shape of the fix.
1. CRUD and field-level security
The review expects every query and every write to respect what the running user is allowed to see and change. Apex does not do that on its own below API 67.0: a plain SOQL query or DML statement runs in system mode and enforces nothing.
public with sharing class InvoiceController {
@AuraEnabled(cacheable=true)
public static List<Invoice__c> getInvoices(Id accountId) {
// Fails review: reads every field, whatever the user's FLS says.
return [
SELECT Id, Name, Amount__c, Bank_Account__c
FROM Invoice__c
WHERE Account__c = :accountId
];
}
}
The modern fix is user mode on the operation itself:
return [
SELECT Id, Name, Amount__c, Bank_Account__c
FROM Invoice__c
WHERE Account__c = :accountId
WITH USER_MODE
];
// and for writes
Database.insert(records, AccessLevel.USER_MODE);
Two things make this harder than it looks. Testing as a System Administrator hides every gap, because the admin can see everything. And from API 67.0 the defaults changed, so the same unannotated line can be a finding in one class and correct in another, depending on the API version it compiles at. Our CRUD and FLS guide walks through the version fork and the remediation shapes in order of preference.
2. Sharing
A class with no sharing keyword can bypass the org's sharing rules, so a user reads records their role should never reach. The review flags Apex classes without with sharing (or inherited sharing) in the header, and Flows set to run in system mode without sharing.
// Fails review: no declaration, and nothing says why.
public class CaseService { ... }
// Passes: the class respects the caller's sharing.
public with sharing class CaseService { ... }
Some classes genuinely need to run without sharing. That is allowed, but it has to be deliberate: keep the class small, put the reason in a comment at the top, and explain it in the false-positive document you submit. The quiet version, a without sharing helper called from a with sharing entry point, is the one that slips through a manual read.
3. Guest access
If your package ships an Experience Cloud component or a public site page, everything a guest user can call is something an anonymous visitor on the internet can call. An @AuraEnabled method that is reachable by the guest profile, runs without sharing and returns records by an id the caller supplies is not a Medium finding. It is a public read of your customer's data.
Before you submit, list every Apex method the guest user can reach and check three things on each one: it runs with sharing, it enforces field-level security, and it does not find a record by a value an outsider can guess.
4. Cross-site scripting in LWC, Aura and Visualforce
Visualforce escapes merge fields by default. The finding is almost always someone turning that off:
<!-- Fails review: a URL parameter rendered as raw HTML -->
<apex:outputText value="{!$CurrentPage.parameters.msg}" escape="false"/>
<!-- Passes: leave escaping on -->
<apex:outputText value="{!$CurrentPage.parameters.msg}"/>
Inside a <script> block, encode for JavaScript with JSENCODE, not for HTML. In Lightning, the framework protects you until you step around it: assigning to innerHTML, using lwc:dom="manual", or rendering rich text without validating it first. Salesforce's own list calls out exactly those three. Stored XSS needs the same care: a value saved today in a rich text field is rendered to another user tomorrow.
5. Secrets
A key in source code is a finding even inside a managed package, where the customer cannot read the code. The customer cannot rotate it either, and it can leak through a log or an error message. Salesforce's guidance is to keep partner-owned secrets in protected custom metadata, customer-owned secrets in protected custom settings, and to use named credentials where the use case calls for them.
Look beyond the Apex: static resources, custom labels and Lightning component JavaScript are where hardcoded tokens hide.
6. Insecure endpoints
Every callout should go over HTTPS, and the review checks the external endpoints your solution talks to, including their TLS configuration. A plain http:// endpoint in a callout is an easy finding to avoid:
HttpRequest req = new HttpRequest();
// Fails review: cleartext, and the URL is hardcoded.
req.setEndpoint('http://api.example.com/rates');
// Passes: a named credential over HTTPS, managed in Setup.
req.setEndpoint('callout:Rates_API/rates');
The external services behind those endpoints are in scope too. If your package calls your own API, that API's dependencies and authentication are part of what you are submitting.
How to pass the first time
The pattern that works is unglamorous:
- Scan the whole package before every submission, not just the classes you changed. CRUD and FLS gaps are spread thinly across a codebase.
- Fix CRUD and FLS first. It is the most common failure, and the fixes are mechanical once you can see them.
- Treat "could not check" as a to-do, not a pass. Anything a scanner cannot evaluate is something you review by hand.
- Write the false-positive document as you go. Every deliberate
without sharingand every system-mode query needs a sentence of justification. - Gate the build. A check that fails the pipeline while a gap remains stops a half-fixed package from going out.
Running the checks with Vulkro
Vulkro for Salesforce reads your package the way a reviewer does, on your own machine. It scores the review's requirement categories as pass, gap or not evaluated, and a category it could not evaluate is never counted as a pass. Each finding carries its rule id and the file and line, for example apex-crud-fls-unenforced-statement for a data operation with no CRUD or FLS check, vf-escape-false-taint for request data rendered with escape="false", sf-guest-aura-read-no-fls for a guest-reachable Aura method that returns records without field security, staticresource-named-secret for a token in a static resource, and apex-setendpoint-cleartext-http for a callout to a plain HTTP endpoint.
# The readiness checklist on screen (free)
vulkro-sf appexchange-report ./force-app --format table
# The HTML report for the reviewer, and the CI gate (Pro)
vulkro-sf appexchange-report ./force-app -o readiness.html
vulkro-sf appexchange-report ./force-app --format table --gate
Nothing is uploaded, and it runs offline. The Security Review use case shows the full workflow from scan to submission, and the readiness checklist lists every section with what to check by hand.
Run it before you pay for the review: get started with Vulkro for Salesforce.
Sources
- Salesforce Developers Blog, The Top 20 Vulnerabilities Found in the AppExchange Security Review (August 2023): the ranking by prevalence, the sharing and Flow guidance, secret storage options, TLS checks on external endpoints, and the Lightning XSS patterns.
