Modèles de revue de sécurité AppExchange

VérifiéSûr

Modèles de revue de sécurité Salesforce AppExchange pour les packages 2GP. Leçons tirées de rejets réels : FLS obligatoire par champ, autorisation côté serveur requise.

Spar Skills Guide Bot
SecuriteAvancé
23/07/2026
Claude CodeCursor
#salesforce#appexchange#apex#fls-crud#security-review

Recommandé pour

Synthèse automatique

Ce skill fournit des règles de revue de sécurité AppExchange pour les packages managés 2GP, afin d'orienter la rédaction d'Apex et de LWC vers des motifs acceptés par les relecteurs Salesforce.

Points forts

  • S'appuie sur un cas réel de rejet (FlexibleTeamShare) avec les constats précis des première et deuxième tentatives
  • Explicite que les garde-fous CRUD au niveau objet ne suffisent pas et impose l'application des FLS champ par champ (as user, AccessLevel.USER_MODE, stripInaccessible)
  • Rappelle que l'autorisation doit être vérifiée côté serveur, un commentaire ne valant pas contrôle d'accès
  • Fournit un arbre de décision concret pour choisir USER_MODE, SECURITY_ENFORCED ou SYSTEM_MODE selon la destination du résultat SOQL

Limites

  • Le contenu est spécifique à Salesforce Apex/LWC et aux packages managés 2GP, inutilisable ailleurs
  • Le document est tronqué avant la section complète sur les portes d'autorisation, donc certains motifs acceptables ne sont pas détaillés
  • Il ne remplace pas une analyse statique ni une revue humaine : il décrit des motifs à respecter, pas un outil de vérification automatique
Quand l'utiliser

À utiliser dès que vous modifiez des fichiers Apex ou LWC dans force-app/** ou que vous préparez une soumission AppExchange Security Review.

Quand l'éviter

À ne pas invoquer pour du développement Salesforce sans enjeu de revue de sécurité (scripts internes, orgs non packagées) ou pour des stacks hors Salesforce.

Analyse de sécurité

Sûr
Score qualité84/100

This is a pure reference/documentation skill: prose guidance on Salesforce AppExchange security-review patterns (FLS/CRUD enforcement, authorization gates, @AuraEnabled attack surface). It declares no allowed-tools, contains no executable commands, no network/curl calls, no file deletion, no credential or secret handling, and no instructions to disable safety controls. Its content is actually security-hardening advice. The only mild note is that it prompts the agent to steer edits in force-app/**, but that is ordinary code-review guidance with no execution risk.

Aucun point d'attention détecté

Comment cette analyse est faite

Exemples

Fix FLS on a DML path
This Apex class inserts Case records after checking Schema.sObjectType.Case.isCreateable(). Rewrite the DML so it passes AppExchange Security Review field-level FLS enforcement, keeping it bulk-safe.
Audit a without sharing selector
Review TeamMemberSelector.cls (a without sharing inner class called from an @AuraEnabled method). Check whether the recordId argument is actually authorized server-side and tell me which AppExchange review findings this would trigger.
Validate a false-positive justification
I want to add 'All read queries use WITH USER_MODE' to SECURITY-SCAN-FALSE-POSITIVES.md. Grep the Apex in force-app/** and tell me whether that claim is provable before I write it.

name: appexchange-review-patterns description: 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.

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; — Apex USER_MODE (requires at-least Spring '23 API and compatible context)
  • Database.insert(records, AccessLevel.USER_MODE) — equivalent for bulk DML
  • Security.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)):

  1. 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.

  2. Reachability into without sharing code must be gated. If this method ultimately causes a SOQL/DML to execute in without sharing context (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.

  3. 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 — @AuraEnabled is a public endpoint from the attacker's perspective.

  4. 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 sharing at the controller class level when the actual SOQL/DML is delegated to a without sharing inner 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 sharing outer classes, where the outer class has an authorization gate per Pattern A/B/C. The auth gate is what makes this safe; without sharing alone is not.

Always:

  • Keep the class private (inner) or tightly scoped.
  • Add justification in SECURITY-SCAN-FALSE-POSITIVES.md with 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-audit command in this repo adds checks for these gaps. Run it before submission.

Related files in this repo

  • Command: .claude/commands/appexchange-audit.md — full pre-submission audit
  • Command: .claude/commands/quality-warden.md — file-level review during dev
  • Command: .claude/commands/audit.md — project-level readiness rollup
  • Skill: .claude/skills/fp-doc-audit/SKILL.md — verifies false-positive doc claims against code
  • Skill: .claude/skills/locker-lwr-compat/SKILL.md — runtime compatibility patterns
  • FP doc (default location): docs/false-positives.md — create when you have your first finding to document. Move to docs/reference/false-positive-template.docx (official Salesforce format) before submission.
  • AppExchange artifacts (optional, present if you've started a real submission process): appexchange-artifacts/ — scan results, prior rejection reports, scanner config. Not shipped by this toolkit; user populates as their submission progresses.

This skill was originally written from the FlexibleTeamShare rejection case (Tucario, 2026). The lessons in this file are the durable patterns; the path references for scan-result CSVs etc. are conventional, not required. If your repo uses a different layout, the substance of the patterns still applies.

Du fichier au premier essai

Le téléchargement contient exactement le contenu affiché sur cette fiche. Cette copie peut différer de la dernière version du dépôt source.

Consulter la source et ses fichiers annexes

Ce téléchargement contient uniquement SKILL.md. Les scripts, références, modèles et dépendances éventuels ne sont pas inclus. Récupérez les fichiers nécessaires depuis la source en conservant leur arborescence avant de poursuivre.

1. Télécharger le fichier

Télécharger SKILL.md

Depuis la racine d’un projet de test, enregistrez le fichier sous .skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/SKILL.md, ou utilisez la commande suivante. Ce dossier sert à examiner la skill avant son installation.

Commande pour macOS/Linux ou WSL : un shell POSIX et curl sont nécessaires. Sous Windows sans WSL, utilisez le bouton de téléchargement et créez le dossier manuellement.

mkdir -p '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/download' --output '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/SKILL.md.part' && mv '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/SKILL.md.part' '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/SKILL.md'

2. Vérifier les prérequis

Lisez SKILL.md avant de l’utiliser. Si le fichier cite des scripts ou services externes, examinez-les et préparez les accès nécessaires dans votre environnement de test.

Aucune section de prérequis reconnue dans ce fichier. Cela ne signifie pas qu’il est autonome : les bibliothèques, outils et accès peuvent être cités ailleurs dans le contenu ou dans le dépôt source.

Les plateformes de la fiche sont des indications du catalogue. Les chemins ci-dessous suivent la documentation des outils ; ils ne prouvent pas que cette skill y a été exécutée. Aucune version minimale de ces outils n’est attestée pour cette fiche.

3. Charger la skill

Après vérification, copiez le dossier complet dans le projet avec l’une des commandes ci-dessous, puis ouvrez une nouvelle session de l’outil. La commande s’arrête si un dossier du même nom existe déjà : comparez les fichiers avant de le remplacer.

Claude Code
test -f '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/SKILL.md' && test -s '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/appexchange-review-patterns' && cp -R '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u' '.claude/skills/appexchange-review-patterns'

Cursor
test -f '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/SKILL.md' && test -s '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/SKILL.md' && mkdir -p '.cursor/skills' && test ! -e '.cursor/skills/appexchange-review-patterns' && cp -R '.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u' '.cursor/skills/appexchange-review-patterns'

OpenSkills

OpenSkills doit déjà être installé. La procédure locale install/read est contrôlée avec OpenSkills 1.5.0, qui exige Node.js ≥ 20.6.0. Aucun slug du catalogue n’est résolu automatiquement.

Installation officielle d’OpenSkills
openskills install './.skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u'

openskills read 'appexchange-security-review-patterns-flexibleteamshare-mrx3y01u'

La lecture doit afficher le contenu du fichier installé et son dossier de base. Elle ne lance ni la skill ni ses dépendances.

Pour un agent qui lit AGENTS.md, synchronisez ensuite l’index. Cette commande modifie AGENTS.md : relisez le diff avant de l’utiliser.

openskills sync

Mode manuel ou autre outil : joignez SKILL.md à une conversation, ou ouvrez-le dans un agent capable de lire les fichiers. Ajoutez les ressources annexes nécessaires. Il n’existe pas de dossier universel reconnu par tous les outils.

4. Faire un premier essai

Utilisez une entrée courte et fictive : un endpoint d’API, un petit document ou un brief d’interface. Dans une nouvelle conversation, joignez le fichier ou donnez son chemin à l’agent, puis adaptez cette demande :

Lis .skill-downloads/appexchange-security-review-patterns-flexibleteamshare-mrx3y01u/SKILL.md et applique ses instructions à cet exemple : [décrire une entrée de test]. Indique les fichiers, dépendances ou accès qui manquent. Produis le résultat demandé par la skill sans inventer de données ni exécuter de script non examiné.

Résultat attendu : une sortie correspondant aux instructions de la skill, ou une liste précise des prérequis manquants. Une simple confirmation de lecture ne valide pas le premier usage. Si une ressource externe est indisponible, ce parcours reste à tester.

Skills similaires