appexchange-review-patterns
Provides security review guidelines and anti-patterns for Salesforce 2GP managed package development.
Install
mkdir -p .claude/skills/appexchange-review-patterns && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16855" && unzip -o skill.zip -d .claude/skills/appexchange-review-patterns && rm skill.zipInstalls to .claude/skills/appexchange-review-patterns
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Salesforce AppExchange Security Review patterns for 2GP managed packages — what manual reviewers accept/reject, anti-patterns to avoid, fix recipes. Derived from a real FlexibleTeamShare rejection (case study in body). TRIGGER when editing Apex (.cls/.trigger) or LWC (.js/.js-meta.xml/.html) in force-app/**, or when the user mentions "security review", "AppExchange", "FLS", "sharing violation", "CRUD", or reads appexchange-artifacts/**. Use to proactively steer code toward review-passing patterns and to flag the recurring anti-patterns that get packages rejected.Key capabilities
- →Enforce FLS per field in Apex
- →Implement `USER_MODE` for DML operations
- →Use `Security.stripInaccessible` before DML
- →Validate client-controlled inputs in `@AuraEnabled` methods
- →Implement server-side authorization checks for `without sharing` code
- →Ensure outputs respect FLS in `@AuraEnabled` methods
How it works
This skill provides patterns and fix recipes for Salesforce AppExchange Security Review, focusing on FLS enforcement, authorization gates, and avoiding anti-patterns in Apex and LWC.
Inputs & outputs
When to use appexchange-review-patterns
- →Preparing code for security review
- →Fixing sharing violations
- →Implementing proper FLS enforcement
- →Reviewing Apex for security patterns
About this skill
AppExchange Security Review Patterns — FlexibleTeamShare
Reference knowledge for writing and reviewing Apex/LWC in this repo so that code does not repeat the patterns that caused prior Salesforce manual-review rejections.
Hard-won lessons (2026 submissions)
Two patterns were explicitly rejected by Salesforce manual reviewers across Checkmarx (1attempt) and the Product Security team's manual review (2attempt). Both had been dismissed as false positives in our own documentation. The FP doc's claims were wrong — do not repeat.
Lesson 1: CRUD alone is not enough. FLS must be enforced per field.
Reviewers will not accept Schema.sObjectType.X.isCreateable() / isUpdateable() / isDeletable() object-level guards as FLS enforcement on DML. They explicitly asked for one of:
insert x as user;/update x as user;/delete x as user;— ApexUSER_MODE(requires at-least Spring '23 API and compatible context)Database.insert(records, AccessLevel.USER_MODE)— equivalent for bulk DMLSecurity.stripInaccessible(AccessType.CREATABLE, records)followed by plain DML- Explicit per-field loop via
Schema.sObjectType.X.fields.getMap()checking each.getDescribe().isCreateable()/isUpdateable()on every field being written
This rule applies inside without sharing classes too. Reviewers no longer accept the legacy argument "without sharing + CRUD check is fine because sharing is intentionally bypassed." Use as user (which enforces FLS+CRUD without re-enabling sharing) or the per-field loop. The 2attempt report Findings 1–5 were all about exactly this.
Lesson 2: A comment is not an authorization gate.
The rejected code had inner without sharing classes (TeamMemberSelector, ElevatedDmlOperations) with comments like:
"users can only see team members for records they already have access to (recordId comes from the UI context)"
This is a claim about callers, not a control. @AuraEnabled methods are callable directly from any authenticated user with Apex execute permission and the argument (recordId, memberId, …) is fully controlled by the client. The reviewer demonstrated a sharing bypass by calling the endpoint with IDs the calling user did not have access to.
Server-side authorization must exist in code, not in comments. Acceptable patterns are listed in the Authorization Gate section below. See 2attempt Sharing Violation Findings 1–2.
Lesson 3: False-positive doc claims must be verifiable.
SECURITY-SCAN-FALSE-POSITIVES.md contained the claim "All read queries use WITH USER_MODE" — which was materially false for TeamMemberSelector and ElevatedDmlOperations. The reviewer verified by grep and rejected the justifications.
When writing a false-positive entry, the claim must be grep-provable. "Uses WITH USER_MODE" is grep-provable. "Users only reach this via UI" is not.
The FLS/CRUD decision tree
When writing a SOQL query:
Is the result returned to the client (LWC / API response)?
├─ Yes → MUST enforce FLS on read:
│ WITH USER_MODE (preferred — enforces sharing + FLS)
│ or WITH SECURITY_ENFORCED (legacy equivalent)
│ or per-field isAccessible() before building the result
└─ No (internal auth/logic only, result never leaves Apex context) →
WITH SYSTEM_MODE is acceptable *if* the class is without sharing
and the result is used only for authorization decisions
When writing DML:
Is the object a *Share / *__Share (platform share table)?
├─ Yes → plain DML is fine; FLS not applicable to share objects
└─ No →
Are you in an InstallHandler inserting PermissionSetAssignment?
├─ Yes → plain DML acceptable (install-time, by design system context)
└─ No → MUST enforce FLS:
`insert record as user;` (Apex USER_MODE on DML)
or `Database.insert(records, AccessLevel.USER_MODE)`
or `Security.stripInaccessible(...)` before plain DML
or explicit per-field loop
The @AuraEnabled attack-surface checklist
For every @AuraEnabled method (including @AuraEnabled(cacheable=true)):
-
Inputs are attacker-controlled.
recordId,memberId,userId,accessLevel,role— all of these come from the client and may be forged. Validate format and semantics; never trust that they correspond to records the caller is allowed to access. -
Reachability into
without sharingcode must be gated. If this method ultimately causes a SOQL/DML to execute inwithout sharingcontext (class-level or inner-selector), there must be a server-side authorization check at this method's entry, before the without-sharing call. See Authorization Gate below. -
Mutating methods (add/update/remove/create/save/delete) MUST have an authorization check server-side. UI-side gates like hiding a button are not enough —
@AuraEnabledis a public endpoint from the attacker's perspective. -
Outputs must respect FLS. Either the SOQL enforces FLS (see decision tree above), or the wrapper only exposes fields the caller is allowed to see, or
Security.stripInaccessible(AccessType.READABLE, records)is applied before building the response.
Authorization gate — acceptable patterns
Use one of these patterns at the entry of any @AuraEnabled method whose work depends on record-level authorization:
Pattern A — explicit boolean check with throw:
@AuraEnabled
public static TeamMemberWrapper updateTeamMember(String memberId, ...) {
ObjectTeamMember__c m = [SELECT Record_Id__c FROM ObjectTeamMember__c
WHERE Id = :memberId WITH USER_MODE LIMIT 1];
if (!isCurrentUserManager(m.Record_Id__c)) {
throw new AuraHandledException(Label.FTS_Error_NotAuthorized);
}
// ... proceed with without-sharing mutation
}
Pattern B — with sharing re-query of the parent record (returns 0 rows if caller lacks access):
// Re-query parent in with-sharing context; if no rows, caller has no access.
List<Account> accessibleParent =
new ParentAccessor().fetch(parentId); // class declared with sharing
if (accessibleParent.isEmpty()) {
throw new AuraHandledException(Label.FTS_Error_NotAuthorized);
}
Pattern C — profile check for admin-only operations:
Profile p = [SELECT Name FROM Profile WHERE Id = :UserInfo.getProfileId()
WITH SYSTEM_MODE LIMIT 1];
if (p.Name != 'System Administrator') {
throw new AuraHandledException(Label.FTS_Error_AdminRequired);
}
NOT acceptable as a gate:
- A comment claiming "UI prevents this"
- Object-level CRUD check alone (
isUpdateable()) — this only says "user has field access," not "user is allowed to affect this specific record" if (!Test.isRunningTest()) { … }or any condition that is not a per-record authorization decision- Relying on
with sharingat the controller class level when the actual SOQL/DML is delegated to awithout sharinginner class
without sharing — when it is OK
Acceptable uses:
- InstallHandler (
PostInstallHandler) — runs system-context at install time by design. Document in FP doc. - Flow-invocable actions (
@InvocableMethod) that must operate across all records regardless of caller — document the business reason. Example in this package:SyncOwnerInvocable(Salesforce reviewer accepted this justification in 2attempt). - Queueable/Batchable/Schedulable system jobs, when they must operate on records the caller does not own. Document.
- Private inner selector classes invoked from
with sharingouter classes, where the outer class has an authorization gate per Pattern A/B/C. The auth gate is what makes this safe;without sharingalone is not.
Always:
- Keep the class
private(inner) or tightly scoped. - Add justification in
SECURITY-SCAN-FALSE-POSITIVES.mdwith a grep-provable claim. - Keep CRUD checks where applicable and FLS enforcement on DML/SOQL per the decision tree.
Anti-patterns that previously failed review
From 2attempt findings — do not reintroduce:
| Anti-pattern | Why it failed | Fix |
|---|---|---|
SELECT … FROM ObjectTeamMember__c WHERE … in without sharing inner class, returned via @AuraEnabled | No FLS, no server-side auth. Any user with read on ObjectTeamMember__c could enumerate team members for records they couldn't see. | Add WITH USER_MODE to the SOQL and add Pattern A auth gate at the @AuraEnabled entry. |
ElevatedDmlOperations.updateMember(member) → plain update member; in without sharing, only object-level CRUD checked at caller | No FLS, no per-record auth. Any user with update on ObjectTeamMember__c could mutate any member org-wide. | update member as user; plus Pattern A auth gate verifying caller manages member.Record_Id__c. |
@SuppressWarnings('PMD.ApexCRUDViolation') on class with no FLS | Silences the static analyzer while the violation remains. Review team verifies manually, not via PMD output. | Remove the suppression, add real FLS per decision tree. Only keep suppression after a grep-provable FP-doc justification. |
| Comment-based "security note" as justification | Claim, not a control. | Add a concrete auth check in code per Authorization Gate. |
FP doc claim "uses WITH USER_MODE" not matching actual SOQL | Reviewer greps; claim false → immediate rejection. | Make the claim true (edit the SOQL) before writing it in the doc. |
What the Code Analyzer catches (and doesn't)
sf code-analyzer run --rule-selector AppExchange reliably flags:
ApexFlsViolation— FLS missing on SOQL/DML (matches Lesson 1)DatabaseOperationsMustUseWithSharing— class-level sharing issue (partial match for Lesson 2)
It does not reliably flag:
- Missing server-side authorization for client-controlled IDs (Lesson 2's real core — it's a logic issue, not a syntactic one)
- Stale false-positive doc entries (Lesson 3)
- Weak-justification patterns in comments
The `/appexchange-a
Content truncated.
When not to use it
- →When CRUD checks alone are considered sufficient for FLS enforcement on DML
- →When comments are used as authorization gates instead of code
- →When `SECURITY-SCAN-FALSE-POSITIVES.md` claims are not verifiable by grep
Limitations
- →Specific to Salesforce AppExchange Security Review patterns for 2GP managed packages
- →Focuses on Apex and LWC development
- →Requires `USER_MODE` or explicit per-field FLS enforcement for DML
How it compares
This skill directly addresses common Salesforce manual-review rejection patterns, providing specific code adjustments and architectural considerations for AppExchange compliance, unlike general security best practices.
Compared to similar skills
appexchange-review-patterns side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| appexchange-review-patterns (this skill) | 0 | 3mo | Review | Advanced |
| software-security | 21 | 6mo | No flags | Intermediate |
| fix-dependabot-alerts | 18 | 6mo | Review | Intermediate |
| backend-security-coder | 24 | 4mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
software-security
project-codeguard
A software security skill that integrates with Project CodeGuard to help AI coding agents write secure code and prevent common vulnerabilities. Use this skill when writing, reviewing, or modifying code to ensure secure-by-default practices are followed.
fix-dependabot-alerts
microsoft
Fix Dependabot security alerts by updating vulnerable npm dependencies. Use when the user mentions "dependabot", "security alerts", "vulnerability", "CVE", or wants to update packages with security issues.
backend-security-coder
sickn33
Expert in secure backend coding practices specializing in input validation, authentication, and API security. Use PROACTIVELY for backend security implementations or security code reviews.
equilateral-agents
Equilateral-AI
22 production-ready AI agents with database-driven orchestration for security reviews, code quality analysis, deployment validation, infrastructure checks, and compliance. Auto-activates for security concerns, deployment tasks, code reviews, quality checks, and compliance questions. Includes upgrade paths to enterprise features (GDPR, HIPAA, multi-account AWS, ML-based optimization).
top-100-web-vulnerabilities-reference
davila7
This skill should be used when the user asks to "identify web application vulnerabilities", "explain common security flaws", "understand vulnerability categories", "learn about injection attacks", "review access control weaknesses", "analyze API security issues", "assess security misconfigurations", "understand client-side vulnerabilities", "examine mobile and IoT security flaws", or "reference the OWASP-aligned vulnerability taxonomy". Use this skill to provide comprehensive vulnerability definitions, root causes, impacts, and mitigation strategies across all major web security categories.
differential-review
trailofbits
Performs security-focused differential review of code changes (PRs, commits, diffs). Adapts analysis depth to codebase size, uses git history for context, calculates blast radius, checks test coverage, and generates comprehensive markdown reports. Automatically detects and prevents security regressions.