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.
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.
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.
As you work through your personas & permission assignments, here are some of the principles you can apply:
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 |
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:
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.
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
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.
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.
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.
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
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!
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.
These apply to all the sites in your org.
Every setting here is very well documented so I won’t go into the details but will call out a few.
NOTE: Please do not clone the standard profiles and use them as-is, yes, you have bypassed this setting but it defeats the purpose.
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.
Site Preferences(Site → Workspaces → Administration → Site Preferences)
Authenticated User Preferences
Guest User Preferences
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 :)
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.
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
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.
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.
If you need granular field level handling for these checks use the Schema.DescribeSObjectResult isAccessible, isCreateable, or isUpdateable Methods as shown here.
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
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.
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!
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!