Secure Coding in Salesforce: SOQL Injection, XSS/CSRF, and Secrets Hygiene
In Salesforce development — whether you’re working with Apex, LWC, Visualforce, or external integrations — even small mistakes can create serious security risks. Poor handling of user input or secrets can lead to data exposure, code execution, or unauthorized access.
To protect your Salesforce org, there are three essential security pillars every developer must understand:
-
Prevent SOQL Injection – block malicious queries by handling input safely.
-
Stop XSS and CSRF Attacks – safeguard your UI and API from script or request exploits.
-
Follow Secrets Hygiene – store credentials properly and never hard-code them.
Below are the key concepts, best practices, and real-world code examples you can immediately apply in your org.
? SOQL Injection
Core Concept (What & Why)
SOQL injection happens when untrusted user input is directly concatenated into a query string.
Attackers can exploit this to modify query logic, bypass filters, or access restricted records.
To prevent this:
-
Use bind variables whenever possible.
-
If you need dynamic queries, escape user input properly.
-
Always whitelist fields and sorting parameters when building dynamic SOQL.
Real-World Example
Imagine a support form where users can search Accounts by name.
If an attacker submits a string like %’ OR Name LIKE ‘%’ --, your unprotected query might return every Account record in the org.
Bad (Vulnerable):
public with sharing class AccountSearch {
public static List<Account> find(String q) {
String soql = 'SELECT Id, Name FROM Account WHERE Name LIKE \'%' + q + '%\'';
return Database.query(soql); // ❌ unsafe concat
}
}
Good (Bind Variables + Safe Escape):
public with sharing class AccountSearchSafe {
public static List<Account> find(String q) {
// Prefer bind variables
String pattern = '%' + q + '%';
return [SELECT Id, Name FROM Account WHERE Name LIKE :pattern LIMIT 50];
}
public static List<Account> findDynamic(String q) {
// If you must build dynamic SOQL, escape AND still bind where possible
String safe = String.escapeSingleQuotes(q);
String soql = 'SELECT Id, Name FROM Account WHERE Name LIKE \'%' + safe + '%\' LIMIT 50';
return Database.query(soql);
}
}
Bonus Tips:
-
Never concatenate user input directly into
ORDER BYor field names. -
Instead, use a whitelist approach:
-
String sort = (new Set<String>{'Name','CreatedDate'}).contains(userSort) ? userSort : 'Name'; String soql = 'SELECT Id, Name FROM Account ORDER BY ' + sort + ' LIMIT 50';
⚔️ XSS (Cross-Site Scripting)
Core Concept (What & Why)
XSS occurs when untrusted HTML or JavaScript is rendered to a web page.
Salesforce frameworks like LWC and Visualforce automatically escape user input, but vulnerabilities arise when developers manually render unescaped HTML.
Always treat user input as plain text, not as executable HTML.
Real-World Example
Suppose a user leaves a comment containing a malicious <script> tag.
If that input is injected directly into the DOM using innerHTML, it can run scripts to steal session data or modify the page.
Bad (LWC – Dangerous DOM Injection):
// commentViewer.js
import { LightningElement, api } from 'lwc';
export default class CommentViewer extends LightningElement {
@api commentHtml;
renderedCallback() {
this.template.querySelector('.slot').innerHTML = this.commentHtml; // ❌ XSS
}
}
Good (LWC – Render as Text):
// commentViewer.js
import { LightningElement, api } from 'lwc';
export default class CommentViewer extends LightningElement {
@api commentText = '';
}
<!-- commentViewer.html -->
<template>
<div class="slds-text-body_regular">{commentText}</div> <!-- ✅ auto-escaped -->
</template>
<!-- Good: escape is true by default -->
<apex:outputText value="{!$CurrentPage.parameters.msg}" />
<!-- Bad: -->
<apex:outputText value="{!$CurrentPage.parameters.msg}" escape="false" /> <!-- ❌ -->
? CSRF (Cross-Site Request Forgery)
Core Concept (What & Why)
CSRF attacks trick a user’s browser into performing an action they didn’t intend, such as submitting a form or calling an API endpoint.
Salesforce automatically includes CSRF tokens for standard Visualforce, Aura, and LWC actions.
However, for custom REST endpoints or integrations, you must enforce CSRF protection manually.
Real-World Example
An attacker could host a malicious web page that silently sends a POST request to your org’s “mark invoice paid” API.
If your endpoint doesn’t validate a CSRF header token, the browser could execute that request under the victim’s session.
Apex REST with CSRF Header Check (Secure Pattern):
@RestResource(urlMapping='/pay/*')
global with sharing class PaymentRest {
private static String requireCsrf() {
String token = RestContext.request.headers.get('X-CSRF-Token');
if (String.isBlank(token) || !CsrfTokens.verify(token)) {
RestContext.response.statusCode = 403;
throw new AuraHandledException('Invalid CSRF token');
}
return token;
}
@HttpPost
global static void markPaid() {
requireCsrf();
// ... process safely ...
}
}
public with sharing class CsrfTokens {
// Issue/verify tokens tied to user + expiry.
// Store the secret in Protected Custom Metadata (see next section).
public static Boolean verify(String token) {
// Minimal demo: verify HMAC + age
try {
Map<String,String> parts = (Map<String,String>)JSON.deserializeUntyped(EncodingUtil.base64Decode(token).toString());
Datetime ts = Datetime.valueOf(parts.get('ts'));
if (Datetime.now().getTime() - ts.getTime() > 15*60*1000) return false; // 15 min window
String expected = hmac(parts.get('nonce') + ':' + parts.get('ts'));
return expected == parts.get('mac');
} catch (Exception e) {
return false;
}
}
private static String hmac(String msg) {
String secret = SecretProvider.csrfSecret(); // ? from protected storage
Blob mac = Crypto.generateMac('HmacSHA256', Blob.valueOf(msg), Blob.valueOf(secret));
return EncodingUtil.convertToHex(mac);
}
}
In real applications, issue tokens to clients safely via a GET request or embed them in the page state.
Then, send them back on every modifying request inside an X-CSRF-Token header.
Do not rely on cookies alone.
? Secrets Hygiene
Core Concept (What & Why)
Sensitive credentials like API keys, client secrets, and signing keys should never appear in your code, logs, or version control.
Hard-coded secrets can be easily leaked, so they must be stored securely in Salesforce-managed facilities.
Use:
-
Named Credentials for external API callouts (handles authentication securely).
-
Protected Custom Metadata or Settings for internal app secrets.
-
Avoid logging or exposing secrets in any way.
Rotate keys periodically and assign only the minimal permissions required.
Real-World Example
Let’s say you integrate with a payment gateway.
Instead of embedding the API key in your Apex class, you can use a Named Credential (best practice) or fetch the key from Protected Custom Metadata.
Using Named Credential (Preferred):
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Payments_NC/v1/charges'); // ? secrets managed in Setup
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(payload));
HTTPResponse res = new HTTP().send(req);
Using Protected Custom Metadata for App Secrets:
public with sharing class SecretProvider {
public static String csrfSecret() {
// Protected Custom Metadata record: Security_Config__mdt
return Security_Config__mdt.getInstance('Default').CsrfSecret__c;
}
}
public with sharing class ApiClient {
public static void callWithKey() {
String key = Security_Config__mdt.getInstance('Default').Payments_ApiKey__c; // protected
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/v1/charges');
req.setMethod('POST');
req.setHeader('Authorization', 'Bearer ' + key); // do not log!
// ...
}
}
Hygiene Checklist:
✅ Use Named Credentials for callouts (OAuth/JWT).
✅ Use Protected Custom Metadata/Settings for app secrets.
✅ Never log or email sensitive data.
✅ Rotate keys and restrict scope.
✅ Use pre-commit hooks or secret scanners to block accidental exposure.

? Short Summary
To keep Salesforce apps secure and compliant:
-
Always bind or escape input to prevent SOQL injection.
-
Never render untrusted HTML or disable escaping — that’s how XSS happens.
-
Use custom header tokens for REST endpoints to block CSRF attacks.
-
Store secrets outside code, using Named Credentials or Protected Metadata.
These simple patterns dramatically harden your Salesforce org — without slowing down your development workflow.
