Skip to main content

Write Secure Apex Controllers

Learning Objectives

After completing this unit, you’ll be able to:

  • Describe why sharing rules are crucial in Apex.
  • Enforce sharing rules.
  • Explain how to protect against create, read, update, and delete (CRUD) and field-level security (FLS) violations.

Apex Security and Sharing

When you use Apex, the security of your code is critical. By default Apex classes have the ability to read and update all data within an organization. Therefore, it’s crucial that you enforce sharing rules, set object and field permissions, and protect against CRUD and FLS. Apex sharing rules are used to determine the “execution context” under which your code executes. You will need to determine which code should be run as “system mode”—that is, with access privileges to many resources—and which code should be run as “user mode,” in which the permissions, field-level security, and sharing rules of the current user are enforced.

Enforcing Sharing Rules

Developers who use Apex need to ensure they don’t inadvertently expose sensitive data that would normally be hidden from users by user permissions, field-level security, or organization-wide defaults. Careful attention should be paid to web services that can be restricted by permissions but execute in system context once they are initiated.

Apex generally runs in system context. In system context, Apex code has access to all objects and fields—object permissions, field-level security, and sharing rules aren’t applied for the current user. This strategy ensures that code doesn’t fail to run because of hidden fields or objects for a user.

Sharing rules, however, aren’t always bypassed: The class must be declared with the without sharing keyword in order to ensure that sharing rules are not enforced. Most of the time, system context provides the correct behavior for system-level operations such as triggers and web services that need access to all data in an organization. However, you can also specify that particular Apex classes should enforce the sharing rules that apply to the current user. 

There are three keywords to remember for sharing rules. You use the with sharing or without sharing keywords on a class to specify whether sharing rules must be enforced. You use the inherited sharing keyword on an Apex class to run the class in the sharing mode of the class that called it.

The only exception to the way Apex runs in system context, is Apex code executed with the executeAnonymous call and Chatter in Apex. The executeAnonymous call always executes using the full permissions of the current user. 

With Sharing
The with sharing keyword lets you specify that the sharing rules for the current user are considered for the class. You have to explicitly set this keyword for the class because Apex code runs in system context. 

Without Sharing
You use the without sharing keywords when declaring a class to ensure that the sharing rules for the current user are not enforced. For example, you can explicitly turn off sharing rule enforcement when a class is called from another class that is declared using with sharing.

Inherited Sharing

Apex without a sharing declaration is insecure by default. Designing Apex classes that can run in either with sharing or without sharing mode at runtime is an advanced technique. Such a technique can be difficult to distinguish from one where a specific sharing declaration is accidentally omitted. An explicit inherited sharing declaration makes the intent clear, avoiding ambiguity arising from an omitted declaration or false positives from security analysis tooling.

Using inherited sharing enables you to pass AppExchange Security Review and ensure that your privileged Apex code is not used in unexpected or insecure ways. An Apex class with inherited sharing runs as with sharing when used as:

  • An Aura component controller
  • A Visualforce controller
  • An Apex REST service
  • Any other entry point to an Apex transaction

There is a distinct difference between an Apex class that is marked with inherited sharing and one with an omitted sharing declaration. If the class is used as the entry point to an Apex transaction, an omitted sharing declaration runs as without sharing. However, inherited sharing ensures that the default is to run as with sharing. A class declared as inherited sharing runs as without sharing only when explicitly called from an already established without sharing context.

Note

Be aware that the sharing setting of the class where the method is defined is applied, not of the class where the method is called. This means that if a method is defined in a class declared with with sharing is called by a class declared with without sharing, the method executes with sharing rules enforced. Class-level security is always still necessary. In addition, all Salesforce Object Query Language (SOQL) or Salesforce Object Search Language (SOSL) queries that use Pricebook2 ignore the with sharing keyword. All Pricebook records are returned, regardless of the applied sharing rules.

Enforcing the current user's sharing rules can impact:

  • SOQL and SOSL queries: A query may return fewer rows than it would operating in system context.
  • Data Manipulation Language (DML) operations: An operation may fail because the current user doesn't have the correct permissions—for example, if the user specifies a foreign key value that exists in the organization but which the current user does not have access to.

Enforcing Object and Field Permissions

Although Apex doesn't enforce object-level and field-level permissions by default, you can enforce these permissions in your SOQL queries by using the WITH SECURITY_ENFORCED clause. 

You can also enforce object-level and field-level permissions in your code by explicitly calling the sObject describe result methods (of Schema.DescribeSObjectResult) and the field describe result methods (of Schema.DescribeFieldResult) that check the current user's access permission levels. In this way, you can verify if the current user has the necessary permission and only if they do, can you perform a specific DML operation or query.

For example, you can call the isAccessible, isCreateable, or isUpdateable methods of Schema.DescribeSObjectResult to verify whether the current user has read, create, or update access to an sObject, respectively. Similarly, Schema.DescribeFieldResult exposes these access control methods that you can call to check the current user's read, create, or update access for a field. In addition, you can call the isDeletable method provided by Schema.DescribeSObjectResult to check if the current user has permission to delete a specific sObject.

These are some examples of how to call the access control methods.

To check the field-level update permission of the contact's email field before updating it:

if (Schema.sObjectType.Contact.fields.Email.isUpdateable()) {
   // Update contact email address
}

To check the field-level create permission of the contact's email field before creating a new contact:

if (Schema.sObjectType.Contact.fields.Email.isCreateable()) {
   // Create new contact
}

To check the field-level read permission of the contact's email field before querying for this field:

if (Schema.sObjectType.Contact.fields.Email.isAccessible()) {
   Contact c = [SELECT Email FROM Contact WHERE Id= :Id];
}

To check the object-level permission for the contact before deleting the contact.

if (Schema.sObjectType.Contact.isDeletable()) {
   // Delete contact
}

Sharing rules are distinct from object-level and field-level permissions and they can coexist. If sharing rules are defined in Salesforce, you can enforce them at the class level by declaring the class with the with sharing keyword. 

If you call the sObject describe result and field describe result access control methods, the verification of object and field-level permissions is performed in addition to the sharing rules that are in effect. Sometimes, the access level granted by a sharing rule could conflict with an object-level or field-level permission.


Protect Against CRUD and FLS Violations

Object-level security within the salesforce.com environment is referred to as create, read, update, and delete (CRUD) access. CRUD settings are applied at the profile and permission set level and can be used to restrict the actions that users can take on each type of standard and custom object. 

Depending on how your custom applications render and process data, unauthorized users have the potential to access and modify data that they shouldn’t. Luckily the platform makes it easy to prevent unauthorized access.

Let’s walk through the DescribeSObjectResult class helper functions that you can use to verify a user’s level of access. These include: 

  • IsCreateable()
  • IsAccessible()
  • IsUpdateable()
  • IsDeletable()

isCreateable()

Before your code inserts a record in the database, you have to check that the logged-in user has both Edit permission on the field and Create permission on the object. You can check both permissions by using the isCreateable() method on the particular object.

Suppose the user needs to create an opportunity with $500 in the Amount field. To ensure that the user calling the function has authorization to create opportunities and opportunity amounts, your Apex code should perform a check to see if the user has the create permission on isCreateable() permission on Opportunity.Amount. Here’s how you write the code.

if (!Schema.sObjectType.Opportunity.fields.Amount.isCreateable()){
   ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,
   'Error: Insufficient Access'));
   return null;
}
Opportunity o = new Opportunity();
// specify other fields
o.Amount=500;
database.insert(o);

You may notice that in this example, we’re explicitly verifying that the user can create the field, not the object. By checking access to the field, you’re also implicitly checking that the user has created access on the object. So in this case, you check FLS and you also get CRUD verification for free.

isAccessible()

Before your code retrieves a field from an object, you want to verify that the logged-in user has permission to access the field by using the isAccessible() method on the particular object.

Suppose the user wants to access the Expected Revenue field in an opportunity. Your Apex code should check if the user has the edit permission on isAccessible permission on Opportunity.ExpectedRevenue. This is how you would write the code to do this.

// Check if the user has read access on the Opportunity.ExpectedRevenue field 
if (!Schema.sObjectType.Opportunity.fields.ExpectedRevenue.isAccessible()){
   ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Error: Insufficient Access'));
   return null; 
}
Opportunity [] myList = [SELECT ExpectedRevenue FROM Opportunity LIMIT 1000];

isUpdateable()

Similarly, before your code updates a record, you have to check if the logged-in user has Edit permission for the field and the object. You can check for both permissions by using the isUpdateable() method on the particular object.

Suppose the user wants to update an opportunity to mark the stage as Closed Won. Your Apex code should then check if the user has the create permission on isUpdateable() permission on Opportunity.StageName. Let’s check out how this looks.

//Let’s assume we have fetched opportunity “o” from a SOQL query 
if (!Schema.sObjectType.Opportunity.fields.StageName.isUpdateable()){ 
   ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Error: Insufficient Access'));
   return null;
}
o.StageName=’Closed Won’; update o;

isDeletable()

Lastly, to enforce “delete” access restrictions, use the isDeletable() function before your code performs a delete database operation. Here’s how to configure this operation: 

if (!Lead.sObjectType.getDescribe().isDeletable()){
   delete l;
   return null;
}

Notice that unlike update, create, and access, with delete we explicitly perform only a CRUD check, verifying that the user can delete the object. Since you delete entire records in SOQL and don’t delete fields, you need to check only the user’s CRUD access to the object.

Field-Level Security

Field-level security (FLS) is configured similarly to CRUD but lets administrators define the profiles that can see and write to most fields of standard and custom objects.

You use the stripInaccessible method to enforce field- and object-level data protection. This method can be used to strip the fields and relationship fields from query and subquery results that the user can’t access. The method can also be used to remove inaccessible sObject fields before DML operations to avoid exceptions and to sanitize sObjects that have been deserialized from an untrusted source.

You access field- and object-level data protection through the Security and SObjectAccessDecision classes. The access check is based on the field-level permission of the current user in the context of the specified operation: create, read, update, or delete. The stripInaccessible method checks the source records for fields that don’t meet field-level security checks for the current user. The method also checks the source records for look up or master-detail relationship fields to which the current user doesn’t have access. The method creates a return list of sObjects that is identical to the source records, except that the fields that are inaccessible to the current user are removed. The sObjects returned by the getRecords method contain records in the same order as the sObjects in the sourceRecords parameter of the stripInaccessible method. Fields that aren’t queried are null in the return list, without causing an exception.

Note

The ID field is never stripped by the stripInaccessible method to avoid issues when performing DML on the result.

To identify inaccessible fields that were removed, you can use the isSet method. For example, the return list contains Contact object and the custom field social_security_number__c is inaccessible to the user. Because this custom field fails the field-level access check, the field is not set and isSet returns false. This is how it looks.

SObjectAccessDecision securityDecision = Security.stripInaccessible(sourceRecords);
Contact c = securityDecision.getRecords()[0];
System.debug(c.isSet('social_security_number__c')); // prints "false"
Note

The stripInaccessible method doesn’t support AggregateResult SObject. If the source records are of AggregateResult SObject type, an exception is thrown.

Now that you understand how to enforce sharing rules, set object and field permissions, and protect against CRUD and FLS, you can secure your Apex code. Next, we look at how to protect against injection vulnerabilities. 

Resources

Keep learning for
free!
Sign up for an account to continue.
What’s in it for you?
  • Get personalized recommendations for your career goals
  • Practice your skills with hands-on challenges and quizzes
  • Track and share your progress with employers
  • Connect to mentorship and career opportunities