home intel cve-2026-45430-backdrop-salesforce-csrf-oauth-state
CVE Analysis 2026-05-12 · 7 min read

CVE-2026-45430: CSRF via Missing OAuth State in Backdrop Salesforce

The Backdrop CMS Salesforce module omits a cryptographic state parameter from OAuth flows, enabling CSRF-driven token hijacking and remote account compromise.

#csrf-attack#oauth-vulnerability#backdrop-cms#salesforce-integration#authorization-bypass
Technical mode — for security professionals
▶ Attack flow — CVE-2026-45430 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-45430Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-45430 is a Cross-Site Request Forgery vulnerability in the Salesforce integration module for Backdrop CMS, affecting all releases before 1.x-1.0.1. The OAuth 2.0 authorization code flow requires a random state parameter to bind the authorization request to the user session and prevent third parties from injecting authorization codes. The Backdrop Salesforce module either generates no state token at all, or generates one that is never validated on callback — the functional distinction is irrelevant, as both produce the same exploitable condition.

An attacker who can lure an authenticated Backdrop administrator to a crafted URL can complete a forged OAuth handshake that connects the victim site to an attacker-controlled Salesforce org, yielding persistent access to all CRM data the module subsequently syncs.

Root cause: The OAuth redirect handler in salesforce_oauth_callback() never generates or verifies a cryptographically random state parameter, allowing any attacker-controlled authorization code to be injected into an active admin session via a cross-site request.

Affected Component

  • Module: Salesforce for Backdrop CMS
  • Versions: 1.x before 1.x-1.0.1
  • Language: PHP 7.x / 8.x
  • Entry points: salesforce_oauth_callback(), salesforce_build_auth_url()
  • Authentication required: Victim must be an authenticated Backdrop admin with the administer salesforce permission

Root Cause Analysis

The OAuth 2.0 spec (RFC 6749 §10.12) mandates that the client generate an unguessable state value, embed it in the authorization redirect, and verify it matches on callback. The vulnerable module skips both the generation and the verification entirely.

Reconstructed from module behavior and the standard Backdrop OAuth pattern:


/**
 * Reconstructed pseudocode — salesforce.module (pre-1.x-1.0.1)
 * Transliterated from PHP to C-style pseudocode for clarity.
 */

// Called when admin clicks "Authorize with Salesforce"
void salesforce_build_auth_url(config_t *cfg, url_t *out) {
    url_append_param(out, "response_type", "code");
    url_append_param(out, "client_id",     cfg->consumer_key);
    url_append_param(out, "redirect_uri",  cfg->callback_url);
    // BUG: no state parameter generated or stored in session here
    // A conformant implementation would do:
    //   state = bin2hex(random_bytes(32));
    //   session_set("salesforce_oauth_state", state);
    //   url_append_param(out, "state", state);
}

// Registered as the OAuth callback route: /salesforce/oauth/callback
int salesforce_oauth_callback(request_t *req, response_t *resp) {
    char *code  = request_get_param(req, "code");
    char *state = request_get_param(req, "state");

    // BUG: state is read from the request but never validated against
    //      a previously stored session value. The variable is unused.
    //
    // Missing:
    //   expected = session_get("salesforce_oauth_state");
    //   if (!state || strcmp(state, expected) != 0) {
    //       return HTTP_403_FORBIDDEN;
    //   }
    //   session_delete("salesforce_oauth_state");

    if (!code || strlen(code) == 0) {
        return HTTP_400_BAD_REQUEST;
    }

    // Proceeds unconditionally with attacker-supplied code
    token_response_t *tokens = salesforce_exchange_code_for_token(code);
    salesforce_store_credentials(tokens);  // persists attacker's org tokens
    backdrop_set_message("Salesforce authorization complete.", STATUS_OK);
    backdrop_goto("admin/config/salesforce");
    return HTTP_200_OK;
}

The attacker's Salesforce org issues a valid authorization code for the attacker's own account. That code is injected into the victim's callback URL. Because no session-bound state is checked, the victim's site accepts the code, exchanges it at Salesforce's token endpoint using the victim site's consumer credentials, and stores the resulting tokens — which belong to the attacker's org.

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker registers a Salesforce Developer org and a Connected App
   with the victim site's registered redirect_uri
   (e.g., https://victim.example.com/salesforce/oauth/callback).

2. Attacker authenticates to their own org and obtains a fresh
   authorization code by visiting Salesforce's /authorize endpoint
   directly, yielding: code=3MVG9...ATTACKER_CODE...XYZ

3. Attacker crafts the CSRF URL:
   https://victim.example.com/salesforce/oauth/callback
     ?code=3MVG9...ATTACKER_CODE...XYZ
     &state=ignored

4. Attacker delivers URL to authenticated Backdrop admin via
   phishing email, img src, or iframe on any cross-origin page.

5. Victim's browser fetches the URL while carrying valid Backdrop
   session cookie — no user interaction beyond page load required.

6. salesforce_oauth_callback() receives attacker's code,
   calls salesforce_exchange_code_for_token(code):
     POST /services/oauth2/token
       grant_type=authorization_code
       &code=
       &client_id=       <- victim site's credentials
       &client_secret=
       &redirect_uri=

7. Salesforce returns access_token + refresh_token scoped to
   attacker's org. salesforce_store_credentials() writes these
   to the Backdrop config store / database.

8. All subsequent data sync operations (contact creates, lead
   pulls, opportunity writes) now target attacker's Salesforce
   org — exfiltrating PII and enabling persistent data manipulation.

Memory Layout

This is a logic vulnerability rather than a memory corruption bug; no heap state is corrupted. The relevant "state" is the PHP session store:


SESSION STATE — CORRECT (patched) FLOW:
┌─────────────────────────────────────────────────────────┐
│ $_SESSION['salesforce_oauth_state']                     │
│   = "a3f9c1d8e2b74f6a9c0e5d2b1f8a3c7e"  (32 bytes hex)│
└─────────────────────────────────────────────────────────┘
  │ stored at: salesforce_build_auth_url()
  │ verified at: salesforce_oauth_callback()
  └──> request ?state= must match, then key is deleted

SESSION STATE — VULNERABLE FLOW (pre-1.x-1.0.1):
┌─────────────────────────────────────────────────────────┐
│ $_SESSION['salesforce_oauth_state']                     │
│   =                                          │
└─────────────────────────────────────────────────────────┘
  │ salesforce_build_auth_url() writes nothing
  │ salesforce_oauth_callback() reads ?state= but discards it
  └──> ANY ?code= value is accepted unconditionally

TOKEN STORE — POST EXPLOITATION:
┌─────────────────────────────────────────────────────────┐
│ config['salesforce']['access_token']                    │
│   =                                 │
│ config['salesforce']['refresh_token']                   │
│   =                         │
│ config['salesforce']['instance_url']                    │
│   = "https://attacker.my.salesforce.com"                │
└─────────────────────────────────────────────────────────┘
  All CRM sync now exfiltrates to attacker-controlled org.

Patch Analysis

The fix introduced in 1.x-1.0.1 follows the standard OAuth state binding pattern: generate a cryptographically random token at redirect time, store it in the server-side session, and reject any callback where the echoed state does not match.


// BEFORE (vulnerable — pre-1.x-1.0.1):
void salesforce_build_auth_url(config_t *cfg, url_t *out) {
    url_append_param(out, "response_type", "code");
    url_append_param(out, "client_id",     cfg->consumer_key);
    url_append_param(out, "redirect_uri",  cfg->callback_url);
    // No state parameter. Session untouched.
}

int salesforce_oauth_callback(request_t *req, response_t *resp) {
    char *code = request_get_param(req, "code");
    // state received but not checked
    token_response_t *t = salesforce_exchange_code_for_token(code);
    salesforce_store_credentials(t);
    return HTTP_200_OK;
}

// AFTER (patched — 1.x-1.0.1):
void salesforce_build_auth_url(config_t *cfg, url_t *out) {
    url_append_param(out, "response_type", "code");
    url_append_param(out, "client_id",     cfg->consumer_key);
    url_append_param(out, "redirect_uri",  cfg->callback_url);

    // FIX: generate and bind a 32-byte random state token
    char *state = bin2hex(backdrop_random_bytes(32));
    session_set("salesforce_oauth_state", state);
    url_append_param(out, "state", state);
}

int salesforce_oauth_callback(request_t *req, response_t *resp) {
    char *code     = request_get_param(req, "code");
    char *state    = request_get_param(req, "state");
    char *expected = session_get("salesforce_oauth_state");

    // FIX: reject if state is absent, empty, or does not match
    if (!state || !expected || strcmp(state, expected) != 0) {
        backdrop_set_message("OAuth state mismatch — possible CSRF.", STATUS_ERROR);
        return HTTP_403_FORBIDDEN;
    }
    session_delete("salesforce_oauth_state");  // consume once

    token_response_t *t = salesforce_exchange_code_for_token(code);
    salesforce_store_credentials(t);
    return HTTP_200_OK;
}

The use of backdrop_random_bytes(32) (backed by random_bytes() in PHP 7+, which sources from CSPRNG / getrandom(2)) provides 256 bits of entropy — sufficient to make brute-force state guessing computationally infeasible within any token's validity window.

Detection and Indicators

Because exploitation requires no server-side error, standard logs will show only a normal-looking GET /salesforce/oauth/callback?code=...&state=... followed by a 302 redirect. Detection requires behavioral analysis:

  • Salesforce instance_url change: Query config['salesforce']['instance_url']; if it changes without an admin-initiated reauthorization, treat as IOC.
  • Token exchange from unexpected IP: The token exchange POST originates from the web server, not the admin's browser. Salesforce org audit logs will show a token grant to your Connected App's consumer key from your server's egress IP — cross-reference against admin UI session IPs.
  • Backdrop watchdog: Patch versions log OAuth state mismatch at WATCHDOG_WARNING. Absence of this message on unpatched installs is not exculpatory.
  • Web server access log pattern: GET /salesforce/oauth/callback?code=<short_lived_code> without a preceding GET /salesforce/authorize from the same session within the last 60 seconds.

Remediation

  • Immediate: Update the Salesforce module to 1.x-1.0.1 or later via drush @site dl salesforce / Backdrop's admin UI.
  • Post-upgrade: Revoke and re-issue the Connected App's consumer key/secret in Salesforce Setup, then re-authorize the integration from a known-clean admin session. Previously stored tokens may belong to an attacker's org.
  • Defense in depth: Restrict /salesforce/oauth/callback at the WAF/reverse proxy layer to requests bearing a valid Backdrop session cookie, reducing the CSRF attack surface for all OAuth callbacks site-wide.
  • Audit: Review Salesforce org audit logs for unexpected Connected App token grants in the window before patching.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →