The 9.1 Severity API Bug That's Still Hitting Production: CVE-2026-35616 and the Authentication Myth
Fortinet's CVE-2026-35616 scores 9.1 CVSS. Active exploitation confirmed. Here's why API authentication bypasses keep shipping—and the three patterns that cause them.
The 9.1 Severity API Bug That's Still Hitting Production: CVE-2026-35616 and the Authentication Myth
On April 4, 2026, Fortinet confirmed what API security researchers have screamed for years: authentication is still the easy button for attackers. CVE-2026-35616, a critical improper access control vulnerability in FortiClient EMS, allows unauthenticated attackers to execute arbitrary code via crafted API requests. CVSS 9.1. Active exploitation observed. Hotfix released for versions 7.4.5 and 7.4.6.
The vulnerability exists because someone treated the API layer as a trusted internal surface—assuming that if you're calling the API, you've already proven your identity. That's not authentication. That's wishful thinking with a CVE number.
This isn't an isolated incident. Auth0's 2025 audit found that over 30% of JWT implementations contained at least one critical vulnerability: accepting unsigned tokens, using brute-forceable secrets, or failing to validate expiration. The attackers know this. They're not hunting for zero-days in crypto primitives. They're exploiting the gap between "we have authentication" and "our authentication actually works."
The FortiClient EMS Timeline
April 4, 2026: FortiGuard PSIRT confirms active exploitation of CVE-2026-35616 in FortiClient EMS 7.4.5 and 7.4.6. CVSS score: 9.1. Impact: privilege escalation via unauthenticated API requests. Hotfixes deployed for both versions. Upcoming 7.4.7 will include the fix. The vulnerability was in the API layer's access control— attackers could bypass authentication checks entirely by sending specially crafted requests to the endpoint.
The Three Authentication Failure Patterns
Pattern 1: Trusting the API Gateway's Auth Without Validating Downstream
The most common mistake. Teams implement authentication at the API gateway, then disable or skip auth checks in downstream services because "the gateway already handled it." This creates a flat trust model where compromising one component compromises everything.
# WRONG: Assuming gateway auth means you don't need it downstream
@app.route('/api/admin/users', methods=['DELETE'])
def delete_all_users():
# Gateway validated the token, we're trusted now
# No role check. No ownership check. Just delete.
db.execute("DELETE FROM users WHERE 1=1")
return {"status": "success"}
The gateway checked credentials. Nobody checked whether the authenticated principal should have admin access to that endpoint. BOLA combined with missing authorization—still one of the most profitable attack chains in 2026.
Pattern 2: JWT Validation That's Half-Assed
// WRONG: Decoding ≠ Validating
func validateToken(tokenString string) bool {
// This only DECODES. It doesn't VERIFY.
token, _ := jwt.Parse(tokenString)
if token == nil {
return false
}
claims := token.Claims.(jwt.MapClaims)
// Checking expiry manually? Most implementations miss this.
if float64(time.Now().Unix()) > claims["exp"].(float64) {
return false // Good, you caught expiry. But is the signature verified?
}
return true
}
The correct implementation requires signature verification with the server's public key, algorithm validation (reject none), and proper error handling that doesn't leak information.
// RIGHT: Full validation with proper error handling
func validateToken(tokenString string) (*jwt.Token, error) {
return jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
return []byte(os.Getenv("JWT_SECRET")), nil
}
// Reject algorithm confusion attacks
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
})
}
Pattern 3: OAuth State Parameter Treated as Optional
The OAuth authorization code flow requires a state parameter to prevent CSRF attacks. Many implementations treat it as optional—either not generating it, not validating it, or validating it incorrectly.
# WRONG: Skipping state validation
@app.route('/auth/callback')
def oauth_callback():
code = request.args.get('code')
# state parameter? what state parameter?
tokens = exchange_code_for_tokens(code)
session['access_token'] = tokens['access_token']
return redirect('/dashboard')
# RIGHT: Validate state to prevent CSRF
@app.route('/auth/callback')
def oauth_callback():
code = request.args.get('code')
returned_state = request.args.get('state')
stored_state = session.pop('oauth_state', None)
if not returned_state or returned_state != stored_state:
abort(400, "Invalid OAuth state parameter")
tokens = exchange_code_for_tokens(code)
session['access_token'] = tokens['access_token']
return redirect('/dashboard')
What Actually Works
Defense-in-depth for API authentication isn't complicated. It's just consistently missing in production.
Gateway authenticates. Each microservice validates the token independently. Authorization checks happen at every boundary. Compromise one layer doesn't collapse the others.
Verify signature, validate algorithm, check expiration, confirm issuer and audience. Parse errors should return 401, not 500. Never decode without verification.
Issue a new refresh token on every use. If a token is reused, revoke the entire token family immediately. Detect token theft through reuse detection, not just expiry.
Code exchange without PKCE is vulnerable to authorization code interception. Require proof of key exchange for every OAuth client, including "trusted" internal ones.
The Hard Truth
The attackers aren't breaking crypto. They're not finding weaknesses in RSA or AES. They're exploiting implementation decisions that treat authentication as a checkbox rather than a control.
CVE-2026-35616 will be patched. The hotfix will ship. But in six months, another API authentication bypass will ship in a different product, and the post-mortem will say the same thing: "authentication was assumed to be handled elsewhere."
Assume nothing. Validate everything.
Sanitize .env Files Before Sharing
Need to share environment variables? Use Env Sanitizer to automatically detect and mask secrets. All processing happens client-side—your data never leaves your browser.
Open Env Sanitizer →