At Dreamforce this last year (2019) Chris Peterson and I gave a theater presentation on combining the new Apex Security.stripInaccessible
method with the existing ESAPI library for enforcing Create, Retrieve, Update, Delete (CRUD) and field level security (FLS) in Apex.
Unfortunately the Dreamforce theater sessions weren't recorded in 2019 as they were in previous years 🤦♂️. To make up for that, below are the session slides. I'll also expand on the key points in this blog post. These will be my words rather than Chris's, although I'll try and cover most of the same content.
Slides from #DF19: Reducing the cost of enforcing FLS & CRUD in the ESAPI https://t.co/T0Cl0MscJv with @FishOfPrey
— Chris Peterson (@ca_peterson) November 21, 2019
What is the ESAPI?
The ES stands for Enterprise Security. And the API... Well, hopefully you know what an API is.
The Salesforce ESAPI is a port of a Java library created by OWASP (Open Web Application Security Project).
To address why you would want it in your Salesforce org, here is a quote from the OWASP ESAPI project page:
The ESAPI libraries are designed to make it easier for programmers to retrofit security into existing applications. The ESAPI libraries also serve as a solid foundation for new development.
These seem like noble goals. We want to improve the security of existing applications and implement newer applications with the same level of security from the start.
The three core areas that the Salesforce ESAPI addresses are:
- Input Validation - is a given string a valid date? Is it a valid credit card number? A valid redirection URL?
- E.g. ESAPI.validator.SFDC_isValidDate
- Output Encoding - is is safe to render the content back to the users browser via HTML?
- E.g. ESAPI.encoder.SFDC_URLENCODE
- Access Control - enforce the built in access control mechanisms: CRUD, FLS, and Sharing.
- insertAsUser/updateAsUser
- DML on a limited set of fields
- Override sharing for a single DML operation
(Re) Introducing the ESAPI
Salesforce originally released their version of the ESAPI library in 2010. In 2016 they added the new ESAPI.encoder.SFDC_BASE64_URLENCODE
method. Other than that it was stalled for any maintenance or new development.
In 2019 Chris Peterson and Jake Meredith from Salesforce took ownership of the Github repo. Even better, they are accepting pull requests.
One of the first steps in rejuvenating the repo was to increase the built in code coverage from 54% to 93% on the security specific test methods. And, perhaps more importantly, add a number of meaningful assertions and negative test cases along the way. Overall project test coverage is now up by 39%.
One of the particular challenges with this was scripting out of the box test cases using only the built in Profiles and sObjects. The test cases needed to be portable to any org, so they couldn't rely on a specific custom Profile existing. At the time the "Read Only" profile was the most restrictive system profile available. Going forward I might revisit this with the still to GA Minimum Access profile.
There is a new Minimum Access profile in #salesforce #summer20.
— Daniel Ballinger 🦈 (@FishOfPrey) April 28, 2020
It may seem like a small thing, but if you've every tried to full control access with permissions sets or had to create such a profile yourself you will know what a useful time saver this will be.#Summer20Treasure pic.twitter.com/SWBMqfSxM3
With the better test coverage in place it was then possible to overhaul how the field and object level security was enforced. More on that later...
A Recap of the Security.stripInaccessible() method
The Spring '20 release included the GA version of the new Security.stripInaccessible
method.
This new method provides a streamlined way to check both field and object level data permissions for the following access types:
- AccessType.READABLE - Check the fields of an sObject for read access.
- AccessType.CREATABLE - Check the fields of an sObject for create access.
- AccessType.UPDATABLE - Check the fields of an sObject for update access.
- AccessType.UPSERTABLE - Check the fields of an sObject for insert and update access.
The SObjectAccessDecision Object
After calling stringInaccessible() an SObjectAccessDecision
object is returned. This provides a number of helpful methods.
getRecords()
provides a new List<SObject>
that has all the inaccessible fields removed. These are also detached from the source sObjects.
Two additional methods getModifiedIndexes()
and getRemovedFields()
provide details about which records and specific fields were modified.
How it works in practice to enforce security requirements
Using the newer stringInaccessible method has a number of advantages. It will cover all possible sObject field types. It will check relationship fields, including nested relationships. Sub-queries and polymorphic lookups are also covered.
The example in the image above shows a new Opportunity for "Appy's App" that was generated in the trusted system mode. As such, it could set the custom Standing__c and Value__c fields. This Opportunity is then passed through stringInaccessible with the AccessType.CREATABLE
parameter. This does all the hard work for us of checking the users Profiles, Permission Sets, the Permission Set groups, the muting permission sets, etc... The output Opportunity has the fields that the user doesn't have create access to completely removed. The stripped fields aren't null: they're really undefined. This is important when it comes to subsequent DML as we don't want to inadvertently clear fields out.
Other advantages:
- It's particularly useful when handling untrusted input (like from JavaScript controllers)
- It's also great for gracefully degrading UI experiences like SFDC does natively
Example walk through code the changes to the ESAPI
The video shows the core structural changes that were made to the ESAPI methods to use the new methods.
Measuring the Performance changes - Methodology
Beyond enforcing the security requirements the next important consideration is changes to CPU Limit usage and Heap usage. Usually you trade more CPU for less Heap or vice versa. If we can cut both down we are doing well!
The general goal is always to see equal of better performance while enforcing the same security requirements. There may be some tradeoffs in performance made, but the security can't be compromised on. The coverage and assertions from the automated tests ensure we are still enforcing the same security requirements.
The performance differences in terms of Apex limits were measured using Test harness classes and Adrian Larsons LimitsProfiler framework. Performing multiple runs between the current/baseline implementation and the new version using the stripping methods allows the relative changes in limits to be measured.
During the testing it was important to have the debug logging completely off as it would otherwise affect the outcomes.
The testing below was done in ALL_OR_NONE mode. This is more demanding than the alternative BEST_EFFORT mode as it requires per field level checks.
To allow for the limits testing framework to repeatedly insert multiple records Savepoints were used as part of the setup and teardown steps. This prevented hitting the storage limits while still measuring the performance differences in the ESAPI.
Measured results for bulk inserting Contacts
The performance difference can vary greatly based on scenario:
- There is negligible performance difference when checking object CRUD permissions. Note, the Apex ESAPI is still enforcing against the sObject Schema.SObjectType until 224 (Spring `20) with the fix.
- There is a significant performance difference if there are a large number of requested fields that aren’t set on all the sObjects.
Measured improvements on 25 iterations inserting 200 Contacts. 33 Standard fields
- 25% Less CPU usage
- 18.6% Less Heap usage