A consolidated list of rules that you can apply to build secure sites on the Salesforce Experience Cloud Platform.
Use this rule book as a reference to identify and fix potential vulnerabilities in your Salesforce Experience Cloud site that could inadvertently expose sensitive customer data. The idea is not to reinvent the wheel, so where there is relevant Salesforce documentation I’ll be referencing them and will add more details as needed.
We will begin with a few functional and configuration-based rules before transitioning to the core technical ones.
- Rule #1 Define Personas with Granular Capabilities
- Rule #2 Keep User Profiles Lean and Grant Granular Access via Permission Sets
- Rule #3 Scrutinize Unauthenticated Guest User Access
- Rule #4 Define and Secure Personally Identifiable Information (PII)
- Rule #5 Know and Review Your Global and Individual Site Settings
- Rule #6 Enable the Appropriate Clickjack Protection for your Site
- Rule #7 Enable the Appropriate Content Security Policy (CSP) Level for your Site
- Rule #8 Restrict the External Organization-Wide Defaults
- Rule #9 Enforce Object (CRUD) & Field (FLS) Permissions in your Apex UI Controller Methods
- Rule #10 Data Accessible via the User Interface & API Should be Consistent
- Rule #11 Review the Impact of Implicit Sharing
- Rule #12 Use ‘Without Sharing’ Sparingly & with Necessary Safeguards
| Rule #1 – Define Personas with Granular Capabilities
As simple as this sounds, it’s one of the fundamental misses I have seen while planning an Experience Cloud implementation. More commonly, I have come across prioritized feature backlogs which are great but it’s equally important to look at these features through the lens of the different personas interacting with your site. This not only gives you a sense of the data access requirements but also drives your security & permission engine.
Once you have the personas & capabilities defined, this should translate into a persona-permission mapping document that becomes the source of truth for the implementation team.
| Rule #2 – Keep User Profiles Lean and Grant Granular Access via Permission Sets
This is also the direction that Salesforce is moving towards and this Salesforce Admins blog covers the benefits of moving away from profiles to permission set groups and permission sets.
Data Security Issues with Profiles
- Due to their monolith nature, it’s difficult to keep track of all the permissions which results in errors granting undesired access
- 1:1 relationship with the user object doesn’t allow for modular feature-specific assignments
- Creating a new profile requires you to clone from an existing standard profile, copying over excessive permissions that your site doesn’t need.
As you work through your personas & permission assignments, here are some of the principles you can apply:
- Use Profiles as a shell for License & Page-Layout Assignment
– In its current state you can’t do this with permission sets - Create Feature-Specific Permission Sets
- Maintain a 1:1 relationship between Persona & Permission Set Groups allowing for easier assignment
Sample Persona & Permission Mapping Template
Persona | Profile | License | Permission Set Group | Permission Sets |
Registered Member | Community Forum Member – CC | Customer Community(CC) | Community Forum Member – CC | Forum Access Self-Service Cases Self-Service Knowledge |
| Rule #3 – Scrutinize Unauthenticated Guest User Access
Data Leak of any kind is not good, but if it’s through an unauthenticated route you are making it way too easy! Specifically for guest user access you really need to go to the granular capability level, question its purpose and justify unauthenticated access.
On the positive side, over the last 2 years, Salesforce has done a fabulous job locking down guest user access and you can find the details here: Guest User Security Policies and Timelines. Since the documentation is quite thorough, I won’t get into the details but here are a few of the critical ones:
- Guest users can’t be the owner of any record in your org.
- Guest users can only get access to records through guest user sharing rules & the maximum access granted is read.
- Guest users can’t have View All or Modify All access on objects.
- Guest users can’t have the update or delete permissions on objects.
- Guest users can’t be members of public groups or queues.
- Run Flows for Guest Users is no longer supported.
– Leverage the new flow permission structure instead!
If you have been working with Experience Cloud you have probably already made the necessary changes to comply with these above mandatory policies. In addition, there are a few more considerations.
Enabling Public Access To Your Site
When you enable public access to your site do so at the page level instead of making the entire site public, this gives you more control by keeping your overall site gated and only opening up specific pages that need public access.
NOTE: This rule won’t apply if 90 to 100% of your site is public
Leverage Page or Component Audience Variations for Guest Users
For the identified public pages you can further lock down what data is made available by leveraging audiences.
For example, if we consider the member profile page below, the question you want to ask is, ‘Do guest users need access to all this info or just a subset? ..maybe just the community nickname & their latest publicly available posts?’
To summarize, it’s important to make these decisions upfront and document them as part of the persona and capability mapping exercise so everyone on the team is well aware of what guest users can access.
NOTE: There are a few guest-user specific site preferences that will be covered under Rule 5.
| Rule #4 – Define and Secure Personally Identifiable Information (PII)
Firstly from a business perspective, you need to have a consensus on what data points are deemed as PII, and then you need to secure access by leveraging platform capabilities & custom handling when needed.
Enhanced Personal Information Management
The identified User PII fields can be added to a field-set called PersonalInfo_EPIM on the User Object. By default Salesforce has 20 fields in this field-set. For all such fields, profile pages in Experience Cloud sites will display blank fields or in the case of Name fields, they will be replaced by the nickname. Reference this detailed documentation for more info.
PersonalInfo_EPIM Field Set
NOTE: If you enabled the EPIM setting before Spring ’22, instead of the field-set you will need to use the Compliance Categorization setting at the individual field level for all PII fields. Reference the above documentation for more details.
Custom Handling
The benefit of this EPIM setting is that all the OOB components and APIs respect this PII setting once configured, however, there are a few caveats that require custom handling
- EPIM Setting isn’t enforced in Apex
– If you have a custom component querying the same User PII fields, this setting isn’t respected - In its current state, this is only supported on the User Object
– If you have PII fields on other objects, more commonly on Contact, that would need custom handling.
NOTE: When it comes to custom handling specifically for the User object you can do this in Apex by validating against the field-set or querying the data classification metadata(Compliance Category), you can also apply this same principle for PII fields on other objects, however, if you leverage OOB components like record-detail/list for non-user objects this scenario goes unhandled. Here is an idea I have submitted to the product team to extend EPIM to other objects, if this impacts you I would urge you to upvote!
| Rule #5 Know and Review Your Global and Individual Site Settings
The common pitfall I have seen here is that certain OOB components require specific settings to be enabled to work as intended, and often when there is a time crunch, we go the easier route of enabling all of the options, which works, but you have inadvertently opened up access that you shouldn’t have!
It’s important to understand that these settings are managed at the global org and the individual site level.
NOTE: For a net new Salesforce Org, the default state of these settings is the most secure, however that may not be the case for older orgs, regardless, I would strongly recommend validating their state as part of your security review.
Global Settings
These apply to all the sites in your org.
1. Digital Experience Settings
Every setting here is very well documented so I won’t go into the details but will call out a few.
- Allow users to see contacts that have not been enabled for partner or customer accounts
– Very rarely would you have external users access internal contact information, the default state is switched off & be extremely cautious of the downstream impacts if you decide to enable this. - Allow using standard external profiles for self-registration, user creation, and login
– The reason these standard profiles are disabled for use by default is that they come with a few object and system permissions pre-enabled that your implementation may not need. Moreover, as stated in Rule 2, the idea is to move away from profiles to permission sets.
NOTE: Please do not clone the standard profiles and use them as-is, yes, you have bypassed this setting but it defeats the purpose.
- Hide badges from guest users in Experience Builder sites
– This is enabled by default and even if you are utilizing Recognition Badges, I would recommend making this available only for registered members.
2. User Visibility Settings
You will find this under Setup → Sharing Settings & there are two types.
Portal User Visibility: If enabled, portal users in the same customer or partner portal account can see each other, regardless of the org-wide defaults.
In the example below, John would see Jane but not Mark.
Site User Visibility: This setting controls whether user sharing is available for authenticated users in your organization’s communities.
In the example below, John would see Jane & Mark i.e all the users under the same site, when Site User Visibility & the corresponding Site Preference is enabled.
Individual Site Settings
Site Preferences(Site → Workspaces → Administration → Site Preferences)
Authenticated User Preferences
- See other members of the site
To view other members of the site first enable the global Site User Visibility Setting, followed by this setting.
Guest User Preferences
- Let guest users view asset files and CMS content available to the site
This setting is automatically enabled if public access is enabled at the page or site level in Experience Builder. It provides access to assets like images associated with topics, branding, or CMS content that is shared for external use. - Give guest users access to public Chatter API requests
Relevant for community-forum implementations, where you are leveraging the OOB chatter-based components like Featured Topics & Feeds - Let guest users see other members of this site
Disabling this would completely lock down guest user access to member details, however, if guest users need to view public discussions for a community-forum implementation you need this setting to be enabled, if not, the users don’t see any posts as shown below.
It’s important to understand these nuances, so in this case, even if you enable this setting, you can make the member profile page private or use guest audience variations as we mentioned in Rule 3.
Lastly, Always Use Nicknames
This is enabled by default and is recommended since first & last names are considered PII.
NOTE: For the next two rules the Salesforce documentation is quite thorough, I have referenced the relevant links, nothing more to add here :)
| Rule #6 – Enable the Appropriate Clickjack Protection for your Site
| Rule #7 Enable the Appropriate Content Security Policy(CSP) Level for your Site
| Rule #8 – Restrict the External Organization-Wide Defaults
As far as possible, keep your org-wide defaults private and open up access as required. Even if it may seem like you can have an open sharing model in the beginning, as your application grows you often have certain scenarios crop up that require data access to be locked down. It’s easier to start with a private model upfront than do it later.
As for the different sharing mechanisms you can utilize, this Salesforce documentation covers it quite well.
| Rule #9 – Enforce Object (CRUD) and Field (FLS) Permissions in your Apex UI Controller Methods
You have probably heard this a million times in the context of Apex general best practices. However, the question I often get is… ‘Do I really need to check granular field-level access? How can this be exploited?’ So let me clarify that with an example.
Let’s say we have a Partner Portal with 3 different personas:
– Partner Member with Standard Access
– Partner Member with Elevated Deal Management Access
– Partner Admin who can manage Partner Account Info & Provision other members
The component you see below is a custom LWC that captures some user details, provides info about the different access types, and has a button to request elevated access. The developer built this component keeping reusability in mind and is leveraging the same LWC and underlying Apex method, enabling/disabling certain features based on the logged-in user as seen below.
The Apex controller method as shown below is quite straightforward:
– All the form elements above are exposed as input parameters
-When it’s not a partner admin, specifically for the access level the same info that was fetched to display is passed as the input, so no changes there.
@AuraEnabled
public static Boolean updateSiteUser(String userId, String fname,String lname, String email, String accessLevel) {
User u = [SELECT FirstName, LastName, Email, Access_Level__c
FROM User WHERE Id= :userId];
u.FirstName = fname;
u.LastName = lname;
u.Email = email;
u.Access_Level__c = accessLevel;
update u;
}
From an access standpoint, only the partner admin has the ‘Delegated External User Administrator’ permission & the developer is using conditional DOM rendering based on the logged-in user profile to show text vs editable input.
From the developer’s perspective, they have done all the right things to lock down access based on the logged-in user, but what gets often missed is that when you have a UI controller public method with the AuraEnabled annotation, the Salesforce framework is making this available to the front-end via the aura site APIs. For any given site you can easily reference all the site APIs by simply inspecting the network tab via the browser dev tools as shown below.
If you zoom into the payload you will notice it exposes your method signature:
Once I have the above information, I can easily make this same request externally through a tool like Postman, manipulating the payload to get elevated access. So in this case the Partner Member can manage their own access level which is not what the developer intended!
The important parameters in the above request
- aura.message: The action to be performed, you can execute multiple methods in the same call. This is what you manipulate to exploit a potential security misconfiguration.
- aura.context: The current session’s context, the two important parameters here are:
-fwuid
-siteforce:communityApp - aura.pageURI: The path to the site without the host. for e.g. ‘/s/userprofile’ or just ‘/s’ would also work.
- aura.token: The current user’s token. ‘undefined’ indicates a guest user.
NOTE: If you wish to test this yourself, here is the postman collection. To make this work, you need to set the sid cookie in the postman request. You can copy the sid from the current session in your browser(Dev Tools->Application Tab->Storage->Cookies). This link also contains all the API requests referenced in this article.
Enforcing Object and Field Permissions
Until recently this was quite cumbersome to do natively in apex, but now with these new methods, you have a lot simpler options to enforce these permissions.
- Filter SOQL Queries Using WITH SECURITY_ENFORCED
- Security.stripInaccessible() Method
- Enforce User Mode for Database Operations (In Beta as of Sep 2022)
If you need granular field level handling for these checks use the Schema.DescribeSObjectResult isAccessible, isCreateable, or isUpdateable Methods as shown here.
| Rule #10 – Data Accessible via the User Interface & API Should be Consistent
In the previous rule, the developer made the assumption that everything is locked down from a UI standpoint and there are no other means to perform the same operation which we saw, wasn’t true. Let’s look at another example to reinforce the need for this rule further.
In this case, you have an existing implementation that utilizes the OOB User-profile component. A new requirement has come your way as part of the integration with Marketing Cloud which requires you to copy over some information from User to Contact since on Marketing Cloud you have the Contact Id as the unique subscriber key.
As for the implementation, you have done the following
- Added the PII user fields to the EPIM fieldset
- Provided edit access to the contact fields that are being copied over
- Enforced CRUD & FLS checks in your Apex User Trigger
– In this case, there was already a user trigger in place with considerable business logic so you didn’t use a flow to avoid another entry point - There are no site pages that expose or work with the contact record
Unlike the previous example, since there is no UI controller involved, none of your logic of copying over the fields is exposed. At this stage, everything seems to be quite solid from a security standpoint, so what could go wrong?
Just as you build custom components, the OOB Lightning Components you leverage also work with underlying Salesforce APIs such as the UI API to fetch data. These Salesforce APIs strictly comply with Object CRUD, FLS & Record Visibility granted to the running user. In this case, since you have granted the running user access to the Contact fields, it can be exploited using the same concept as before, the only difference is now we are working with a native Salesforce API, specifically the UI search API in this example.
As seen in this screenshot, the user John can now fetch his own contact record via an API call. Although this potentially exposes some internal fields that the user shouldn’t access, from a security standpoint it isn’t too bad since they are viewing their own information.
But what if I tell you this can be further exploited?
Let’s move to the next rule to understand how and we will also discuss the potential fix for this issue.
| Rule #11 – Review the Impact of Implicit Sharing
Considering Implicit Sharing often gets missed when it comes to external sharing for site users.
Site or Portal Implicit Sharing provides access to a site or portal account and all associated contacts for all site or portal users under that account.
*Shared to the lowest role under the site or portal account.
So in the example below, John would have access to the GSCloudSolutions Account, his own Contact as well as Jane’s Contact.
So in the example above, through the search API, John can not only see his own data but also query for Jane’s contact info which is a data leak and a much bigger problem!
The fix for the issues highlighted in the example above is understanding that this is a backend operation that doesn’t really need to run in the user mode & can run in system mode without giving the running user access to the contact fields. Even if you use flows instead of Apex you have an option to run flows in the system mode.
NOTE: As you go through the examples in rules 9, 10 & 11, the important thing to remember is that ‘Rule 10: Data Accessible via the User Interface & API should be Consistent’ always takes precedence & should drive your design decisions!NOTE: For the example referenced in Rule 10 & 11, if there was a need to expose the contact record on the UI via OOB Lightning Components like record-detail, be cognizant of the fact that the running user will need access to the relevant contact fields and if the intention was just to give them access to their own contact record, through implicit sharing there could be a data-leak via APIs. Unfortunately as stated in Rule-4 if EPIM was supported on Contact you could have controlled access to sensitive fields but until then this is limitation. Vote for this idea!
| Rule #12 – Use ‘Without Sharing’ Sparingly & with Necessary Safeguards
Although ‘Without Sharing’ should be the last resort, there are certain scenarios where you may need to use them, e.g. with the new guest user security policies, a multi-step self-registration process that couldn’t be done as a post-login flow, may require some of the logic to run in a without-sharing mode as the guest user can’t have update-permission on Objects.
When using ‘Without Sharing’ make sure you limit the record exposure by restricting it to a specific use case or in this example a one-time update operation within a set time limit required to complete the registration process. Always remember, a UI controller method exposed in the ‘Without Sharing’ mode is a dangerous combination so always test for potential data leaks!
I will conclude with this message — Keeping your data secure is a joint effort between You and Salesforce. Salesforce is doing its bit; we need to do ours!