Secure Server-Side Development

Write Secure Apex Controllers

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 using Apex, the security of your code is critical
    • By default, Apex classes can read and update all data in the organization, so you need to:
      • 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. It can be:
      • System Mode": access privileges to many resources
      • User Mode": 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 org-wide defaults
    • Apex generally runs in system context, where it has access to all objects and fields
      • Sharing rules, however, aren’t always bypassed, depending on whether an Apex class is declared as with sharing, without sharing, or with inherited sharing
        • with sharing: lets you specify that the sharing rules for the current user are considered for the class
        • without sharing: ensures that the sharing rules for the current user are not enforced
        • inherited sharing: run as with sharing when used as an Aura component controller, a Visualforce controller, an Apex REST service, or any other entry point to an Apex transaction
      • Enforcing the current user’s sharing rules can impact:
        • SOQL and SOSL queries: may return fewer rows than it would operating in system context
        • DML operations: may fail because the current user doesn’t have the correct permissions
  • Enforcing Object and Field Permissions
    • Object-level and field-level permissions can be enforced in SOQL using:
      • WITH SECURITY_ENFORCED clause
      • sObject describe result methods of Schema.DescribeSoObjectResults and field describe results methods of Schema.DescribeFieldResult. Examples follow.
    • Sharing rules are distinct from object-level and field-level permissions and they can coexist - enforece sharing rules at the class level using the with sharing keyword
if (Schema.sObjectType.Contact.fields.Email.isUpdateable()) {
   // Update contact email address
}
if (Schema.sObjectType.Contact.fields.Email.isCreateable()) {
   // Create new contact
}
if (Schema.sObjectType.Contact.fields.Email.isAccessible()) {
   Contact c = [SELECT Email FROM Contact WHERE Id= :Id];
}
if (Schema.sObjectType.Contact.isDeletable()) {
   // Delete contact
}
  • Protect Against CRUD and FLS Violations
    • CRUD: create, read, update, and delete access at the object-level security level.
      • Applied at the profile and permission set level and can be used to restrict the actions that users can take
      • DescribeSObjectResult class helper functions can be used to very a user’s level of access:
        • isCreateable(), isAccessible(), isUpdateable(), isDeleteable()
  • isCreateable()
    • Ex: a user needs to create an Opportunity with $500 in the Amount field
    • To ensure the user calling the function has authorization, the Apex code should check to see if the user has create permission using isCreateable on Opportunity.Amount
      • Note that in this example, verifying FLS (field-level) on the field grants CRUD (object-level) verification for free
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);
  • isAccessible()
    • Ex: user wants to access the Expected Revenue field on an Opportunity
    • Apex code should check if the user has edit permission using isAccessible on Opportunity.ExpectedRevenue
// 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()
    • Ex: user wants to update an opportunity to mark the stage as Closed Won
    • Apex code should check if the user has create permission using isUpdateable() on Opportunity.StageName
//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;
  • isDeleteable()
    • isDeletable() is distinct from the other functions in that since users can’t delete fields, you perform a CRUD check only
if (!Lead.sObjectType.getDescribe().isDeleteable()){
   delete l;
   return null;
}
  • Field-Level Security (FLS)
    • 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
    • stripInaccessible: method use to enforce field- and object-level data protection
      • Strip fields and relationship fields from query and subquery results the user can’t access
      • Can also remove inaccessible sObject fields before DML operations to avoid exceptions and sanitize sObjects that have been deserialized from an untrusted source
    • Access field- and object-level data protection through the Security and SObjectAccessDecision classes
    • To identify inaccessible fields that were removed, can use the isSet method
      • Example below: return list contains the Contact object and the custom field social_security_number__c is inaccessible to the user. In this case, the field is not set and isSet returns false
SObjectAccessDecision securityDecision = Security.stripInaccessible(sourceRecords);
Contact c = securityDecision.getRecords()[0];
System.debug(c.isSet('social_security_number__c')); // prints "false"
  • Apex sharing rules allow you to designate code that should run in User Mode, as opposed to System Mode
  • Salesforce platform builds in protection against CRUD and FLS violations by including helper functions to verify users' level of access

Mitigate SOQL Injection

Define how SOQL differs from SQL. Explain SOQL vulnerabilities. Learn to prevent SOQL injection attacks.
  • Structured Object Query Language (SOQL) versus Structured Query Language (SQL)
    • SOQL is similar to SQL, but is essentially a customized version of SQL developed specifically for the Salesforce platform
      • SOQL is designed exclusively for querying the database, rather than modifying data like in traditional SQL
    • SOQL does not have:
      • INSERT, UPDATE, or DELETE. Only SELECT.
      • Command execution
      • Join statements, though you can include info from parent objects:
        • SELECT Name, Phone, Account.Name FROM Contact
      • UNION operator
      • Ability to chain queries together
  • Is SOQL Vulnerable to Injection Attacks?
    • Yes, it is still possible for a developer to trust user input incorrectly, leading to an exposure of information via a SOQL injection attack
    • Ex: imagine a custom page has been developed to allow users to search through an org’s information to get an overview of personnel at a school district
      • So intended use would be typing in teacher returns a list of people with title “Teacher”
    • Consider the input %' and Performance_rating__c<2 and name like '% into the exploitable query below
      • Since the user input is concatenated into the SOQL query without validation, the attacker can close the single quote for the title parameter and add another condition to the query

Underlying SOQL query used by the application:

String query = 'SELECT Id, Name, Title__c FROM Books';
String whereClause = 'Title__c like \'%'+textualTitle+'%\' ';
List<Books> whereclause_records = database.query(query+' where '+whereClause);

Before:

Title__c like '%'+textualTitle+'%'

After:

Title__c like '% %' and Performance_rating__c<2 and name like '% %'';
  • SOQL Injection Prevention
    • Several techniques can prevent SOQL injection:
      • Static Queries with bind variables
      • String.escapeSingleQuotes()
      • Type casting
      • Replacing characters
      • Allowlisting

  • Static Query and Bind Variables
    • This is the first and most recommended method
    • This step involves translating the query into a static query - see example below
      • If user types a value like test' LIMIT 1 the data base will search for any names that are “test' LIMIT 1” in the database, so with a bind variable the attacker isn’t about to break out and control the SOQL query

Instead of this:

String query = select id from contact where firstname =\’’+var+’\’’;
queryResult = Database.execute(query);

Use something like this, where possible:

queryResult = [select id from contact where firstname =:var]
  • String.escapeSingleQuotes()
    • Salesforce platform provides this escape function that escapes any instance it finds of a single quote mark (') in the string using the backslash () escape character
      • This prevents an attacker’s input from being treated as code by constraining them to the boundary of the string
      • It ensures the user-provided single quote is treated as data rather than code, so the application is no longer vulnerable

Instead of this:

String query = 'SELECT Id, Name, Title__c FROM Books';
String whereClause = 'Title__c like \'%'+textualTitle+'%\' ';
List<Books> whereclause_records = database.query(query+' where '+whereClause);

Use this line instead:

String whereClause = 'Title__c like \'%'+String.escapeSingleQuotes(textualTitle)+'%\' ';
  • Typecasting
    • By casting all variables as strings, user input can drift outside of expectation
      • By typecasting variables as integers or Booleans, when applicable, erroneous user input is not permitted
      • Consider the user input 1 limit 1 in the following query - the resulting query would be Select Name, Role__c, Title__c, Age__c from Personnel__c where Age__c > 1 limit 1
      • Instead, typecast the user input from String to Integer, then wrap the resulting Integer in string.valueOf(), as shown in the second code block
        • User input of 1 limit 1 would throw an error in this scenario
public String textualAge {get; set;} 
[...] 
whereClause+='Age__c >'+textualAge+'';
whereclause_records = database.query(query+' where '+whereClause);
whereClause+='Age__c >'+string.valueOf(textualAge)+'';
  • Replacing Characters
    • This final tool is also known as “blocklisting”, and involves removing “bad characters” from user input
    • Consider the query String query = 'select id from user where isActive='+var;
      • A viable approach would be to remove all spaces from the supplied input, so a SOQL injection payload of true AND ReceivesAdminInfoEmails=true would be returned as trueANDRecievesAdminInfoEmails=true
    • The code to remove all spaces from a string can be written as follows:
String query = 'select id from user where isActive='+var.replaceAll('[^\\w]','');
  • Allowlisting
    • Allowlisting is analogous to blocklisting, but more powerful in that its easier to predict a few good inputs than all possible bad inputs
    • This involves creating a list of all “known good” values that the user is allowed to supply - if they enter anything else, reject the response

  • One way SOQL differs from SQL: disallows wild card fields
  • Injection attacks are still possible while using SOQL: when developers trust user input incorrectly

Mitigate Cross-Site Request Forgery

Define a CSRF vulnerability. Identify a CSRF vulnerability in Lightning Platform applications. Prevent a CSRF vulnerability using code- and org-level protections.
  • What is CSRF?
    • CSRF (Cross-Site Request Forgery) is a common web app vulnerability where a malicious application causes a user’s client to perform an unwanted action on a trusted site for which the user is currently authenticated
    • Consider an application for a school district that has a special URL (such as /promote?UserId=<userid>) that promotes students in the system to the honor roll, subject to two conditions:
      1. Only admins or superintendents can access the page that allows users to promote students to the honor roll
      2. If you click the honor roll link, the page automatically refreshes
      • Behind the scenes the Honor Roll button makes a GET request to /promote?userId=<userid>. Imagine another website has a malicious link that includes the /promote?userId=<userid> as part of the URL
        • This link is executed on behalf of the admin if they are signed in, which promotes a student to the honor roll without them knowing it
    • So, the attacker got the user’s client (browser) to perform an unwanted action (advancement of a student to the honor roll) on a trusted site (School District Management app) for which the user is currently authenticated

  • Prevent CSRF Attacks
    • Performing a CSRF attack is not trivial - the attacker needs to know the URL parameter name (userId, above) as well as the associated values (<userid>, above)
    • One way to prevent these attacks is to design the “promote” URL to require two parameters: userId and token, where token is a random, unique value that changes on every request

  • Use the Salesforce Platform to Protect Against CSRF
    • Salesforce includes out-of-the-box protection against CSRF for developers - requests made against Salesforce resources have CSRF tokens attached to them by default
    • Beyond this, Lighting application developers can:
      1. Avoid the use of state-changing HTTP GET requests, and use POST or PUT instead when state changes are needed
      2. When endpoints are hit in your API, validate the origin header. The origin header is set on HTTP GET requests and specifies the URL from which the request originated - if it is on the forbidden headers list, it will always return the correct value unless the request initiates from a nonstandard browser or tool
      3. When you integrate your Salesforce Lightning application with a third-party application via API, you may use your own anti-CSRF tokens. These can be added to XMLHttpRequests within Lightning by using setRequestHeader() like this:
var o = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(){
    var res = o.apply(this, arguments);
    var err = new Error();
    this.setRequestHeader('anti-csrf-token', csrf_token);
    return res;
};
  • Which one of these describes how CSRF works? An attacker gets a user’s browser to perform an unwanted action
  • What is a way you can prevent CSRF attacks? Include a unique token in the request