Experience Cloud makes it easy to publish a portal and difficult to publish a secure one. The defaults that get a proof of concept working are frequently the same settings that fail a security assessment months later, once the portal has real users and real data behind it.
These five issues account for most of the findings we see when auditing an existing portal.
1. Guest user access that was never tightened
The guest user profile is the single highest-risk object in an Experience Cloud site. During build, someone grants read access to an object so a public page renders. That access is rarely revisited, and it applies to anyone on the internet who can reach the site.
- Audit the guest user profile object by object and field by field. Assume anything readable is public.
- Keep 'Secure guest user record access' enabled. Disabling it to fix a rendering problem is trading a security control for a convenience.
- Never grant the guest user access through sharing rules that target broad record sets.
- Test the public site while fully logged out, in a private window, including direct record URLs — not just the navigation paths you designed.
2. Sharing sets scoped wider than the use case
Sharing sets grant portal users access to records related to their account or contact. They are the correct mechanism, but the access-mapping criteria are easy to draw too broadly — particularly when a mapping is added to fix one user's missing record and inadvertently widens visibility for every user of that profile.
Review every sharing set against a single question: can a user of this profile see a record belonging to a different customer? Test it with two real accounts rather than reasoning about it, because the interaction between sharing sets, the account role hierarchy and implicit sharing is not intuitive.
3. Apex that does not enforce CRUD and FLS
Apex runs in system context by default. A controller backing a portal component will happily return fields the running user has no permission to see, and write to objects they cannot edit. The sharing model you designed is bypassed entirely unless the code respects it.
// Enforce sharing at the class level, and field-level security in the query.
public with sharing class PortalCaseController {
@AuraEnabled(cacheable=true)
public static List<Case> getMyCases() {
return [
SELECT Id, CaseNumber, Subject, Status, CreatedDate
FROM Case
WHERE ContactId = :getRunningContactId()
WITH USER_MODE
ORDER BY CreatedDate DESC
LIMIT 200
];
}
}Use 'with sharing' on every controller reachable from a portal, and 'WITH USER_MODE' on queries and DML so field-level security is enforced by the platform rather than by the developer remembering to check.
4. Sensitive data in URL parameters and component attributes
Record identifiers and filter values passed through URL parameters are trivially editable. If a component renders whatever record id it finds in the query string, a user will eventually try someone else's id — and if the sharing model has a gap, they will find it.
Resolve the record server-side from the running user's context wherever possible. Where an id genuinely must be passed, validate ownership in Apex before returning anything.
5. No re-test after a template or release change
Salesforce ships three releases a year, and Experience Cloud templates change with them. Access behaviour that was correct at launch can shift after an upgrade, particularly around guest access and caching.
- Re-run your logged-out and cross-account access tests in the sandbox preview before every seasonal release.
- Keep the tests written down as a checklist rather than in one person's memory.
- Re-run the Security Health Check and review portal-relevant findings each quarter.
Design the access model before the pages
Every one of these issues stems from the same root cause: the portal was built first and the access model was adjusted afterwards to make it work. Reverse that order. Decide licence types, profiles, sharing sets and guest access on paper, validate them with test users, and only then start building pages on top.