Safeguard
Security•

Ivanti Connect Secure CVE-2026-104935: A Header Picks the Script, the URL Picks the Rules

CISA added Ivanti Connect Secure CVE-2026-104935 to KEV on 13 October. Affected and fixed versions, how the authentication bypass and code execution chain works, why the Integrity Checker misses it, and what to check beyond the upgrade.

Safeguard Security Team
8 min read

CVE-2026-104935 lets an unauthenticated attacker run commands on an Ivanti Connect Secure (ICS) VPN appliance, and it has been exploited in the wild since at least 3 October 2026. Ivanti published its advisory and CISA added the CVE to the Known Exploited Vulnerabilities catalog on 13 October, with a remediation due date of 16 October for US federal agencies.

The fix closes the entry point. It does not finish the job. The exploit chain Volexity recovered also includes a root privilege escalation that Ivanti says will be patched "in the coming weeks", and the attackers erased their tracks well enough that Ivanti's own Integrity Checker does not flag it. This post is for teams that run ICS. It covers which versions are affected, how the chain works, and what to check beyond the upgrade.

The short answer

CVE-2026-104935 is an authentication bypass in Ivanti Connect Secure that leads to unauthenticated command execution (CVSS 8.6, CWE-290). Affected versions are 22.8R2.4 and earlier 22.8 releases, 25.1.1.1 and earlier, and 25.1.2.2 and earlier. Fixed versions are 25.1.1.2, 22.8R2.5, and 25.1.3.0 or 25.1.3.1. Versions 22.7.x and lower are not affected. If you ran an affected version exposed to the internet, upgrade and then investigate, because the attackers' post-exploitation activity leaves few traces on the appliance.

Affected and fixed versions

From Ivanti's advisory:

Release lineAffectedFixed inNotes
22.7.x and lowerNon/a22.7 reaches end of life on 31 January 2027
22.822.8R2.4 and prior22.8R2.5 (released 12 Oct)Ivanti recommends moving to 25.1.1.2. 22.8 also ends on 31 January 2027
25.1.125.1.1.1 and prior25.1.1.2 (released 5 Oct)For ISA6000 and ISA8000
25.1.225.1.2.2 and priorNot listedThe advisory names no 25.1.2.x fix. Confirm your path with Ivanti
25.1.3No25.1.3.0 (July 2026), 25.1.3.1Requires the new ISA4500, ISA6500 or ISA8500 platform, or a rebuilt virtual or cloud appliance

Ivanti says it found part of this vulnerability through its own product security work and fixed it in 25.1.3.0 in July. It was still backporting the fix to other supported versions when exploitation began. That is an uncomfortable but common gap: a fix that exists in one release line tells anyone who compares builds where to look in the others.

If you track appliance versions in an inventory, this small script sorts them using the advisory's table:

#!/usr/bin/env python3
"""Triage Ivanti Connect Secure versions for CVE-2026-104935 (Ivanti advisory, 13 Oct 2026).
Usage: python3 ics_triage.py 22.8R2.4 25.1.1.1 25.1.3.0 ..."""
import re, sys

def key(v):
    # "22.8R2.4" -> (22, 8, 2, 4); "25.1.1.2" -> (25, 1, 1, 2)
    return tuple(int(n) for n in re.findall(r"\d+", v))

def status(v):
    k = key(v)
    if k < (22, 8):
        return "not affected (22.7.x and lower), but 22.7 is end of life on 31 Jan 2027"
    if k[:2] == (22, 8):
        return "FIXED (22.8R2.5+)" if k >= (22, 8, 2, 5) else "AFFECTED: upgrade to 25.1.1.2, or 22.8R2.5 as a bridge"
    if k[:3] == (25, 1, 1):
        return "FIXED (25.1.1.2+)" if k >= (25, 1, 1, 2) else "AFFECTED: upgrade to 25.1.1.2"
    if k[:3] == (25, 1, 2):
        return "AFFECTED: advisory lists no 25.1.2.x fix; confirm your upgrade path with Ivanti"
    if k >= (25, 1, 3):
        return "not affected (25.1.3.0+, new ISA platforms)"
    return "unknown version line: check the advisory"

for v in sys.argv[1:]:
    print(f"{v:10} {status(v)}")
$ python3 ics_triage.py 22.8R2.4 22.8R2.5 25.1.1.1 25.1.2.2 25.1.3.0
22.8R2.4   AFFECTED: upgrade to 25.1.1.2, or 22.8R2.5 as a bridge
22.8R2.5   FIXED (22.8R2.5+)
25.1.1.1   AFFECTED: upgrade to 25.1.1.2
25.1.2.2   AFFECTED: advisory lists no 25.1.2.x fix; confirm your upgrade path with Ivanti
25.1.3.0   not affected (25.1.3.0+, new ISA platforms)

"FIXED" here means fixed for CVE-2026-104935. The privilege escalation described below is a separate, still-unpatched bug.

How the chain works

Volexity found the activity on 5 October while investigating a customer's network, traced it to an ICS appliance running 22.8R2.4 (Build 19097), which was the latest release at the time, and worked with Ivanti on the fix. Its write-up describes three linked bugs. The first two are tracked together as CVE-2026-104935.

1. Authentication bypass: the URL and the header disagree. ICS serves its login page from /dana-na/auth/url_default/welcome.cgi, which has to be reachable without logging in. Requests to it could also carry an X-DSCGI-PATH header naming a different CGI script. According to Volexity, the URL decided whether a request was allowed, and the header decided where it was routed. An attacker could send a request to the public login page with the header pointing at a protected script, such as the upload handler wu.cgi, and that script would run without authentication.

This is the same failure shape as two parsers disagreeing about one request, which we covered in two correct parsers can disagree about the same request. Authorization checked one field. Dispatch used another.

2. Code execution: write first, validate second. wu.cgi wrote an uploaded file to /home/runtime/tmp/<MD5(filename)> and only then checked whether it was a valid ZIP, deleting it if not. That leaves a short window where an attacker-supplied script sits on disk. The attacker uploaded a Perl script with a .zip name and, in parallel, sent requests that used the same header trick to execute the temporary file before it was deleted.

3. Root: a trusted maintenance service. The resulting code ran as the cgi-server user. To reach root, the attacker patched an Ivanti library in that process's memory, edited the shared DSCache configuration so that a root maintenance task launched Perl, and asked the privileged dsinvoked service to run it. Afterwards the exploit restored the configuration and the in-memory library. Ivanti is working on a fix for this step.

Why the Integrity Checker misses it

Volexity reports that the actor, which it tracks as UTA0576, never installed a web shell or backdoor. Each command re-ran the whole exploit chain and returned its output in the HTTP response. The exploit also removed recent entries from the Ivanti debug log and the Linux audit log, deleted its temporary files, and restored everything it had changed in memory. The on-disk files were never modified, which is why Ivanti's Integrity Checker Tool, built to find unexpected file changes, does not see this compromise.

After exploitation, the actor went after credentials: LDAP service account credentials stored on the appliance, LAPS local administrator passwords and BitLocker recovery keys queried from Active Directory, credentials parsed from packet captures of LDAP traffic, and password spraying against domain accounts. Volexity assesses with low confidence that the motive was criminal rather than espionage.

What to do now

1. Upgrade. Use the table above. For 22.8, Ivanti's preferred path is 25.1.1.2. 22.8R2.5 is the bridge if you cannot move yet.

2. Assume compromise if you ran an affected version on the internet. Given exploitation since early October and evidence removal, a clean Integrity Checker result is not reassurance. Ivanti's recovery steps require a customer login. Volexity recommends memory and network forensics, plus checking /tmp/ on the appliance for leftover post-exploitation files.

3. Rotate what the appliance could reach. Treat the LDAP or Active Directory service account configured on the ICS appliance as exposed and rotate it. Review recent LAPS and BitLocker recovery key reads by that account, and look for password spraying against domain accounts originating from the appliance.

4. Watch for the header. Volexity advises monitoring requests to /dana-na/auth/url_default/welcome.cgi that carry an X-DSCGI-PATH header, and more generally any mismatch between the URL and that header. An external client has no legitimate reason to send it. If TLS for the appliance terminates on a load balancer or inspection device you control, a rule along these lines is a starting point. We have not tested this rule, so validate it with suricata -T against your ruleset first:

alert http any any -> $HOME_NET any (msg:"Inbound X-DSCGI-PATH header (Ivanti ICS CVE-2026-104935 chain)"; flow:established,to_server; http.header_names; content:"X-DSCGI-PATH"; nocase; classtype:attempted-admin; sid:9100001; rev:1;)

ICS normally terminates TLS itself, so a network sensor in front of it will only see encrypted traffic unless you decrypt it. Volexity has published related indicators on its GitHub.

5. Plan the 2027 migrations now. Both 22.7 and 22.8 reach end of life on 31 January 2027, and 25.1.3.0 and later need the new hardware platforms or rebuilt virtual appliances. That is a procurement timeline, not a patch window.

The pattern to take away

Edge appliances keep failing in the same place: the code that decides whether a request is allowed and the code that decides what the request does read different parts of it. Here it was a URL and a header. Each component was reasonable in isolation. Together they turned a public login page into a router for every protected script on the box.

The second lesson is about response. When an attacker can execute code without dropping files and can clean the logs behind them, "patched and the integrity check passed" is not the same as "not compromised". Our post on CVE-2025-22457, an earlier Connect Secure zero-day, covered the same two themes: a fix that existed before anyone treated it as urgent, and attacker tooling built to stay invisible to the Integrity Checker. For internet-facing appliances, budget for forensics alongside patching, every time.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.