
A pre-authentication SQL injection in Fortinet FortiClient EMS 7.4.4, CVE-2026-21643, was being exploited in the wild by late March 2026, weeks after Fortinet published a fix. The bug is a textbook example of a tenant identifier taken from an HTTP header and pasted into SQL. That is worth understanding even if you never touch Fortinet gear, and if you do run EMS, the version that fixes it was itself hit by a second exploited flaw days later.
What happened
Fortinet published advisory FG-IR-25-1142 on 6 February 2026. It describes an SQL injection (CWE-89) in FortiClient EMS that "may allow an unauthenticated attacker to execute unauthorized code or commands" through crafted HTTP requests. Gwendal Guégniaud of Fortinet's own product security team found it. Only version 7.4.4 is affected. The fix is 7.4.5, and the 7.2 and 8.0 branches are not affected.
On the weekend of 28–29 March, threat intelligence firm Defused said its honeypots had recorded the first exploitation about four days earlier. At the time, Shodan showed close to 1,000 publicly exposed EMS instances. Shadowserver tracked more than 2,000 exposed EMS web interfaces, over 1,400 of them in the United States and Europe. CISA added CVE-2026-21643 to its Known Exploited Vulnerabilities catalog on 13 April 2026.
A second problem followed quickly. Fortinet released an out-of-band hotfix for CVE-2026-35616, a pre-authentication API access bypass in EMS 7.4.5 and 7.4.6. Defused Cyber reported zero-day exploitation, and watchTowr saw attempts against its honeypots from 31 March. CISA added that bug to KEV on 6 April. The full fix is planned for 7.4.7.
Who is affected
- FortiClient EMS 7.4.4 with multi-tenant (multi-site) mode enabled is exposed to CVE-2026-21643. According to Bishop Fox's analysis, single-site deployments are not exploitable.
- FortiClient EMS 7.4.5 and 7.4.6 without the hotfix are exposed to CVE-2026-35616, so upgrading from 7.4.4 to 7.4.5 alone is not enough.
- Organisations whose EMS admin interface is reachable from the internet carry the most risk. EMS holds admin credentials, endpoint inventory, security policies and certificates for every managed device.
What it means for developers and engineering teams
Bishop Fox and Picus describe the root cause. In 7.4.4, a refactor of EMS's middleware and database layer started reading the tenant from an HTTP header named Site. That value was put into a PostgreSQL statement that sets search_path (the schema used for the tenant) using string interpolation, and this happened before any authentication check. A single request to a pre-auth endpoint such as /api/v1/init_consts could therefore run attacker-chosen SQL.
The lesson applies to any multi-tenant app that uses schema-per-tenant in PostgreSQL. Bind parameters protect values, but SET search_path takes an identifier, and you can't bind identifiers. Developers then reach for string formatting, and that is where injection happens. The safe pattern is to resolve the tenant from your own allowlist and quote it as an identifier:
# Python + psycopg 3
from psycopg import sql
schema = TENANTS[request.headers["Site"]] # KeyError on unknown tenants
cur.execute(sql.SQL("SET search_path TO {}").format(sql.Identifier(schema)))
Two more lessons: tenant resolution middleware that runs before authentication is part of your unauthenticated attack surface and needs the same review as a login form. And a database role that can do far more than the request needs turns an SQL injection into code execution.
What to do now
- Find every EMS instance and its version. 7.4.4 is vulnerable to CVE-2026-21643. 7.4.5 and 7.4.6 need Fortinet's hotfix for CVE-2026-35616, or upgrade to 7.4.7 when it is available.
- Take the admin interface off the internet. Restrict it to a management VPN or allowlisted IPs.
- Hunt for exploitation. Look in web and PostgreSQL logs for requests with unusual
Siteheader values (quotes, semicolons, SQL keywords), and for unexpected admin accounts or policy changes since late March. - If you find signs of compromise, treat EMS as breached: rotate EMS admin credentials and any certificates or secrets it stored, and review policies pushed to endpoints.
- In your own code, search for
search_path,SET SCHEMAor dynamic table names built with f-strings or concatenation, and switch to allowlists plus identifier quoting (sql.Identifier,format('%I', ...)in PL/pgSQL).
Related on TechLogHub
- SQL: the basics behind injection
- Middleware: why ordering relative to authentication matters
- PostgreSQL vs MySQL
Sources
- NVD: CVE-2026-21643 (includes CISA KEV entry)
- BleepingComputer: Critical Fortinet FortiClient EMS flaw now exploited in attacks
- Help Net Security: FortiClient EMS bug under active attack (30 March 2026)
- Picus Security: CVE-2026-21643 technical analysis
- The Hacker News: Fortinet patches actively exploited CVE-2026-35616

