← All posts
· 5 min read ·
Experience CloudSecuritySalesforceApex

Experience Cloud Site Security: Common Vulnerabilities and Fixes

Guest user permission creep, insecure Apex sharing, SOQL injection via search parameters - these are the vulnerabilities that compromise Experience Cloud sites. Here's how to find and fix them.

Digital security concept with lock and shield

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:

  1. Navigate to SetupSites → your site → Public Access Settings
  2. Review Object Permissions - the Guest User should only have Read access (not Create/Edit/Delete) on objects that display public content
  3. Review Field-Level Security - no sensitive fields (SSN, bank details, personal health info) should be readable
  4. 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:

SetupSites → your site → AdministrationSecurity

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.

← All posts