How this bug class keeps getting shipped

Path traversal is the vulnerability class that will not die. It is one of the oldest input-handling flaws in the catalogue; every security training mentions it, and every mature web framework ships helpers to prevent it. Yet two entries CISA added to the Known Exploited Vulnerabilities catalogue before 2026-10-20 are path traversal bugs in enterprise-grade security appliances, and a third is an authentication-bypass-by-spoofing in a remote support product that gives attackers access to the same file-operation primitives without credentials. The pattern that keeps shipping is not exotic. It is the same mistake, in the same place in the request lifecycle, regardless of which framework surrounds it.

This post traces the mechanism through one lead finding from the current KEV additions: CVE-2026-76519, Cisco's advisory for an arbitrary file write in Secure Email Gateway's Advanced Malware Protection path. It then compares that finding to CVE-2025-61624 in Fortinet's FortiOS, FortiPAM, FortiProxy, and FortiSwitchManager, which sits one authentication boundary further in but exploits the same primitive. The point is not to grade vendors. The point is to show application-security engineers what the defective code shape looks like so they can grep for it in their own repositories before it is grepped for them.

The lead finding: CVE-2026-76519

Cisco's advisory is direct. The Advanced Malware Protection feature of Cisco Secure Email Gateway, when the file reputation service is enabled, improperly handles email attachments. An unauthenticated remote attacker can send a crafted attachment through an affected device and write arbitrary files to the underlying operating system, executing commands with root privileges. CISA added it to KEV on 2026-10-14 with confirmation of in-the-wild exploitation.

Consider where that handler sits in the request lifecycle. An inbound SMTP message hits the gateway. MIME parsing extracts attachments. Each attachment is handed to the reputation subsystem, which stages the bytes on disk so other scanners, sandboxes, and signature engines can read them. That staging step is the sink. Somewhere in that pipeline, an attacker-controlled string from the attachment's metadata (filename, Content-Disposition parameter, nested archive entry name, or similar) is joined onto a staging directory path without being canonicalised and checked against that directory.

The defective shape, in pseudocode, is almost always a variation on this:

# UNSAFE: do not ship this
STAGING_DIR = "/var/spool/amp/staging"

def stage_attachment(part):
    # part.filename comes from the MIME headers an attacker controls
    name = part.filename or "attachment.bin"
    dest = os.path.join(STAGING_DIR, name)
    with open(dest, "wb") as fh:
        fh.write(part.payload)
    return dest

The reason this keeps shipping is that os.path.join and its equivalents in every language family (path.Join in Go, Path.resolve in Node, Paths.get(...).resolve(...) in Java) look safe. They are not. If name is ../../etc/cron.d/pwn, the join collapses the base and the attacker owns a cron file. If name contains a URL-encoded or UTF-8-overlong .., naive filters that check only the literal substring miss it. On Windows targets, backslash and alternate data stream syntax add further variants. The right shape canonicalises first, then verifies containment:

import os

STAGING_DIR = os.path.realpath("/var/spool/amp/staging")

def safe_stage(part):
    base = os.path.basename(part.filename or "attachment.bin")
    # basename alone is not enough on Windows and does not stop
    # symlink races; it is one layer in a defence-in-depth stack.
    candidate = os.path.realpath(os.path.join(STAGING_DIR, base))
    if os.path.commonpath([candidate, STAGING_DIR]) != STAGING_DIR:
        raise ValueError("path escapes staging directory")
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW
    fd = os.open(candidate, flags, 0o600)
    with os.fdopen(fd, "wb") as fh:
        fh.write(part.payload)
    return candidate

Three defences stack here. basename strips directory components from the untrusted string. realpath plus commonpath enforces that the resolved target stays inside the staging root. O_NOFOLLOW with O_EXCL closes the symlink and pre-existing-target races that attackers reach for when the first two defences are in place. Any one alone is bypassable. The combination is not.

Telling whether you are exposed

For CVE-2026-76519, the exposure test is operational, not code-level: identify every Secure Email Gateway in your estate, confirm whether the AMP file reputation service is enabled, and apply the vendor's fixed release. Because the trigger is an inbound email, perimeter MTAs that fan out to internal gateways do not reduce exposure; they expand it. Hunt in gateway logs for attachments whose stored filenames differ from the MIME-advertised filenames, and for writes to filesystem paths outside the normal staging and quarantine directories. CISA's current KEV remediation directive, BOD 26-04 (Prioritizing Security Updates Based on Risk, effective 2026-06-10), obligates federal civilian agencies to remediate KEV entries on the catalogue's published schedule. Private-sector blue teams should treat the KEV listing itself as the trigger to act, independent of any deadline.

The sibling bug: CVE-2025-61624

CVE-2025-61624 covers FortiOS, FortiPAM, FortiProxy, and FortiSwitchManager. Fortinet's advisory, carried into KEV on 2026-10-16, describes a path traversal that lets an authenticated attacker with admin profile and read-write permissions write or delete arbitrary files via specific CLI commands. The authentication precondition sounds reassuring until you consider the standing privilege: on a security appliance, an admin with read-write access is the identity most likely targeted by credential theft, phishing, and session hijack. Treat admin-only path traversal as an opportunistic post-exploitation primitive, not a tolerable risk.

Both CVEs collapse to the same sentence: an untrusted string reached a filesystem path join without being canonicalised and bounded against the intended root. The authentication boundary differs; the sink does not.

What to go do on Monday

Grep your own code for os.path.join, path.Join, Path.resolve, and filepath.Join call sites where one argument is request-derived. For each, confirm that the resolved path is checked against an allow-listed root after resolution, that symlinks are refused with O_NOFOLLOW, and that filenames are reduced to a basename before joining. In code review, flag any PR that introduces a new filesystem sink without those three controls. In detection, alert on writes to paths outside documented working directories by service accounts that have no business touching them. The bug class is older than most of the frameworks it lives in. The defence has to be older still.

Verifiable security.