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.
A serious security flaw has been discovered in a tool used by websites built on Backdrop CMS that connect to Salesforce, a popular business software platform. The vulnerability could let hackers trick your company into unknowingly giving them access to your Salesforce accounts and all the sensitive customer data stored there.
Here's what's happening: When you authorize a website to access your Salesforce account, there's supposed to be a secret handshake between your browser and the website to make sure the request is legitimate. Think of it like a bouncer checking both your ID and a special ticket before letting you into a club. In this case, the bouncer is asleep on the job.
The vulnerability means hackers can forge fake authorization requests that look completely legitimate to your company's system. They could trick your employees into clicking a malicious link, and suddenly the attacker has full access to your Salesforce data without anyone realizing it happened. For companies using Salesforce to manage customer information, sales pipelines, or contracts, this is a nightmare scenario.
Small and mid-sized businesses using Backdrop CMS with Salesforce integration are most at risk, especially those without dedicated IT security teams to monitor unusual account activity.
Here's what you should do: First, if you use Backdrop CMS with Salesforce, update the Salesforce module to version 1.0.1 or later immediately. Second, review your recent Salesforce login history to check for any suspicious access you don't recognize. Third, consider enabling two-factor authentication on all Salesforce accounts if you haven't already. This extra security layer would prevent hackers from accessing your data even if they somehow obtained login credentials.
Want the full technical analysis? Click "Technical" above.
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.
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.