Executive Summary
CVE-2026-21643 is a critical, pre-authentication SQL injection vulnerability in Fortinet FortiClient Endpoint Management Server (EMS) version 7.4.4, carrying a CVSS v3.1 score of 9.8. The flaw allows an unauthenticated remote attacker to inject arbitrary SQL commands through an unsanitized HTTP header value in the multi-tenant routing layer, granting full database access and a clear path to remote code execution. CISA added this vulnerability to its Known Exploited Vulnerabilities (KEV) catalog with an urgent remediation deadline of April 16, 2026 — organizations running FortiClient EMS 7.4.4 in multi-tenant mode must patch immediately.
1. What Is This Vulnerability?
CVE-2026-21643 is a CWE-89 (Improper Neutralization of Special Elements used in an SQL Command) vulnerability rooted in a code regression introduced during a routine refactoring of the FortiClient EMS database connection layer in version 7.4.4.
When Fortinet added multi-tenant support to FortiClient EMS, an HTTP header — used to identify which tenant a request belongs to — was passed directly into an SQL query string using raw string interpolation, rather than the parameterized queries used in previous versions. Critically, this injection point sits before any authentication check, meaning attackers can trigger it without valid credentials.
The Vulnerable Code Pattern
In the affected version, the tenant-routing middleware constructs its database query like this (pseudocode):
# VULNERABLE — 7.4.4 (raw string interpolation)
tenant_id = request.headers.get("X-Tenant-ID")
query = f"SELECT * FROM tenants WHERE tenant_id = '{tenant_id}'"
db.execute(query)
The correct implementation, used in prior and subsequent releases, uses parameterized queries:
# SAFE — 7.4.5 and later (parameterized)
tenant_id = request.headers.get("X-Tenant-ID")
query = "SELECT * FROM tenants WHERE tenant_id = ?"
db.execute(query, (tenant_id,))
This single-line regression — swapping parameterized handling for raw string interpolation — opened a catastrophic pre-authentication attack surface.
Attack Vector
The vulnerable endpoint is publicly accessible at:
POST /api/v1/init_consts
An attacker crafts a malicious X-Tenant-ID header and sends it in an unauthenticated HTTP request:
POST /api/v1/init_consts HTTP/1.1
Host: <target-ems-server>
X-Tenant-ID: ' OR '1'='1'; --
Content-Type: application/json
Because the endpoint returns detailed database error messages and has no lockout or rate-limiting, attackers can enumerate and extract the entire EMS management database rapidly using time-based or error-based SQL injection techniques. From database access, attackers can:
- Dump endpoint agent credentials and authentication tokens
- Modify tenant/agent configuration data
- Leverage
xp_cmdshell(on MSSQL backends) or equivalent stored procedure execution to escalate to remote code execution on the EMS server
Real-World Impact
Active exploitation was publicly reported on March 30, 2026. Threat intelligence from Fortinet FortiGuard Labs recorded dozens of exploitation attempts peaking on April 14, 2026, following the vulnerability's CISA KEV listing. Bishop Fox published a detailed proof-of-concept writeup demonstrating the full pre-auth to RCE chain. Attackers are actively targeting internet-exposed FortiClient EMS servers — particularly in enterprise environments running multi-tenant deployments across healthcare, government, and financial services sectors.
2. Who Is Affected?
| Component | Affected | Notes |
|---|---|---|
| FortiClient EMS 7.4.4 (multi-tenant) | YES | Core vulnerable release |
| FortiClient EMS 7.4.4 (single-site) | No | Multi-tenant feature not present |
| FortiClient EMS 7.2.x | No | Not affected per Fortinet advisory |
| FortiClient EMS 8.0.x | No | Not affected per Fortinet advisory |
| FortiClient EMS 7.4.5+ | No | Patched release |
Key factors that increase exposure:
- EMS management interface exposed to the public internet
- Multi-tenant mode enabled
- Running on Windows Server with MSSQL backend (enables xp_cmdshell RCE path)
- No WAF or reverse proxy filtering
X-Tenant-IDheaders upstream
3. How to Detect It (Testing)
Manual Testing Steps
Step 1: Identify the EMS version
Check the FortiClient EMS web interface or query the /api/v1/version endpoint:
curl -sk https://<EMS_HOST>/api/v1/version | jq .
Look for a version string returning 7.4.4. If confirmed, proceed to Step 2.
Step 2: Probe the init_consts endpoint without authentication
curl -sk -X POST https://<EMS_HOST>/api/v1/init_consts \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: test" \
-d '{}'
A response with a 200 OK and tenant-related data (without any auth prompt) confirms the endpoint is pre-auth accessible.
Step 3: Test for SQL error-based injection
curl -sk -X POST https://<EMS_HOST>/api/v1/init_consts \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: '" \
-d '{}'
What indicates vulnerability: An HTTP response containing SQL syntax error messages (e.g., Unclosed quotation mark, Syntax error near..., or MSSQL/SQLite error strings) rather than a generic application error confirms the injection point is unprotected.
Warning: Only perform these tests against systems you own or have explicit written authorization to test. Any further exploitation beyond detection constitutes unauthorized access.
Automated Scanning
Tool 1: sqlmap
sqlmap -u "https://<EMS_HOST>/api/v1/init_consts" \
--method POST \
--headers "X-Tenant-ID: *\nContent-Type: application/json" \
--data "{}" \
--level 3 \
--risk 2 \
--dbms mssql \
--batch
Expected output: [CRITICAL] parameter 'X-Tenant-ID' is vulnerable
Tool 2: Nuclei (community template)
nuclei -u https://<EMS_HOST> -t cve/2026/CVE-2026-21643.yaml -v
Check the Nuclei template repository for the CVE-2026-21643 template — community templates for this CVE were published within days of the initial disclosure.
Tool 3: Qualys / Tenable Nessus
Both Qualys ThreatPROTECT and Tenable published authenticated and unauthenticated detection plugins for CVE-2026-21643 by early April 2026. Run a credentialed scan against EMS hosts with the plugin IDs:
- Nessus Plugin: Search "CVE-2026-21643" in the plugin manager
- Qualys QID: Published under Fortinet FortiClient EMS category
Code Review Checklist
If reviewing the EMS codebase or a fork/internal build:
- Verify all SQL queries in the multi-tenant routing layer use parameterized statements (prepared statements), not f-string or
%s-style interpolation - Confirm
X-Tenant-IDand all other user-controlled HTTP headers are validated and sanitized before reaching any database layer - Check that authentication middleware runs before the tenant-routing/DB-query middleware in the request pipeline
- Verify
/api/v1/init_constsand other pre-auth endpoints do not access the database with user-supplied input - Confirm SQL error details are never returned to the client (disable verbose DB errors in production)
4. How to Fix It (Mitigation)
Step-by-Step Remediation
1. Identify all FortiClient EMS instances in your environment
# On your asset inventory or via network scan
nmap -p 443,8443 --open -sV <subnet> | grep -i forticlient
Check the EMS administration panel: Settings → About to confirm the exact version.
2. Verify if multi-tenant mode is enabled
In the EMS admin console: Administration → Global Settings → Multi-Tenant. If this feature is disabled, CVE-2026-21643 is not exploitable on your instance (though upgrading is still strongly recommended).
3. Apply the patch — upgrade to FortiClient EMS 7.4.5
Download the update from the Fortinet Support Portal:
FortiClientEMS_windows_7.4.5.xxxx.exe (Windows Server)
Follow Fortinet's upgrade guide:
- Back up the EMS database and configuration before upgrading
- Place EMS in maintenance mode: Administration → Maintenance Mode → Enable
- Run the installer — the upgrade is in-place and preserves existing agent enrollments
- Validate services restart cleanly post-upgrade
4. Apply interim network controls (while patching)
If an immediate upgrade is not possible:
# Upstream WAF/reverse proxy rule — block X-Tenant-ID injection attempts
# Nginx example:
if ($http_x_tenant_id ~* "['\";--]") {
return 403;
}
Block or restrict access to /api/v1/init_consts from untrusted networks at the firewall level:
# iptables: restrict EMS HTTPS to internal/VPN ranges only
iptables -A INPUT -p tcp --dport 443 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -s 172.16.0.0/12 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j DROP
5. Rotate credentials post-patch
If your EMS was potentially exposed:
- Rotate all FortiClient agent authentication tokens and certificates
- Rotate EMS admin account passwords
- Audit EMS database for unauthorized tenant/configuration modifications
- Review Windows Server event logs for unusual
xp_cmdshellor service account activity
Code Fix Example (For internal/custom EMS integrations)
Before (vulnerable):
def get_tenant_config(request):
tenant_id = request.headers.get("X-Tenant-ID", "")
query = f"SELECT config FROM tenant_config WHERE tenant_id = '{tenant_id}'"
return db.execute(query).fetchone()
After (safe):
def get_tenant_config(request):
tenant_id = request.headers.get("X-Tenant-ID", "")
# Validate format before touching DB
if not re.match(r'^[a-zA-Z0-9_-]{1,64}$', tenant_id):
raise ValueError("Invalid tenant ID format")
# Use parameterized query
query = "SELECT config FROM tenant_config WHERE tenant_id = ?"
return db.execute(query, (tenant_id,)).fetchone()
Configuration Hardening
- Disable multi-tenant mode if it is not actively used in your deployment
- Enable MSSQL xp_cmdshell protection: Ensure
xp_cmdshellis disabled in SQL Server if not required (it is disabled by default; verify it remains so) - Restrict EMS management interface to internal/VPN networks — never expose the EMS admin port directly to the internet
- Enable SQL Server auditing to log all stored procedure executions and failed authentication events
5. How to Test the Fix (Validation)
Regression Test Scenarios
- Scenario A: Attempt the SQL error injection probe (
X-Tenant-ID: ') against the patched EMS — should receive a generic400 Bad Requestor application error, not a SQL syntax error message - Scenario B: Send a valid multi-tenant request with a legitimate
X-Tenant-ID— tenant routing should function normally, confirming no functionality regression - Scenario C: Attempt a
sqlmapscan against the patched host — should return no injectable parameters found
Security Test Cases
Test Case 1: Verify SQL injection is no longer exploitable
- Precondition: FortiClient EMS upgraded to 7.4.5
- Steps:
- Send
POST /api/v1/init_constswith headerX-Tenant-ID: ' OR '1'='1'; -- - Inspect HTTP response body and status code
- Send
- Expected Result: No SQL error string in response; receives
400or403; no database data returned
Test Case 2: Verify authentication gating on pre-auth endpoints
- Precondition: 7.4.5 installed
- Steps:
- Send unauthenticated request to
/api/v1/init_consts - Observe whether sensitive tenant configuration data is returned without credentials
- Send unauthenticated request to
- Expected Result: Response does not include tenant configuration data; authentication is required or endpoint behavior is scoped to non-sensitive constants only
Test Case 3: Functional regression — multi-tenant enrollment
- Precondition: 7.4.5 installed, multi-tenant mode enabled
- Steps:
- Enroll a FortiClient agent under a test tenant
- Verify the agent correctly appears in the correct tenant's dashboard
- Verify cross-tenant data isolation is maintained
- Expected Result: Normal agent enrollment flow succeeds; no data leakage between tenants
Automated Tests
import requests
EMS_HOST = "https://your-ems-host"
def test_sqli_injection_blocked():
"""CVE-2026-21643 regression: injection payload should be rejected."""
payloads = [
"' OR '1'='1",
"'; DROP TABLE tenants; --",
"1' UNION SELECT NULL, NULL --",
]
for payload in payloads:
resp = requests.post(
f"{EMS_HOST}/api/v1/init_consts",
headers={"X-Tenant-ID": payload, "Content-Type": "application/json"},
json={},
verify=False,
timeout=10
)
assert resp.status_code in (400, 403, 422), \
f"Unexpected status {resp.status_code} for payload: {payload}"
assert "syntax error" not in resp.text.lower(), \
f"SQL error leaked in response for payload: {payload}"
print(f"PASS — payload rejected: {payload[:40]}")
test_sqli_injection_blocked()
6. Prevention & Hardening
Best Practices
- Never trust HTTP headers as safe input. Headers like
X-Tenant-ID,X-Forwarded-For, andX-User-IDare fully attacker-controlled and must be validated and sanitized before any use in backend logic, especially database queries. - Parameterize all database queries. SQL injection is a solved problem — parameterized queries (prepared statements) prevent injection by design. Enforce this in code reviews and CI via SAST tools (Semgrep, Snyk Code, SonarQube).
- Authentication before data access. Pre-authentication endpoints that touch the database create a dangerous attack surface. Implement strict separation: routes that require tenant context should require authentication first.
- Principle of least privilege for DB accounts. The EMS service account connecting to MSSQL should have only the permissions it needs — not
sysadmin, notxp_cmdshellexecution rights. - Network segmentation. Management interfaces for security products (EMS, SIEM, NAC) should never be directly internet-accessible. Place them behind a VPN or jump host.
- Vendor advisory subscriptions. Subscribe to Fortinet's PSIRT advisories (https://www.fortiguard.com/psirt) to receive immediate notification of new vulnerabilities in your deployed products.
Monitoring & Detection
Indicators of Compromise (IoCs):
- HTTP requests to
/api/v1/init_constscontaining SQL metacharacters (',--,;,UNION,SELECT) in theX-Tenant-IDheader - Unusual MSSQL stored procedure execution (especially
xp_cmdshell) in SQL Server audit logs - New or unauthorized Windows services or scheduled tasks created on the EMS server
- Unexpected outbound connections from the EMS server to unknown IPs
Detection rules (SIEM — Splunk/ELK example):
# Splunk: Detect SQLi probe on EMS init_consts endpoint
index=web_logs uri_path="/api/v1/init_consts"
| eval sqli_suspect=if(match(http_x_tenant_id, "(?i)(union|select|insert|drop|--|;|')"), "true", "false")
| where sqli_suspect="true"
| table _time, src_ip, http_x_tenant_id, http_status
# Elastic detection rule
rule:
name: CVE-2026-21643 SQLi Probe on FortiClient EMS
query: >
http.request.uri.path: "/api/v1/init_consts" AND
http.request.headers.x-tenant-id: (*'* OR *;* OR *UNION* OR *SELECT* OR *--*)
severity: critical
References
- CVE Entry: CVE-2026-21643 — NVD/NIST
- Fortinet PSIRT Advisory: Fortinet Security Advisory FG-IR-26-21643
- Patch Download: Fortinet Support Portal — FortiClient EMS 7.4.5
- Bishop Fox Technical Writeup: Pre-Authentication SQL Injection in FortiClient EMS 7.4.4
- Horizon3.ai Attack Research: CVE-2026-21643 FortiClient EMS SQL Injection
- CISA KEV Entry: Known Exploited Vulnerabilities Catalog
- Picus Security Analysis: CVE-2026-21643 Exploited in the Wild
- Help Net Security Coverage: Critical FortiClient EMS bug under active attack