Experience Cloud sites expose your Salesforce org to unauthenticated users. Done right, that’s a controlled, intentional surface. Done wrong, it’s an open door to your CRM data. The vulnerabilities in Experience Cloud sites follow predictable patterns - and most of them are introduced gradually, one “let’s just add this permission” decision at a time.
The Most Common Issue: Guest User Permission Creep
The Guest User profile is the Salesforce user that represents unauthenticated visitors to your site. Every request from someone who hasn’t logged in runs as this user. The problem: over time, developers add permissions to the Guest User profile to fix broken functionality - CRUD on objects, FLS on fields - without understanding the full scope of what they’re enabling.
The result is a Guest User that can read (or worse, create and edit) records it should never touch.
The principle: Guest users should have the minimum permissions needed to render the site’s public content, nothing more.
Audit your Guest User profile regularly:
- Navigate to Setup → Sites → your site → Public Access Settings
- Review Object Permissions - the Guest User should only have Read access (not Create/Edit/Delete) on objects that display public content
- Review Field-Level Security - no sensitive fields (SSN, bank details, personal health info) should be readable
- Run the Guest User Access Report from Setup → Security → Guest User Access Report
Insecure Apex Sharing on Experience Cloud Sites
When Apex is called from an Experience Cloud site - whether via LWC, an action, or a community page - it executes in the context of the Guest User or the logged-in community user. The sharing model for that user determines which records are accessible.
The danger is without sharing classes:
// VULNERABLE - guest users can access any Account record
public without sharing class PublicAccountSearch {
@AuraEnabled
public static List<Account> searchAccounts(String searchTerm) {
return [SELECT Id, Name, Phone, Revenue__c
FROM Account
WHERE Name LIKE :('%' + searchTerm + '%')];
}
}
With without sharing, the query ignores sharing rules entirely. A guest user hitting this endpoint can enumerate your entire Account database.
The fix is to enforce sharing, and to be explicit about it:
// SECURE - sharing rules and OWD are enforced
public with sharing class PublicAccountSearch {
@AuraEnabled
public static List<Account> searchAccounts(String searchTerm) {
// Only returns records the current user has access to via sharing
return [SELECT Id, Name, Phone
FROM Account
WHERE Name LIKE :('%' + searchTerm + '%')
AND Is_Public__c = true // additional explicit filter
LIMIT 50];
}
}
Default to with sharing for all Apex called from Experience Cloud. Only use without sharing when you’ve explicitly reviewed the implications and have alternative access controls in place.
SOQL Injection via URL Parameters
LWC components in Experience Cloud often take search terms or filter values from URL parameters. If those values are used in SOQL without sanitization, you have a SOQL injection vulnerability.
// VULNERABLE - direct string concatenation in dynamic SOQL
public with sharing class ProductSearch {
@AuraEnabled
public static List<Product2> search(String category) {
String query = 'SELECT Id, Name, Price FROM Product2 WHERE Category__c = \'' + category + '\'';
return Database.query(query);
}
}
An attacker can pass ' OR Is_Internal__c = true OR Name = ' as the category parameter and bypass your filter entirely.
The fix for dynamic SOQL is bind variables, not string sanitization:
// SECURE - bind variable prevents injection
public with sharing class ProductSearch {
@AuraEnabled
public static List<Product2> search(String category) {
// Bind variable - category value is treated as a literal string, not SQL
return [SELECT Id, Name, Price FROM Product2
WHERE Category__c = :category
AND Is_Active__c = true
LIMIT 100];
}
}
If you genuinely need dynamic field names or object names in a query, use String.escapeSingleQuotes() on those identifiers specifically - but prefer static SOQL with bind variables wherever possible.
Using OWD and Sharing Rules Instead of Guest User Permissions
The right way to expose records to guest users is through Sharing Rules, not by widening the Guest User profile.
Set the OWD for the object to Private or Public Read Only. Then create Sharing Rules that share specific records with the Guest User based on criteria:
- Products with
Is_Public__c = true→ share with Guest User, Read access - Articles with
Status = Published→ share with Guest User, Read access
This way, even if someone queries the object without a filter, they only see records explicitly shared with the guest user. The sharing model does the access control rather than relying on developers to always write the right WHERE clause.
Clickjacking and CSP Headers
Experience Cloud sites are clickjacking targets because they can be embedded in iframes on attacker-controlled pages. Salesforce provides CSP (Content Security Policy) settings at the site level:
Setup → Sites → your site → Administration → Security
Enable Clickjack Protection - set to at minimum “Allow framing by the same origin only.” For public sites with no legitimate reason to be embedded, set it to “Don’t allow framing by any page.”
For custom CSP policies, use the CSP Trusted Sites feature (Setup → CSP Trusted Sites) to explicitly allowlist external domains your site loads resources from, and keep the default-src policy restrictive.
Vulnerable vs Secure Pattern Summary
// VULNERABLE
public without sharing class SiteController {
@AuraEnabled
public static List<Account> getAccounts(String filter) {
String q = 'SELECT Id, Name, SSN__c FROM Account WHERE ' + filter;
return Database.query(q);
}
}
// SECURE
public with sharing class SiteController {
@AuraEnabled
public static List<Account> getPublicAccounts(String nameFilter) {
return [
SELECT Id, Name // no sensitive fields
FROM Account
WHERE Is_Public__c = true // explicit public filter
AND Name LIKE :('%' + nameFilter + '%') // bind variable
LIMIT 50
];
}
}
The secure version is with sharing, queries only explicit public records, excludes sensitive fields from the SELECT, uses a bind variable, and limits the result set size. Any one of these controls helps; all of them together make a meaningful difference.
Experience Cloud security isn’t a one-time audit - it’s an ongoing posture. Add the Guest User Access Report to your quarterly security review, enforce with sharing as a code review requirement for any Apex with @AuraEnabled methods, and treat every new Guest User permission grant as a decision that requires justification.