
Security.stripInaccessible() Bug with Protected Custom Settings
This post is my attempt to reach out to Salesforce with a full narration of a bug in Security.stripInaccessible() method with protected custom settings only. Also this could help any ISV Partners, who are using Security.stripInaccessible() with protected custom settings and running into issues.
Here is quick summary of the issue:
- Create a Protected Custom setting either of Hierarchy or List Type
- As part of Security Review Compliance, one will either check for CRUD/FLS via Apex Describe Calls or can use the newly released Security.stripInaccessible() Apex utility.
- Add a class lets say Foo.cls, that uses Security.stripInaccessible() to verify Custom Setting access in any Aura, LWC, Batch, or other Apex code.
- Do a managed release or beta package upload i.e. with a package prefix
- Install the managed package in a client org.
- In the client org, invoke Foo.cls and you will get a FALSE Exception from Security.stripInaccessible() despite having READ/WRITE access on the protected custom settings.
Please Note: This issue is not reproducable in the packaging org, it only comes in client/target org after installing the package.
Following video explains the situation
How to reproduce this error?
Install Managed Package
All the source code demonstrated in above vide can be installed via this package link: https://login.salesforce.com/packaging/installPackage.apexp?p0=04t2x000003D7Rb&isdtp=p1 Or deploy this video’s source from Github to a new Developer Edition org, and upload a managed package as released/beta after registering a prefix. You can check out this repo, and use normal SFDX development tooling/flows to deploy this code to your own org.
Setup a Target Org
- Assign the Accessor permission set to get the required permissions on Classes and Custom Settings.
- Create a class with the following source code, in case you deployed the Github source to your own org, replace the “striptest” package prefix in the following code, with your own namespace prefix.
public class AccessorPlayGround {
public static void populate(){
//populate sample data
striptest.Accessor.populateAll();
}
public static void assertAccess() {
// Generate classic CRUD FLS report
String accessReport = striptest.Accessor.generateClassicAccessReport();
System.debug (accessReport);
try {
striptest.Accessor.accessProtectedHierarchy(AccessType.READABLE);
System.debug (' ProtectedHierarchy is READABLE');
} catch (Exception ex) {
System.debug (' ProtectedHierarchy ' + ex.getMessage() + '\n' + ex.getStackTraceString());
}
try {
striptest.Accessor.accessProtectedList(AccessType.READABLE);
System.debug (' ProtectedList is READABLE');
} catch (Exception ex) {
System.debug (' ProtectedList ' + ex.getMessage() + '\n' + ex.getStackTraceString());
}
try {
striptest.Accessor.accessPublicHierarchy(AccessType.READABLE);
System.debug (' PublicHierarchy is READABLE');
} catch (Exception ex) {
System.debug (' PublicHierarchy ' + ex.getMessage() + '\n' + ex.getStackTraceString());
}
try {
striptest.Accessor.accessPublicList(AccessType.READABLE);
System.debug (' PublicList is READABLE');
} catch (Exception ex) {
System.debug (' PublicList ' + ex.getMessage() + '\n' + ex.getStackTraceString());
}
}
}
Execute following code to start observing the debug logs for exception traces regarding Protected Custom Settings
AccessorPlayGround.assertAccess();
If you have configured permission sets correct, the user_debug output in debug logs for the above code line should have something to the following. Observe the bug in Security.stripInaccessible() around Protected Custom Settings only. This is a bug because the CRUD/FLS permissions are given for UPDATE to all 4 custom settings, only the public custom settings work correctly.
00:46:05.2 (147600827)|USER_DEBUG|[10]|DEBUG|
Access Report for Object: striptest__Protected_Hierarchy__c
Object : striptest__Protected_Hierarchy__c is Accessible: true, Createable: true, Updateable: true
Field : Record ID is Accessible: true, Createable: false, Updateable: false
Field : Deleted is Accessible: true, Createable: false, Updateable: false
Field : Name is Accessible: true, Createable: true, Updateable: true
Field : Location is Accessible: true, Createable: true, Updateable: true
Field : Created Date is Accessible: true, Createable: true, Updateable: false
Field : Created By ID is Accessible: true, Createable: true, Updateable: false
Field : Last Modified Date is Accessible: true, Createable: true, Updateable: false
Field : Last Modified By ID is Accessible: true, Createable: true, Updateable: false
Field : System Modstamp is Accessible: true, Createable: false, Updateable: false
Field : Last Viewed Date is Accessible: true, Createable: false, Updateable: false
Field : Last Referenced Date is Accessible: true, Createable: false, Updateable: false
Field : value is Accessible: true, Createable: true, Updateable: true
Access Report for Object: striptest__Public_Hierarchy__c
Object : striptest__Public_Hierarchy__c is Accessible: true, Createable: true, Updateable: true
Field : Record ID is Accessible: true, Createable: false, Updateable: false
Field : Deleted is Accessible: true, Createable: false, Updateable: false
Field : Name is Accessible: true, Createable: true, Updateable: true
Field : Location is Accessible: true, Createable: true, Updateable: true
Field : Created Date is Accessible: true, Createable: true, Updateable: false
Field : Created By ID is Accessible: true, Createable: true, Updateable: false
Field : Last Modified Date is Accessible: true, Createable: true, Updateable: false
Field : Last Modified By ID is Accessible: true, Createable: true, Updateable: false
Field : System Modstamp is Accessible: true, Createable: false, Updateable: false
Field : Last Viewed Date is Accessible: true, Createable: false, Updateable: false
Field : Last Referenced Date is Accessible: true, Createable: false, Updateable: false
Field : Value is Accessible: true, Createable: true, Updateable: true
Access Report for Object: striptest__Protected_List__c
Object : striptest__Protected_List__c is Accessible: true, Createable: true, Updateable: true
Field : Record ID is Accessible: true, Createable: false, Updateable: false
Field : Deleted is Accessible: true, Createable: false, Updateable: false
Field : Name is Accessible: true, Createable: true, Updateable: true
Field : Location is Accessible: true, Createable: true, Updateable: true
Field : Created Date is Accessible: true, Createable: true, Updateable: false
Field : Created By ID is Accessible: true, Createable: true, Updateable: false
Field : Last Modified Date is Accessible: true, Createable: true, Updateable: false
Field : Last Modified By ID is Accessible: true, Createable: true, Updateable: false
Field : System Modstamp is Accessible: true, Createable: false, Updateable: false
Field : Last Viewed Date is Accessible: true, Createable: false, Updateable: false
Field : Last Referenced Date is Accessible: true, Createable: false, Updateable: false
Field : Value is Accessible: true, Createable: true, Updateable: true
Access Report for Object: striptest__Public_List__c
Object : striptest__Public_List__c is Accessible: true, Createable: true, Updateable: true
Field : Record ID is Accessible: true, Createable: false, Updateable: false
Field : Deleted is Accessible: true, Createable: false, Updateable: false
Field : Name is Accessible: true, Createable: true, Updateable: true
Field : Location is Accessible: true, Createable: true, Updateable: true
Field : Created Date is Accessible: true, Createable: true, Updateable: false
Field : Created By ID is Accessible: true, Createable: true, Updateable: false
Field : Last Modified Date is Accessible: true, Createable: true, Updateable: false
Field : Last Modified By ID is Accessible: true, Createable: true, Updateable: false
Field : System Modstamp is Accessible: true, Createable: false, Updateable: false
Field : Last Viewed Date is Accessible: true, Createable: false, Updateable: false
Field : Last Referenced Date is Accessible: true, Createable: false, Updateable: false
Field : Value is Accessible: true, Createable: true, Updateable: true
....
....
00:46:05.2 (176462805)|USER_DEBUG|[16]|DEBUG| ProtectedHierarchy No access to entity
Class.System.Security.stripInaccessible: line 15, column 1
Class.System.Security.stripInaccessible: line 10, column 1
Class.striptest.Accessor.assertAccess: line 177, column 1
Class.striptest.Accessor.accessProtectedHierarchy: line 65, column 1
Class.abhinav.AccessorPlayGround.assertAccess: line 13, column 1
AnonymousBlock: line 1, column 1
AnonymousBlock: line 1, column 1
....
....
00:46:05.2 (184833029)|USER_DEBUG|[23]|DEBUG| ProtectedList No access to entity
Class.System.Security.stripInaccessible: line 15, column 1
Class.System.Security.stripInaccessible: line 10, column 1
Class.striptest.Accessor.assertAccess: line 177, column 1
Class.striptest.Accessor.accessProtectedList: line 43, column 1
Class.abhinav.AccessorPlayGround.assertAccess: line 20, column 1
AnonymousBlock: line 1, column 1
AnonymousBlock: line 1, column 1
....
....
00:46:05.2 (203847426)|USER_DEBUG|[28]|DEBUG| PublicHierarchy is READABLE
....
....
00:46:05.2 (215334265)|USER_DEBUG|[35]|DEBUG| PublicList is READABLE
Possible Workarounds?
Avoid Security.stripInaccessible() only for custom settings by checking it dynamically, as shown below
public static void assertAccess(AccessType accessType, SObject[] records){
Schema.DescribeSObjectResult dsr = records[0].getSObjectType().getDescribe();
String objectName = dsr.getLabel();
if (dsr.isCustomSetting()) {
/*
Security.stripInaccessible(...) doesnt works correctly with protected custom settings
*/
Boolean hasAccess = false;
switch on accessType {
when READABLE {
hasAccess = dsr.isAccessible();
}
when CREATABLE {
hasAccess = dsr.isCreateable();
}
when UPDATABLE {
hasAccess = dsr.isUpdateable();
}
when UPSERTABLE {
hasAccess = dsr.isUpdateable() && dsr.isCreateable();
}
when else {
hasAccess = false;
}
}
if (hasAccess == false) {
String msg = String.format('"{0}" access missing on Object: "{1}"',
new String[]{
accessType.name(), objectName
});
throw new YourException(msg);
}
} else {
// Normal Object
try {
// Strip fields that are not updatable
SObjectAccessDecision decision = Security.stripInaccessible(accessType, records);
.. usual code
}
Managed Beta Upload Failure
With protected custom settings and this Github source, I ran into an issue of internal Salesforce error while uploading a Managed Beta package. I managed to get out of this issue, by doing a managed release upload. I know it might not be possible to always skip Beta packages. This issue happened with me and my peer developer as well, though after uploading a released package the beta packages are uploading as well 🙁
Update 24-July-2020
I shared the blog in a support case with Salesforce, and they acknowledged this problem as a known issue. If you are impacted by this issue as well, You can click on the button – “This issue affects me” on this article to get the latest updates on this issue.

References
- Managed Package Source Code: https://github.com/abhinavguptas/sf-managed-package-custom-setting-stripInaccessible
- YouTube Video Walkthrough: https://youtu.be/fKCly_WeBc8
- Update 24-July-2020: Salesforce reported this as a Known Issue.