How Security Bypass in Salesforce SOQL Queries Happens in Apex and How to Fix It
Introduction
In the ProductController.cls file, a critical authorization bypass vulnerability allowed attackers to circumvent Salesforce's security model. The getProducts method at line 53 constructed dynamic SOQL queries and executed them using Database.countQuery() without enforcing user-level security context. Because the method was annotated with scope='global' and exposed through an Experience Cloud LWC component, even unauthenticated guest users could invoke it to retrieve sensitive product record counts and data—bypassing both field-level security (FLS) and object-level security (OLS) checks entirely.
This wasn't a subtle logic flaw. The vulnerable code pattern is a well-known security anti-pattern in Salesforce development: executing queries without the WITH USER_MODE clause, which tells Salesforce to enforce the current user's permissions at query time.
The Vulnerability Explained
What Went Wrong
The vulnerable code in ProductController.cls at line 56 looked like this:
result.totalItemCount = Database.countQuery(
'SELECT count() FROM Product__c ' + whereClause
);
Notice what's missing: there's no WITH USER_MODE clause. This means Salesforce's security enforcement is disabled for this query. The whereClause variable is built from user-supplied filter inputs (like product category or price range), but the real problem isn't the dynamic SOQL itself—it's that the query runs with no security context whatsoever.
Here's the full context from the vulnerable code:
@AuraEnabled(cacheable=true, scope='global')
public static ProductSearchResult getProducts(
String whereClause,
Integer pageSize,
Integer pageNumber
) {
ProductSearchResult result = new ProductSearchResult();
result.pageSize = pageSize;
result.pageNumber = pageNumber;
result.totalItemCount = Database.countQuery(
'SELECT count() FROM Product__c ' + whereClause // ← VULNERABLE: No WITH USER_MODE
);
result.records = Database.query(
'SELECT Id, Name, MSRP__c, Description__c, Category__c, Level__c, Picture_URL__c, Material__c FROM Product__c ' +
whereClause // ← ALSO VULNERABLE
);
return result;
}
The Attack Scenario
An attacker with access to the Salesforce Experience Cloud site (or even a guest user, thanks to scope='global') performs these steps:
- Discovers the LWC component that calls
ProductController.getProducts() - Calls the method directly via browser console or a custom script, passing filter conditions
- Receives the total count and records even though:
- They lack read access to theProduct__cobject
- Their user profile has no FLS permissions for sensitive fields likeMSRP__c
- The data should be restricted by their organization's security model
Without WITH USER_MODE, Salesforce treats this query as if the caller is a system administrator—no restrictions apply.
Real-World Impact
- Data Exposure: Competitors or malicious insiders learn product pricing, inventory counts, and other sensitive metadata
- Compliance Violations: PII or restricted data becomes accessible to unauthorized users, violating GDPR, HIPAA, or industry regulations
- Privilege Escalation: A guest user gains visibility into data that should only be available to authenticated employees
The Fix
The fix is surgical and specific: add the WITH USER_MODE clause to both the Database.countQuery() and Database.query() calls.
Before (Vulnerable)
result.totalItemCount = Database.countQuery(
'SELECT count() FROM Product__c ' + whereClause
);
result.records = Database.query(
'SELECT Id, Name, MSRP__c, Description__c, Category__c, Level__c, Picture_URL__c, Material__c FROM Product__c ' +
whereClause
);
After (Fixed)
result.totalItemCount = Database.countQuery(
'SELECT count() FROM Product__c ' + whereClause + ' WITH USER_MODE'
);
result.records = Database.query(
'SELECT Id, Name, MSRP__c, Description__c, Category__c, Level__c, Picture_URL__c, Material__c FROM Product__c ' +
whereClause + ' WITH USER_MODE'
);
Why This Works
The WITH USER_MODE clause tells Salesforce to enforce the current user's permissions at query execution time. Now:
- FLS is enforced: If a user lacks read access to the
MSRP__cfield, the query returnsnullfor that field - OLS is enforced: If a user lacks read access to the
Product__cobject, the query returns zero records - Sharing rules apply: Record-level sharing rules and org-wide defaults are respected
The fix is backward-compatible: valid users continue to access the data they're authorized to see. Invalid users get empty results instead of full access.
Prevention & Best Practices
1. Always Use WITH USER_MODE in SOQL Queries
Make this a code review requirement. Every Database.query() and Database.countQuery() call should include WITH USER_MODE unless there's an explicit, documented reason not to (e.g., a scheduled batch job that needs system-level access, which should use a separate service class without with sharing).
2. Use with sharing on Apex Classes
The ProductController class should be declared as:
public with sharing class ProductController {
// ...
}
The with sharing keyword tells Salesforce to enforce sharing rules, but it only works if your SOQL queries also use WITH USER_MODE.
3. Restrict scope='global' Carefully
The @AuraEnabled(cacheable=true, scope='global') annotation makes this method callable from any context, including guest users. If this data should only be visible to authenticated users, change it to:
@AuraEnabled(cacheable=true, scope='com.salesforce.wave')
Or remove scope='global' entirely to default to the current user's scope.
4. Validate and Sanitize whereClause
While WITH USER_MODE prevents authorization bypass, the whereClause parameter should still be validated to prevent SOQL injection. Use parameterized queries or whitelist allowed filter fields:
// Instead of concatenating user input directly:
String whereClause = 'WHERE Category__c = \'' + category + '\''; // ← RISKY
// Use parameterized approach:
String whereClause = 'WHERE Category__c = :category';
result.records = Database.query(
'SELECT Id, Name FROM Product__c ' + whereClause + ' WITH USER_MODE'
);
5. Use Static Analysis Tools
Enable Salesforce security scanners like:
- Salesforce Code Analyzer (built into VS Code)
- PMD with Salesforce rules
- Orbis AppSec (which detected this vulnerability)
These tools flag missing WITH USER_MODE clauses and other authorization issues automatically.
6. Security Testing
Add unit tests that verify authorization enforcement:
@isTest
static void testGetProductsRespectsFLS() {
// Create a user with limited FLS permissions
User limitedUser = createUserWithoutMSRPAccess();
System.runAs(limitedUser) {
ProductSearchResult result = ProductController.getProducts('', 10, 1);
// Assert that MSRP__c is null or not returned
for (SObject record : result.records) {
System.assertEquals(null, record.get('MSRP__c'),
'MSRP__c should be null for users without FLS');
}
}
}
Key Takeaways
- Never execute SOQL without
WITH USER_MODEunless you have a documented security justification (e.g., a system service account with explicitwithout sharing) - The
ProductController.getProducts()method was vulnerable because it lacked security context, allowing guest users to bypass FLS/OLS through a globally-scoped@AuraEnabledmethod - One-line fix, massive security impact: Adding
WITH USER_MODEto line 56 restored Salesforce's authorization model with sharingalone is insufficient—you must pair it withWITH USER_MODEin SOQL queries for defense-in-depth- Scope your
@AuraEnabledmethods carefully—scope='global'should be reserved for truly public, unauthenticated operations
How Orbis AppSec Detected This
Source: The whereClause parameter passed to the getProducts() method from an LWC component, combined with the scope='global' annotation allowing guest user access.
Sink: The Database.countQuery() call at line 56 and Database.query() call at line 59, both executing SOQL without the WITH USER_MODE clause.
Missing control: No security context enforcement at query execution time; Salesforce's FLS/OLS checks are bypassed because the query lacks the WITH USER_MODE clause.
CWE: CWE-639 (Authorization Bypass Through User-Controlled Key) and CWE-276 (Incorrect Default Permissions).
Fix: Added + ' WITH USER_MODE' to both the Database.countQuery() and Database.query() calls, enforcing the current user's permissions at query time.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Authorization bypass vulnerabilities in Salesforce are particularly dangerous because they silently expose data to unauthorized users—there's no error message, just silent data leakage. The ProductController.getProducts() method is now fixed, but this incident underscores a critical principle: security must be enforced at every layer.
In Salesforce, that means:
- Declare classes with with sharing
- Add WITH USER_MODE to every SOQL query
- Restrict @AuraEnabled scope appropriately
- Test authorization enforcement in your unit tests
By adopting these practices, you'll prevent authorization bypass vulnerabilities before they reach production. Security isn't a feature you add at the end—it's a foundation you build into every query.