[object Object]

Most Experience Cloud data leaks are not breaches. They are the Guest User profile granted “View All” on an object three years ago by someone who has since left the company. Nobody removed it. Now it is indexed by Google.

Run this audit on every public-site-enabled Experience Cloud community. Every quarter, minimum.

What the Guest User profile actually is

Each Experience Cloud site with public access has its own Guest User. Permissions assigned to that user are what unauthenticated visitors can do — read, write, and in dangerous cases delete records. Guest User now respects sharing rules and FLS by default, but the default is overridable, and an admin who clicks past the warning can re-open the gate.

The post-Winter ‘24 lockdowns helped, but legacy sites carry legacy permissions. Audit them.

Step 1: enumerate every Guest User in the org

sf data query --query "
  SELECT Id, Name, ProfileId, Profile.Name,
         IsActive, UserType, NetworkId,
         (SELECT Id FROM PermissionSetAssignments)
  FROM User
  WHERE UserType = 'Guest'
  AND IsActive = true
" --target-org prod

You should know the count from memory. If you can’t, you’ve already lost.

Step 2: list object permissions on each Guest profile

sf data query --query "
  SELECT Parent.Profile.Name, SobjectType,
         PermissionsRead, PermissionsCreate,
         PermissionsEdit, PermissionsDelete,
         PermissionsViewAllRecords, PermissionsModifyAllRecords
  FROM ObjectPermissions
  WHERE (Parent.Profile.UserType = 'Guest'
         OR Parent.Profile.UserLicense.Name LIKE '%Guest%')
  AND (PermissionsRead = true OR PermissionsCreate = true
       OR PermissionsEdit = true OR PermissionsDelete = true)
" --target-org prod --result-format csv > guest-perms.csv

Anything with PermissionsViewAllRecords or PermissionsModifyAllRecords is a four-alarm fire. Fix today.

PermissionsEdit or PermissionsDelete on standard or custom objects: rarely correct for guests. Verify use case.

PermissionsCreate is legitimate for some patterns (lead capture, case submission). Confirm via Apex; never expose direct DML to guests.

Step 3: sharing rules and org-wide defaults

Guest user sharing has its own model since Spring ‘21. Check the secure-guest-sharing setting in Setup:

  • All custom objects: org-wide default must be Private for guest.
  • Standard objects accessed by the site: explicit sharing rules only.
  • No “Read/Write” public guest defaults.

Pull every guest sharing rule:

sf data query --query "
  SELECT Id, ObjectType, BooleanFilter,
         AccessLevel, Description
  FROM SharingRules
  WHERE UserOrGroupType = 'GuestUser'
" --target-org prod

Each should have a description that names the public-facing use case. Anything without a description is suspect.

Step 4: walk every public LWC, Aura, and Visualforce page

Search the codebase for [SELECT patterns invoked from controllers exposed to guests. The dangerous shape:

@AuraEnabled(cacheable=true)
public static List<Account> getAccounts() {
  return [SELECT Id, Name FROM Account LIMIT 100];
}

If this controller is callable by guest (no auth required, no with sharing), it bypasses sharing and you have an open API. The fix:

public with sharing class PublicAccountController {
  @AuraEnabled(cacheable=true)
  public static List<Account> getFeaturedAccounts() {
    return [
      SELECT Id, Name, Industry
      FROM Account
      WHERE IsPubliclyFeatured__c = true
      LIMIT 50
    ];
  }
}

with sharing enforces the guest user’s actual share visibility, and the explicit IsPubliclyFeatured__c filter is a belt-and-braces guarantee that only intended records are returned.

Step 5: Apex sharing recalculation jobs

Custom Apex sharing recalculations that run as System Mode can grant guest access by mistake. Look for Apex_Sharing__c records owned by the Guest User. None should exist for record types not explicitly public.

SELECT ParentId, RowCause, AccessLevel, UserOrGroupId
FROM Account__Share
WHERE UserOrGroup.UserType = 'Guest'
AND RowCause = 'Manual'

Step 6: indexing exposure

Once you’ve closed the holes, audit what’s already in the wild.

site:yourcommunity.force.com inurl:apex
site:yourcommunity.com filetype:pdf

Anything returning sensitive content needs both removal AND a noindex header added at the page level until the cache clears.

Submit removed URLs to Google Search Console’s Removals tool. Submit to Bing too. It is faster than waiting for the crawl.

Step 7: continuous detection

Schedule this Apex job weekly. It fires a platform event when guest permissions change.

public class GuestPermissionDriftDetector implements Schedulable {
  public void execute(SchedulableContext sc) {
    Integer dangerousGrants = [
      SELECT COUNT()
      FROM ObjectPermissions
      WHERE Parent.Profile.UserType = 'Guest'
      AND (PermissionsViewAllRecords = true
           OR PermissionsModifyAllRecords = true)
    ];
    if (dangerousGrants > 0) {
      EventBus.publish(new SecurityAlert__e(
        Severity__c = 'CRITICAL',
        Category__c = 'GuestUserPermissionDrift',
        Detail__c = 'View All or Modify All granted to guest profile: '
          + dangerousGrants + ' grants'
      ));
    }
  }
}

Wire the platform event to PagerDuty.

The fundamentals of field-level security patterns apply doubly to guest profiles. Field-level read on an unindexed field is still indexable.

UX note

If your Experience Cloud site lets guests submit data (lead form, support case), show an explicit “Submitting as guest — this will not be private” notice on the form. Sounds legalistic; it’s actually a trust signal. It also primes legal to never push you toward auto-prefilling guest forms with cookied data.

Bottom line

  • Run the audit quarterly. Set a calendar invite.
  • View All and Modify All on a guest profile is a fire. Treat it like one.
  • with sharing + explicit predicate filters on every guest-callable Apex controller, no exceptions.
  • Schedule a drift detector; the regression risk is permanent.
  • After cleanup, check the index. Google remembers what you leaked.
[object Object]
Share