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.


References

Frequently Asked Questions

What is authorization bypass in Salesforce SOQL?

Authorization bypass occurs when SOQL queries execute without enforcing Salesforce's field-level security (FLS) and object-level security (OLS), allowing unauthorized users to access restricted data.

How do you prevent authorization bypass in Apex?

Always use the WITH USER_MODE clause in SOQL queries, use the `with sharing` keyword on Apex classes, and validate user permissions explicitly before data access.

What CWE is this vulnerability?

CWE-639 (Authorization Bypass Through User-Controlled Key) and CWE-276 (Incorrect Default Permissions).

Is using `with sharing` enough to prevent this vulnerability?

No. While `with sharing` is necessary, you must also add WITH USER_MODE to SOQL queries to enforce FLS/OLS at query execution time.

Can static analysis detect this vulnerability?

Yes. Security scanners can flag SOQL queries missing WITH USER_MODE, especially in Experience Cloud-accessible methods marked with `scope='global'`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #975

Related Articles

critical

How ReDoS Vulnerabilities Happen in Node.js Express Applications and How to Fix Them

A critical Regular Expression Denial of Service (ReDoS) vulnerability in the path-to-regexp package (CVE-2024-45296) was discovered in the lacartoons-addon project's dependency tree. The vulnerable versions used backtracking regular expressions that could cause catastrophic performance degradation when processing malicious route patterns. Upgrading to patched versions (0.1.10 for Express's internal router) eliminates this attack vector.

critical

How unvalidated URL input handling happens in SvelteKit with Tauri and how to fix it

A critical vulnerability in `src/routes/+page.svelte` allowed attackers to supply arbitrary URLs—including `http://` and local file paths—through query parameters and drag-drop events, which were then fetched without validation. The fix restricts input to HTTPS-only URLs and removes the dangerous local file fetch path entirely, eliminating both SSRF and local file disclosure attack vectors.

critical

How SQL injection happens in Node.js string interpolation and how to fix it

A critical SQL injection vulnerability was discovered in the `getScript()` method of `src/core/statistics.js`, where the `metadata_id` variable was directly interpolated into DELETE and UPDATE SQL statements without any validation. An attacker controlling this parameter could inject malicious SQL payloads to delete entire tables or exfiltrate sensitive data. The fix implements strict input validation using `parseInt()` and regex patterns to ensure only safe values reach the database queries.

critical

How Missing Rate Limiting Happens in Next.js API Routes and How to Fix It

Three public API endpoints in a Next.js application — `/api/send-review`, `/api/contact`, and `/api/auth` — were deployed without any server-side rate limiting, allowing attackers to flood them with unlimited requests. The `/api/send-review` and `/api/contact` endpoints were especially dangerous because every request triggered an outbound email via Gmail SMTP, making them prime targets for email bombing and quota exhaustion. The fix introduces a lightweight in-memory rate limiter capping each IP

critical

How SQL Injection happens in Node.js MySQL queries and how to fix it

A critical SQL injection vulnerability was discovered in `divisible_asset.js` where `message_index` and `output_index` values from external payment data were directly interpolated into SQL queries without proper escaping. This fix applies `conn.escape()` to these parameters, preventing attackers from manipulating database queries through crafted payment elements.