
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
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
Remember: an Aura or LWC front-end only suggests which fields will be sent. Attackers build their own payloads.
@AuraEnabled
Methods — Guard Rails
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
60-Second Audit Checklist
- Guest profile – zero object CRUD, no Apex classes unless absolutely required.
- Every external Apex class –
with sharing
, DTO input, wrapper output. - Self-registration – inactive User until email/phone verified; codes expire ≤15 min.
- Custom Metadata allow-lists – partner domains, RecordTypeIds, ProfileIds.
- Logs & alerts – Event Monitoring on AuraInvocations where
UserType='Guest' AND DmlRows > 0
. - 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.

**Articles worth your scroll**
**Playbooks we actually use**
**Conversations worth remembering**
Our team
We’re not here to jump from project to project or slap together whatever’s in the spec. We embed with our clients for the long haul — mastering our domains, owning what we deliver, and upskilling those around us.
We love seeing the real impact of our work—on revenue, on teams, on careers. And we hate seeing things built wrong, rushed, or left to burn after go-live. So we do it right, with senior engineers, proven best practices, and modern frameworks to ensure what we build today scales for tomorrow.
MORE INSIGHTS, LESS FLUFF
Explore articles, playbooks, and case studies built for teams who like their resources actionable and their time well spent.
