Back to Blog
critical SEVERITY7 min read

How Security Bypass in Salesforce SOQL Queries Happens in Apex and How to Fix It

A critical security vulnerability in the ProductController.cls file allowed unauthorized users to bypass Salesforce's field-level and object-level security by executing unprotected SOQL queries. The fix adds a single `WITH USER_MODE` clause to enforce security checks, preventing guest users and unauthorized callers from accessing sensitive product data.

O
By Orbis AppSec
Published August 5, 2026Reviewed August 5, 2026

Answer Summary

This is a Salesforce Apex security bypass vulnerability (CWE-639: Authorization Bypass Through User-Controlled Key) where the getProducts method in ProductController.cls executed SOQL queries without the WITH USER_MODE clause, bypassing field-level security (FLS) and object-level security (OLS). The fix adds `WITH USER_MODE` to line 56, enforcing Salesforce's security model and preventing unauthorized data access from Experience Cloud guest users and other callers.

Vulnerability at a Glance

cweCWE-639 (Authorization Bypass Through User-Controlled Key)
fixAdd WITH USER_MODE clause to Database.countQuery() and Database.query() calls
riskUnauthorized access to product records, data exposure to guest users
languageApex (Salesforce)
root causeSOQL query executed without WITH USER_MODE clause, bypassing FLS/OLS enforcement
vulnerabilityAuthorization Bypass via Missing Security Context in SOQL

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:

  1. Discovers the LWC component that calls ProductController.getProducts()
  2. Calls the method directly via browser console or a custom script, passing filter conditions
  3. Receives the total count and records even though:
    - They lack read access to the Product__c object
    - Their user profile has no FLS permissions for sensitive fields like MSRP__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__c field, the query returns null for that field
  • OLS is enforced: If a user lacks read access to the Product__c object, 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_MODE unless you have a documented security justification (e.g., a system service account with explicit without sharing)
  • The ProductController.getProducts() method was vulnerable because it lacked security context, allowing guest users to bypass FLS/OLS through a globally-scoped @AuraEnabled method
  • One-line fix, massive security impact: Adding WITH USER_MODE to line 56 restored Salesforce's authorization model
  • with sharing alone is insufficient—you must pair it with WITH USER_MODE in SOQL queries for defense-in-depth
  • Scope your @AuraEnabled methods carefullyscope='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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #975

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.