The night before a marketing launch

Imagine it's 10:07 p.m., the night before a marketing launch of your new Partner Portal.

A partner portal built on Salesforce Experience Cloud site would invite new partners to “Sign up to get started”.

Behind the pretty LWC form sits a guest-accessible Apex class

@AuraEnabled public static Id selfReg(User u, Contact c).

Sophia, a bored security tester, opens DevTools, repeats the POST, and tweaks two fields the UI never shows:

json
CopyEdit
"params":{
  "u":{"Username":"hacker@nowhere.io","ProfileId":"00ePartnerADMIN"},
  "c":{"FirstName":"Evil","LastName":"Hacker"}
}

Because the server believes every field it receives, Sophia is created as a Partner Community Admin—no email verification, no governance. In minutes she exports price books and private chatter files.

Everything broke at one spot. That is trusting what comes over the wire, simply because it arrived from the site’s own component.

This playbook is a barricade you can drop in front of that path—broken into six fast-scan sections.

Self-Registration Attack Surface

What can go wrong? Real-world symptoms
Guest-exposed method inserts the User outrightAttackers script thousands of fake users, burning licenses and polluting data.
Email never verified (or link easy to guess)Disposable addresses create sleeper accounts, later escalated.
No domain allow-listPublic email domains register as “partners.”

How to Harden Experience Cloud

  • Separate steps: One method only creates a pending Contact + caches a verification code; a second method (logged-in or via one-time token) activates the User.
  • Require a domain allow-list: Store allowed partner domains in Custom Metadata and reject the rest.
  • Rate-limit verification codes: Salesforce allows only 5 codes per email per hour. Log and throttle beyond that.
  • Enforce “Inactive until verified” in your ConfigurableSelfRegHandler.

Robust E-mail Verification Pattern

apex
CopyEdit
@AuraEnabled
public static String startRegistration(String emailAddress) {
   // 1. Basic format check
   if (String.isBlank(emailAddress) ||
       !Pattern.matches('[\\\\w.%+-]+@[\\\\w.-]+\\\\.[A-Za-z]{2,6}', emailAddress)) {
       throw new AuraHandledException('Invalid e-mail');
   }

   // 2. Domain allow-list
   if (!EmailDomainWhiteList__mdt.getInstance(getDomain(emailAddress)).Allowed__c) {
       throw new AuraHandledException('Domain not permitted');
   }

   // 3. Strip non-alphanumerics → safe cache key
   String cacheKey = emailAddress.replaceAll('[^a-zA-Z0-9]', '');

   // 4. Send 6-digit code & cache for 15 min
   String code = Application_PP.Service
                 .newInstance(EmailsService_PP.class)
                 .sendVerificationCode(emailAddress);
   Cache.Org.put('local.verificationCodes.' + cacheKey, code, 900);

   return 'CODE_SENT';
}

The attacker can still call this directly, but now every downstream step checks that the code in Cache.Org matches before activating the User.

Parameter-Smuggling: Stop It at the Door

Defensive rule Why
Accept DTOs, not SObjects Any field not in the DTO is automatically ignored; no sneaky ProfileId, OwnerId.
Whitelist picklists & RecordTypeIds Compare against a Custom Metadata set, throw on mismatch.
with sharing + stripInaccessible() Prevents bypass of record-level sharing and enforces FLS.
Reject oversize bodies Drop any payload >50 KB—common in fuzzing attacks.

Remember: an Aura or LWC front-end only suggests which fields will be sent. Attackers build their own payloads.

@AuraEnabled Methods — Guard Rails

Abuse scenario Guard-rail
Guest calls hidden action because the class is on the Guest profile. Remove class from Guest profile; if truly needed, wrap every method: if(UserInfo.getUserType()=='Guest') throw ….
Authenticated partner adds forbidden fields in JSON. DTO validation + Security.stripInaccessible.
Method leaks everything on success/error via raw exception JSON. Catch exceptions, return generic error enums.
Large SObject list returned exposes private fields. Return wrappers only (section 5).
Bulk “create” via loop (DoS) Check Limits.getCallouts() / Limits.getDMLRows() and short-circuit.

The risk is not hypothetical. Public reports of guest-user data leaks trace back to over-permissive Apex access, and to classes left visible after package installs.

Why Wrappers Beat Raw SObjects

Raw Opportunity → sends hidden fields (IsPrivate, ForecastCategory, margins).

A wrapper sends only what the UI needs:

apex
CopyEdit
public class OppSummary {
  @AuraEnabled public Id      id;
  @AuraEnabled public String  name;
  @AuraEnabled public Decimal amount;
  @AuraEnabled public Boolean canEdit;
  public OppSummary(Opportunity o){
      id=o.Id; name=o.Name; amount=o.Amount;
      canEdit = Schema.sObjectType.Opportunity.isUpdateable();
  }
}

Wrappers also allow you to add flags (canEdit, showRenewalsCTA) without changing client code—a pattern Salesforce highlights as best practice.

Threat Matrix — Guest vs Logged-In Partner

Actor Typical goals Highest-risk surfaces Mitigation
Guest Enumerate data, DoS registration, create fake users @AuraEnabled classes on Guest profile, Flow “Autolaunched” with API access Strip Guest CRUD; no DML in guest context; CAPTCHA + throttle self-reg
Partner user Lateral move to other Accounts, privilege escalation Passing foreign AccountId, ProfileId; abusing overlooked FLS Re-query every record by Id & compare AccountId; enforce with sharing; DTO validation
Compromised employee Mass exfiltration via API Large list responses, un-logged downloads Use wrappers, event monitoring on Export API, set API session timeouts

60-Second Audit Checklist

  1. Guest profile – zero object CRUD, no Apex classes unless absolutely required.
  2. Every external Apex classwith sharing, DTO input, wrapper output.
  3. Self-registration – inactive User until email/phone verified; codes expire ≤15 min.
  4. Custom Metadata allow-lists – partner domains, RecordTypeIds, ProfileIds.
  5. Logs & alerts – Event Monitoring on AuraInvocations where UserType='Guest' AND DmlRows > 0.
  6. CI static scan – block PRs when @AuraEnabled method exposes SObject or lacks FLS checks.

Parting Thought

A public Experience Cloud site is effectively a REST API; Lightning just gives it a friendly face. Treat every Apex method as internet-facing, assume every parameter can be forged, and let wrappers be your last-line bouncer. Follow the segments above, and the next Sophia who appends ?role=Admin will land on a 401—not in your data.

Author
Jack Cronin
Technical Architect

**Articles worth your scroll**

Ideas, insights, and the occasional strong opinion — nothing you'd find in a LinkedIn echo chamber.
View all articles

**Conversations worth remembering**

Candid chats with sharp people. No jargon. Just honest thoughts that made us think twice.
View all conversations
No items found.

Our team

We're proud masters & teachers of our craft.
Our story
Join our team

MORE INSIGHTS, LESS FLUFF

Curious minds tend to scroll. **We get it.**

Explore articles, playbooks, and case studies built for teams who like their resources actionable and their time well spent.

Return to all resources