Salesforce Policy & Guardrails
Overview
Automated policy enforcement for Salesforce integrations: SOQL injection prevention, API key leak detection, governor limit guardrails, and CI pipeline checks.
Prerequisites
- ESLint configured in project
- jsforce TypeScript project
- CI/CD pipeline with policy checks
- Understanding of Salesforce security model
Instructions
Step 1: SOQL Injection Prevention
// CRITICAL: Never concatenate user input into SOQL strings
// BAD — SOQL injection vulnerability
async function findAccount(name: string) {
return conn.query(`SELECT Id FROM Account WHERE Name = '${name}'`);
// User input: "'; DELETE FROM Account; --"
// Result: SOQL injection (though Salesforce doesn't support DELETE via SOQL,
// user can still extract data with UNION-like techniques)
}
// GOOD — Escape special characters
function escapeSoql(value: string): string {
return value
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/"/g, '\\"')
.replace(/%/g, '\\%')
.replace(/_/g, '\\_');
}
async function findAccountSafe(name: string) {
const safeName = escapeSoql(name);
return conn.query(`SELECT Id, Name FROM Account WHERE Name = '${safeName}'`);
}
// BEST — Use parameterized queries with jsforce
// jsforce doesn't have native parameterized SOQL, so always use escapeSoql()
// For Apex, use bind variables:
// [SELECT Id FROM Account WHERE Name = :accountName]
Step 2: ESLint Rules for Salesforce
// eslint-plugin-salesforce-integration/rules/no-soql-injection.js
module.exports = {
meta: {
type: 'problem',
docs: { description: 'Prevent SOQL injection by detecting string concatenation in query calls' },
},
create(context) {
return {
CallExpression(node) {
// Detect conn.query(`...${variable}...`)
if (
node.callee.property?.name === 'query' &&
node.arguments[0]?.type === 'TemplateLiteral' &&
node.arguments[0].expressions.length > 0
) {
// Check if expressions use the escapeSoql wrapper
for (const expr of node.arguments[0].expressions) {
if (expr.type !== 'CallExpression' || expr.callee?.name !== 'escapeSoql') {
context.report({
node: expr,
message: 'SOQL injection risk: wrap user input with escapeSoql(). Example: `WHERE Name = \'${escapeSoql(userInput)}\'`',
});
}
}
}
},
};
},
};
Step 3: Credential Leak Detection
#!/bin/bash
# pre-commit-salesforce-check.sh
# Detect Salesforce credential patterns in staged files
PATTERNS=(
'00D[a-zA-Z0-9]{15}' # Org ID (shouldn't be hardcoded)
'005[a-zA-Z0-9]{15}' # User ID (context-dependent)
'force://[a-zA-Z0-9]+' # Salesforce login token
'SF_PASSWORD=.' # Password in code
'SF_SECURITY_TOKEN=.' # Security token in code
'SF_CLIENT_SECRET=.' # OAuth client secret in code
)
FOUND=0
for PATTERN in "${PATTERNS[@]}"; do
if git diff --cached --name-only | xargs grep -l "$PATTERN" 2>/dev/null; then
echo "ERROR: Possible Salesforce credential found: $PATTERN"
FOUND=1
fi
done
# Check for .env files being committed
if git diff --cached --name-only | grep -E '\.env$|\.env\.local$|\.env\.prod'; then
echo "ERROR: .env file staged for commit"
FOUND=1
fi
exit $FOUND
Step 4: API Usage Guardrails
// Runtime guardrails preventing API limit exhaustion
class SalesforceGuardrails {
private callsThisMinute = 0;
private lastReset = Date.now();
private maxCallsPerMinute = 50; // Conservative limit
async guard(operation: string, estimatedCalls: number = 1): Promise<void> {
// Reset counter every minute
if (Date.now() - this.lastReset > 60000) {
this.callsThisMinute = 0;
this.lastReset = Date.now();
}
// Per-minute throttle (prevent burst)
if (this.callsThisMinute + estimatedCalls > this.maxCallsPerMinute) {
const waitMs = 60000 - (Date.now() - this.lastReset);
console.warn(`SF guardrail: throttling ${operation}, waiting ${waitMs}ms`);
await new Promise(r => setTimeout(r, waitMs));
this.callsThisMinute = 0;
this.lastReset = Date.now();
}
// Check daily limit before proceeding
const conn = await getConnection();
const limits = await conn.request('/services/data/v59.0/limits/');
const usagePercent = (limits.DailyApiRequests.Max - limits.DailyApiRequests.Remaining) / limits.DailyApiRequests.Max;
if (usagePercent > 0.95) {
throw new Error(`SF guardrail: API usage at ${(usagePercent * 100).toFixed(1)}% — blocking ${operation}`);
}
if (usagePercent > 0.80) {
console.warn(`SF guardrail: API usage at ${(usagePercent * 100).toFixed(1)}%`);
}
this.callsThisMinute += estimatedCalls;
}
}
Step 5: CI Policy Checks
# .github/workflows/salesforce-policy.yml
name: Salesforce Policy Check
on: [push, pull_request]
jobs:
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for SOQL injection risks
run: |
# Detect raw string interpolation in .query() calls
if grep -rn "\.query(\`.*\$\{" --include="*.ts" --include="*.js" src/ | grep -v "escapeSoql"; then
echo "ERROR: Possible SOQL injection — wrap user input with escapeSoql()"
exit 1
fi
- name: Check for hardcoded credentials
run: |
if grep -rE "(SF_PASSWORD|SF_SECURITY_TOKEN|SF_CLIENT_SECRET)\s*=" --include="*.ts" --include="*.js" src/; then
echo "ERROR: Hardcoded Salesforce credentials found"
exit 1
fi
- name: Check for production org IDs
run: |
if grep -rE "00D[a-zA-Z0-9]{15}" --include="*.ts" --include="*.js" --include="*.json" src/; then
echo "WARNING: Hardcoded Salesforce Org ID found — use environment variables"
fi
- name: Verify .gitignore includes sensitive files
run: |
for pattern in ".env" ".env.local" "server.key" "*.pem"; do
if ! grep -q "$pattern" .gitignore; then
echo "ERROR: .gitignore missing '$pattern'"
exit 1
fi
done
Step 6: SOQL Best Practices Enforcement
// Automated SOQL quality checks
function validateSoql(soql: string): { valid: boolean; warnings: string[] } {
const warnings: string[] = [];
// Warn on SELECT FIELDS(ALL) — performance anti-pattern
if (soql.includes('FIELDS(ALL)')) {
warnings.push('Avoid FIELDS(ALL) — select only needed fields');
}
// Warn on missing LIMIT
if (!soql.toUpperCase().includes('LIMIT') && !soql.toUpperCase().includes('COUNT(')) {
warnings.push('Missing LIMIT clause — add LIMIT to prevent hitting 50K row limit');
}
// Warn on LIKE with leading wildcard
if (/LIKE\s+'%/.test(soql)) {
warnings.push("Leading wildcard in LIKE '%...' causes full table scan");
}
// Warn on missing WHERE clause
if (!soql.toUpperCase().includes('WHERE') && !soql.toUpperCase().includes('LIMIT 1')) {
warnings.push('No WHERE clause — query may return too many rows');
}
return { valid: warnings.length === 0, warnings };
}
Output
- SOQL injection prevention with escape function
- ESLint rule detecting injection risks
- Pre-commit hook blocking credential leaks
- Runtime API usage guardrails
- CI pipeline policy checks
Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| ESLint rule false positive | escapeSoql used but not detected | Update rule to check function name |
| Guardrail blocks valid request | Threshold too low | Tune per-minute and daily thresholds |
| Pre-commit hook slow | Too many files | Use lint-staged for incremental checks |
| SOQL injection detected | String concatenation | Apply escapeSoql() wrapper |
Resources
Next Steps
For architecture blueprints, see salesforce-architecture-variants.