CRITICALSecurity · Catch-up round-up · Edge appliances · Self-hosted infrastructure · Dependencies · Workstations September 26 to October 2, 2026 · 10 items · Updated October 4, 2026

Catch-Up Bulletin, September 26 to October 2, 2026: NetScaler Under Active Exploitation, an Unpatched FortiMail Path Traversal, and the Zammad Zero-Days That Breached DIVD

By NewMaxx /October 2, 2026, updated October 4, 2026

Seven days, five products with confirmed exploitation in the wild, and one of them still has no patch. The lead is Citrix NetScaler ADC and Gateway, where CVE-2026-88771 (CVSS v4 9.5) gives an unauthenticated attacker arbitrary command execution against every deployment in its default configuration, no feature required, and Cloud Software Group states plainly that exploitation of it and of CVE-2026-88772 has been observed. CISA added both to KEV on September 27 with a three-day federal deadline. Next, and more urgent per host: FortiMail CVE-2026-104286 (CVSS 9.8) is an unauthenticated path traversal that writes arbitrary files, Fortinet marks it Known Exploited, and as of today the solution column says "upcoming" for every affected branch. There is no fix to install; there is a workaround and a published implant to hunt for. Third, Zammad: DIVD published CVE-2026-102489 and CVE-2026-102490 on September 29 after the chain was used to breach DIVD itself on September 21, driven, by DIVD's own account, by an autonomous AI agent. Both reached KEV on October 2 with an October 5 due date. Go to Zammad 7.2.0: DIVD says "version 7" and Zammad says 7.0 and later are unaffected by the remote code execution with its own hardening in 7.2.0, so 7.2.0 satisfies both. The local privilege escalation to root still has no published fix, though Zammad says it needs prior access to the server. Cisco Catalyst SD-WAN Manager CVE-2026-76504 (CVSS 9.8) rounds out the exploited set, an authentication bypass through URI encoding that hands over the admin API. On the dependency side, Next.js and satori disclosed an SVG escaping flaw in next/og that Vercel rates Critical and the CVE record rates Medium, PyJWT disclosed a universal-forgery bypass of its own CVE-2022-29217 guard, and vm2 had fifteen advisories land at once, ten of them critical sandbox escapes. If you do one thing: get every internet-facing NetScaler to 14.1-73.37 or 13.1-64.23, and treat any appliance that was exposed and unpatched as suspect before you reboot it into the fix.

Framing note. This edition covers September 26 to October 2, 2026, the seven days of published activity not reached by the September 25 catch-up or the September 28 KEV supplement, both of which stop at September 25. Unlike the September 25 catch-up, which was built behind a network policy that blocked almost everything, this edition had working network access, as the September 28 supplement did: the CISA KEV catalog was fetched in full (catalog version 2026.10.01, 1,731 entries, filtered on dateAdded), and NVD, the Fortinet and Cisco PSIRTs, Citrix Knowledge Center, Apple's support site, DIVD CSIRT, OSV, PyPI and the npm registry all answered. Two sources did not, and both cost real content: the Citrix community blog that carries the NetScaler indicators of compromise returns a Cloudflare challenge to every request from this environment, so no NetScaler IOC list appears below; and github.com is restricted to this session's own repository, so GitHub advisory pages could not be opened directly and the advisory text used here came from the OSV mirror of the GitHub Advisory Database instead. One consequence is worth naming here: the satori companion GHSA referenced by the Next.js advisory could not be opened, but the related CVE record carrying the same range could, so item 5 does give a satori floor and attributes it to that record rather than to the GHSA. Items are ordered by confirmed exploitation first, then by breadth of exposure for developers, ops engineers, AI/ML infrastructure operators and self-hosters. Neither ordering is CVSS: item 4 outranks item 5 because it is being exploited, not because more readers run it. Every "as of" statement is anchored to October 2, 2026, and all registry and KEV state was read live that day. On dates for the dependency items: most of them disclose a vulnerability whose fix shipped weeks earlier, and each section gives both dates rather than implying the flaw is new.

Update, October 4, 2026. This edition was written on October 2 and re-checked on October 4 before publication. Three things changed and one was missed first time round, and all four are corrected in place rather than appended. Both Zammad CVEs are now in KEV, added October 2 with an October 5 due date, so item 3's original statement that neither was listed is gone; it was true of the catalog version this edition first fetched and false by the end of the same day. Zammad published its own statement on October 1 disputing part of DIVD's account, and the first edition did not use it: item 3 now carries both primaries, and the recommended floor is 7.2.0 rather than DIVD's "version 7". Fortinet revised FG-IR-26-175: the surface to restrict is now the webmail interface rather than the management interface, there is a new web application firewall mitigation that names /ibe as the vulnerable endpoint, and the seven file paths and fourteen hashes that were in the advisory on October 2 have been removed from it. Item 2 reflects all three and keeps the withdrawn hashes clearly labelled as withdrawn. Still unchanged: FortiMail has no fixed build for any branch, and the Zammad root escalation has no published fix.

If you only do three things on reading this (published October 2, 2026)

(1) NetScaler, in this order: look, then patch. CVE-2026-88771 needs no feature enabled, so every ADC and Gateway below 14.1-73.37 or 13.1-64.23 is vulnerable, and Cloud Software Group says exploitation has been observed. If the appliance was reachable from the internet while unpatched, capture a VPX snapshot and a technical support bundle first: the upgrade warm-restarts the packet engine and destroys the memory state any later compromise decision depends on. Then run Citrix's indicator checks, from the community blog linked out of CTX697096, against that evidence, because this bulletin could not read that list for you. Only then install 14.1-73.37, 13.1-64.23, 14.1-73.37 FIPS, or 13.1-37.279 for FIPS and NDcPP. Secure Private Access hybrid deployments use NetScaler instances and need the same builds.

(2) FortiMail: hunt first, then apply the workaround, because there is nothing to install. Fortinet's solution column reads "upcoming" for 8.0.2, 7.6.7 and 7.4.9, and the 7.2 branch gets no fix at all. Start with the implant: Fortinet published file hashes, two IP addresses and distinctive log lines, all reproduced in item 2, and the log checks run against the logs you already ship elsewhere rather than against the appliance, so they cost minutes and change nothing. A hit means incident response, and the configuration change below would then be a change to a system you no longer control. With no hit, disable the IBE feature (config system encryption ibe / set status disable / end, or Encryption then IBE then IBE Service off in the GUI) and get the webmail interface off the internet, or block POST requests to /ibe containing ../ at a web application firewall. Fortinet's wording here changed after October 2, from "management interface" to "webmail interface", so check which surface you actually restricted.

(3) Sweep the dependency floors in one pass. next 16.3.6 or later (16.3.8 is current), satori 0.33.5 or later if you depend on it directly, pyjwt 2.14.0 or later (2.15.1 is current), vm2 3.11.7 or later (3.12.2 is current), piscina 5.3.2 / 4.9.4 / 6.0.0-rc.5 depending on your branch, and @xhmikosr/decompress 11.1.4 or 10.2.2. The unmaintained decompress package has no fix and needs replacing. Every one of those releases already existed before this window opened, so for most readers this is a re-resolve and a redeploy rather than a wait.

Priorities, by what you run

Items 1 to 4 come first because exploitation is confirmed, which is why Cisco Catalyst SD-WAN Manager precedes Next.js despite a much smaller footprint in this audience; items 5 to 10 are then ordered by breadth. "Higher" means broad footprint in this audience combined with either confirmed exploitation or an unauthenticated path from the network to code execution. "Medium" means a real fix to apply on your normal cadence, or a narrower precondition. "Lower" means the exposure depends on something else being wrong first. Five items here carry confirmed exploitation in the wild: items 1, 2 and 4 on their vendors' own statements, item 3 on DIVD's account of being breached through it, and item 8 on Apple's. Item 8 still sits at Medium because Apple's wording is narrow, a sophisticated attack against specifically targeted individuals rather than commodity exploitation, and because the update is a routine one. The other five items carry no exploitation claim, because none of their advisories makes one.

Priority Item Who and what
Higher 1. Citrix NetScaler CVE-2026-88771 and CVE-2026-88772, plus six more (KEV September 27) Anyone self-hosting NetScaler ADC or Gateway, including Secure Private Access hybrid. Exploitation observed. Preserve evidence if it was exposed, then upgrade to 14.1-73.37, 13.1-64.23, 14.1-73.37 FIPS, or 13.1-37.279 FIPS/NDcPP.
Higher 2. FortiMail CVE-2026-104286 (KEV October 1, no fix available) Operators of FortiMail 8.0.0 to 8.0.1, 7.6.0 to 7.6.6, 7.4.0 to 7.4.8, 7.2.0 to 7.2.9. Known exploited, and still no released fix as of October 4. Disable IBE, restrict the webmail interface (the advisory's own wording changed from "management" after October 2), or block POST to /ibe containing ../ at a WAF, and hunt the indicators.
Higher 3. Zammad CVE-2026-102489 and CVE-2026-102490 (DIVD, September 29) Anyone self-hosting Zammad helpdesk at 6.3.0 up to but not including 6.5.4, and, per DIVD, every version from 1.5.0 for the root escalation. Used to breach DIVD on September 21; both CVEs reached KEV on October 2 with a October 5 due date. Run DIVD's log check, then upgrade to 7.2.0; the root escalation has no published fix and Zammad says it needs prior server access.
Higher 4. Cisco Catalyst SD-WAN Manager CVE-2026-76504 (KEV September 30) Anyone running on-prem Catalyst SD-WAN Manager (vManage). Unauthenticated admin API access, active exploitation, no workaround. Check the two log files Cisco names, then upgrade to the fixed release for your train.
Higher 5. Next.js and satori, GHSA-vcvr-r3jv-pc5j and CVE-2026-94545 (advisories September 30) Self-hosted Next.js 16.2.0 through 16.3.5 that renders attacker-controlled values into SVG through the Node.js ImageResponse. Upgrade to 16.3.6 or later; the Edge implementation is not affected. If you use satori directly, that is a separate floor of 0.33.5. Vercel rates it Critical and the CVE record rates it 5.3 Medium; both are in the item.
Medium 6. PyJWT CVE-2026-102268 (advisory September 28) Python services on pyjwt 2.13.0 or below. Higher if any jwt.decode allow-list mixes an HMAC algorithm with an asymmetric one, which makes it universal token forgery. Upgrade to 2.14.0 or later and split the allow-lists.
Medium 7. vm2: fifteen advisories, ten critical (published October 1) Anyone executing untrusted JavaScript in vm2 below 3.11.7, including AI agent and plugin sandboxes. Upgrade to 3.11.7 or later (3.12.2 is current), and stop treating vm2 as a security boundary.
Medium 8. Apple CoreGraphics CVE-2026-86950 (KEV September 29) macOS and iOS development workstations below macOS Sequoia 15.8.1, macOS Tahoe 26.7.1, or iOS/iPadOS 26.7.1. Apple says it may have been exploited against specific targeted individuals, which is narrower than commodity exploitation. Update on your normal workstation cadence, sooner if you are plausibly a target.
Medium 9. decompress path traversal CVE-2026-101894 (NVD September 28) Any Node service extracting archives it did not create. Upgrade @xhmikosr/decompress to 11.1.4 or 10.2.2. The upstream decompress package shares the flaw and will not be patched: replace it.
Lower 10. piscina CVE-2026-102992 (advisory October 1) Node services using the piscina worker pool that also have a prototype-pollution primitive somewhere in the dependency tree. Upgrade to 5.3.2, 4.9.4 or 6.0.0-rc.5 for your branch.

Prioritization is this bulletin's own, based on how many readers in this audience are likely to run each component and how ordinary the triggering action is. It is not a vendor urgency rating. Where a vendor or CSIRT states exploitation, that is quoted in the item and attributed; where none is stated, no exploitation claim is made either way.


1. Citrix NetScaler ADC and Gateway: unauthenticated command execution against every default deployment, exploitation observed (CVE-2026-88771 and CVE-2026-88772, plus CVE-2026-88773 through CVE-2026-88778)

Cloud Software Group published CTX697096 on September 27, 2026, covering eight vulnerabilities in NetScaler ADC and NetScaler Gateway. The sentence that sets the priority is in the bulletin's own "What Customers Should Do" section: "Exploits of CVE-2026-88771 and CVE-2026-88772 on unmitigated NetScaler deployments have been observed." CISA added both to the Known Exploited Vulnerabilities catalog the same day, September 27, with a remediation due date of September 30, which is already past as of today.

CVE-2026-88771 is the one that makes this a drop-everything item. Its precondition, in Citrix's words, is "All NetScaler ADC and NetScaler Gateway deployments (Default configuration / No additional feature required)." There is no feature to switch off, no virtual server type that saves you, and no configuration audit that can clear an unpatched appliance. It is improper input validation (CWE-20) leading to arbitrary command execution by an unauthenticated attacker, scored CVSS v4.0 9.5 by Citrix and CVSS 3.1 9.8 by NVD. CVE-2026-88772 is a memory overflow leading to remote code execution or denial of service, and its precondition is DTLS, which Citrix notes is "Enabled by default on VPN vServer": a NetScaler Gateway is vulnerable unless DTLS was explicitly turned off. Citrix scores it CVSS v4.0 9.5; NVD scores the same issue CVSS 3.1 8.1, the difference being the high attack complexity NVD records and Citrix's v4 vector also carries as AC:H.

The other six are narrower but worth knowing, because they decide how much urgency a non-internet-facing appliance deserves: CVE-2026-88773 is HTTP request smuggling (CVSS v4 9.3) on any HTTP or SSL virtual server; CVE-2026-88774 is a feature policy bypass through HTTP URL based policy expressions (7.0); CVE-2026-88775 (8.8) is a memory overflow reachable through Gateway or AAA virtual servers; CVE-2026-88776 (8.8) needs a load balancing virtual server of type Oracle; CVE-2026-88777 (8.8) needs a non-HTTP layer 7 feature such as FTP, RTSP, DNS64 or NAT64 on an LB, CS or CGNAT deployment; and CVE-2026-88778 (8.8) is TCP initial sequence number prediction, which uniquely also needs a configuration change after the upgrade, enabling Enhanced ISN Generation.

NetScaler: affected and fixed builds (Citrix CTX697096, read 2 October 2026)
CVEs: CVE-2026-88771 (CVSS v4 9.5, unauth command execution, ALL deployments) CVE-2026-88772 (CVSS v4 9.5, RCE or DoS, DTLS; default on VPN vServer) CVE-2026-88773 (9.3) CVE-2026-88774 (7.0) CVE-2026-88775 (8.8) CVE-2026-88776 (8.8) CVE-2026-88777 (8.8) CVE-2026-88778 (8.8) Exploited: CVE-2026-88771 and CVE-2026-88772, per Citrix. KEV dateAdded 2026-09-27, dueDate 2026-09-30. The other six carry no exploitation statement. Affected (Citrix "Affected Versions"): NetScaler ADC and NetScaler Gateway 14.1 BEFORE 14.1-73.37 NetScaler ADC and NetScaler Gateway 13.1 BEFORE 13.1-64.23 NetScaler ADC FIPS BEFORE 14.1-73.37 FIPS NetScaler ADC FIPS and NDcPP BEFORE 13.1-37.279 Fixed (install one of these or later in the same branch): 14.1-73.37 ADC and Gateway 13.1-64.23 ADC and Gateway, 13.1 branch 14.1-73.37 FIPS ADC 14.1-FIPS 13.1-37.279 ADC 13.1-FIPS and 13.1-NDcPP Also in scope: Secure Private Access Hybrid deployments that use NetScaler instances. Citrix-managed cloud services and Citrix-managed Adaptive Authentication are patched by Cloud Software Group, not by you. Out of scope for a version check: nothing. CVE-2026-88771 requires no feature, so an unpatched appliance is vulnerable whatever the configuration audit says. Credited: Michael Tucker, Chew Keong Tan and Alex Bernier of the JPMorgan Chase XOR Team, and Maxim Suhanov.
Build strings are quoted from CTX697096 as published. Note one rendering difference that has bitten readers before: Citrix's affected-versions list writes the FIPS and NDcPP fix as 13.1-37.279 while its "What Customers Should Do" list and the NVD CPE records write it 13.1.37.279. They are the same build. There are no indicators of compromise in this item, and that is a gap, not a finding. Citrix publishes a NetScaler IOC list in a community blog post linked from CTX697096, and CISA's KEV note points at it as well ("Running the provided IOCs in the NetScaler console may help identify indicators of exploitation"). That blog returns a Cloudflare interstitial to every request from the environment this bulletin was built in, so the list could not be read and nothing is reproduced from it. Fetch it yourself before you conclude an appliance is clean.

Am I affected?

# 1) The version is the whole answer for CVE-2026-88771. On the NetScaler CLI
#    (this is a NetScaler command, not a shell one, hence commented out here):
#      show ns version
#    Compare against: 14.1-73.37 / 13.1-64.23 / 14.1-73.37 FIPS / 13.1-37.279
#    Anything below the fix for your branch is affected regardless of config.

# 2) Which of the other six apply to you. Run from the appliance shell, or copy
#    /nsconfig/ns.conf off the box and run these there. `show ns runningConfig`
#    works too if you prefer the live config over the saved one.
NSCONF=${NSCONF:-/nsconfig/ns.conf}   # override if you copied the file off the box

echo "== CVE-2026-88772: DTLS. A Gateway is vulnerable unless DTLS is OFF =="
# The trap: grepping for "DTLS" finds NOTHING on a vulnerable default Gateway,
# because the default is implicit. Look for the ABSENCE of -dtls OFF instead.
# Case-insensitive, and a later "set vpn vserver <name> -dtls OFF" also disables
# it, so collect both before deciding. Erring toward alarm is deliberate: a false
# "DTLS on" costs you a config read, a false "DTLS off" costs you the finding.
awk 'tolower($0) ~ /^add vpn vserver/ { name=$4; on[name]=1; line[name]=$0;
       if (tolower($0) ~ /-dtls[[:space:]]+off/) delete on[name] }
     tolower($0) ~ /^set vpn vserver/ && tolower($0) ~ /-dtls[[:space:]]+off/ { delete on[$4] }
     END { for (n in on) print "  DTLS enabled (not explicitly disabled): " line[n] }' "$NSCONF"
awk '/^add (lb|vpn|cs) vserver/ && /[[:space:]]DTLS[[:space:]]/ { print "  virtual server of type DTLS: " $0 }' "$NSCONF"

echo "== CVE-2026-88773 and CVE-2026-88774: any HTTP or SSL virtual server =="
grep -cE '^add (lb|cs|vpn|authentication) vserver [^ ]+ (HTTP|SSL)([[:space:]]|$)' "$NSCONF"

echo "== CVE-2026-88775: Gateway (SSL VPN, ICA Proxy, CVPN, RDP Proxy) or AAA =="
grep -cE '^add (vpn|authentication) vserver ' "$NSCONF"

echo "== CVE-2026-88776: LB virtual server of type Oracle =="
grep -inE '^add lb vserver.*ORACLE' "$NSCONF"

echo "== CVE-2026-88777: non-HTTP L7 features =="
grep -inE '^add (lb|cs) vserver .* FTP|^add service .* FTP|^add lb monitor .* FTP(-EXTENDED)?|^set lsn group .* -rtspalg ENABLED|^add lb vserver .* DNS .* -dns64 ENABLED|^add dns policy64|^add nat64' "$NSCONF"
# FTP ALG is ON by default inside an LSN group unless explicitly DISABLED, so the
# same absence problem applies here:
awk '/^add lsn group /{grp[$4]=1} /^set lsn group /{if ($0 ~ /-ftp[[:space:]]+DISABLED/) off[$4]=1} END{for (g in grp) if (!(g in off)) print "  FTP ALG enabled by default on lsn group: " g}' "$NSCONF"

echo "== CVE-2026-88778: Enhanced ISN Generation. On the CLI, not in ns.conf =="
# show ns tcpparam | grep "Enhanced ISN Generation"
#   DISABLED, together with any virtual server of a TCP-derived type, means the
#   precondition is met. This one needs a config change as well as the upgrade.

echo "== Is it reachable from outside? Decision rule is the connection, not the status =="
# Run this from a host OUTSIDE your perimeter against your own appliance only.
# Any HTTP status at all (200, 302, 401, 403, 404) means the port answered and the
# interface IS reachable. A non-zero curl exit means no HTTP answer came back:
# 7 is connection refused, 28 is a timeout (filtered or down), others are usually
# DNS or TLS. Run the same probe against a port you know is closed as a control.
for u in "https://your-netscaler.example.com/vpn/index.html" "https://your-netscaler.example.com:9/"; do
  out=$(curl -sS -k -o /dev/null -m 10 -w '%{http_code}' "$u" 2>/dev/null); rc=$?
  if [ "$rc" -eq 0 ] && [ "$out" != "000" ]; then echo "REACHABLE $u (HTTP $out)"; else echo "no answer $u (curl exit $rc)"; fi
done

The DTLS and FTP ALG checks are written to find an absence on purpose. Citrix documents both defaults as on-unless-disabled, so a grep for the feature name returns nothing on exactly the hosts that are vulnerable. That pattern is one of the commonest ways an "am I affected" check lies to you. The virtual server counts for CVE-2026-88773 through CVE-2026-88775 are preconditions, not verdicts: a non-zero count means the CVE applies to you, and a zero count means only that those particular CVEs do not. CVE-2026-88771 is unaffected by any of it.

Response

  1. If the appliance was internet-facing and unpatched at any point since September 27, preserve evidence before you touch it. Citrix's own CTX694799 puts evidence capture first and it is right to: for a VPX, take a hypervisor snapshot of the running instance; document system time, timezone and NTP configuration; pull logs from your remote syslog and NetScaler Console, not just the appliance. Generate a technical support bundle, and generate a Packet Engine core file if you are prepared for the warm restart it causes. For MPX and SDX hardware, work with your incident response team on disk imaging before power-down. The upgrade restarts the packet engine, so doing it first throws away the memory that would answer "was this box used". Then take the ADC or Gateway off the network if you have any reason to suspect it: an appliance that terminates your VPN and holds your LDAP service account is not a box to debug in place while it is still reachable.
  2. Actually look for evidence before you upgrade. Fetch Citrix's indicator list from the community blog linked out of CTX697096, which is the list CISA's KEV note points at and the one this bulletin could not read, and run it against the appliance and the evidence you preserved in step 1. Work CTX694799's checks on the same material. A hit means stay in the incident path: keep the evidence, keep the appliance isolated, rotate from a clean host and rebuild, and do not upgrade that appliance in place, because the upgrade is how you lose the rest of the picture. Only an appliance that comes through this clean goes to the next step.
  3. Then install the fixed build. 14.1-73.37, 13.1-64.23, 14.1-73.37 FIPS, or 13.1-37.279 for FIPS and NDcPP. Include Secure Private Access hybrid NetScaler instances in the same pass. Do not stop at the one appliance you were thinking of: CVE-2026-88771 needs no feature, so internal ADCs count too, they just queue behind the exposed ones.
  4. Rotate everything the appliance held, from a clean host. Citrix lists the set: LDAP and other service account passwords, RADIUS shared secrets, OAuth tokens, API keys, SNMP community strings, every user account that authenticated through a Gateway or AAA virtual server, and the certificates and private keys stored on the appliance, which should be revoked rather than reused. Do this from a host you trust, not from a session that traverses the appliance.
  5. If you find evidence of compromise, rebuild rather than clean. Citrix's guidance is replacement: erase and reinstall for MPX, replace and restore the instance for VPX, and treat VPX instances inside an SDX the same way. Then investigate what the appliance could reach, which Citrix specifically calls out as authentication servers, sensitive systems, web tier systems and management jump hosts. Enabling Enhanced ISN Generation for CVE-2026-88778 belongs at the end of this list, after the box is one you trust.

2. FortiMail: unauthenticated path traversal writing arbitrary files, exploited in the wild, and no released fix as of today (CVE-2026-104286)

Fortinet published FG-IR-26-175 on October 1, 2026 and CISA added it to KEV the same day with a due date of October 4. The advisory's own summary says "This has been reported to be exploited in the wild, customers are urged to apply the workaround below," and its metadata block reads Known Exploited: Yes, Severity: Critical, Attack Type: Unauthenticated, CVSSv3 9.8. It is a path traversal (CWE-22) combined with improper neutralization of a NULL byte (CWE-158) in the GUI component, letting an unauthenticated attacker write arbitrary files on the underlying system through crafted HTTP or HTTPS requests.

The part that changes your plan: there is nothing to install. As read on October 2, 2026, Fortinet's solution column says "Upgrade to upcoming 8.0.2 or above", "Upgrade to upcoming 7.6.7 or above" and "Upgrade to upcoming 7.4.9 or above". The 7.2 branch has no fix planned at all; its instruction is to move to 7.4 or above, which is a branch upgrade, and 7.4 does not yet have its fix either. So for every affected reader the available actions today are the workaround and the hunt, and the upgrade is a thing to watch for rather than a thing to do.

The published indicators make this one of the more answerable incidents in this bulletin. Fortinet names an added ld.so.preload and a planted liblog.so, which together are a classic preload implant: every process on the box loads the attacker's library. It also names a modified /bin/smit, two added binaries, a modified httpd.conf, a modified migadmin.tar.gz, two IP addresses, and log lines including an exfiltration path set up as a FortiMail "archive account" pointed at one of those IPs.

FortiMail CVE-2026-104286: affected versions and indicators (Fortinet FG-IR-26-175, read 2 October 2026)
CVE: CVE-2026-104286 CWE-22 and CWE-158 CVSSv3 9.8 (Fortinet) Advisory: FG-IR-26-175, published 2026-10-01. Known Exploited: Yes. KEV: dateAdded 2026-10-01, dueDate 2026-10-04. Component: GUI. Attack type: unauthenticated. Virtual patch: No. Reported: internally, by Gwendal Guégniaud of the Fortinet Product Security team. Affected and solution, exactly as the advisory states it on 2026-10-02: FortiMail 8.0 8.0.0 through 8.0.1 Upgrade to upcoming 8.0.2 or above FortiMail 7.6 7.6.0 through 7.6.6 Upgrade to upcoming 7.6.7 or above FortiMail 7.4 7.4.0 through 7.4.8 Upgrade to upcoming 7.4.9 or above FortiMail 7.2 7.2.0 through 7.2.9 Upgrade to branch 7.4 or above No fixed build exists for any branch as of 2026-10-02. Workaround (the only remediation available as of 2026-10-04): config system encryption ibe set status disable end (GUI equivalent: Encryption, then IBE, then IBE Service off) Alternatively, per the advisory as revised between 2026-10-02 and 2026-10-04: - "Disable access to the FortiMail webmail interface from the internet or limit the access only from trusted private network" - "Or if there is a Web Application Firewall in front of FortiMail, block POST requests to /ibe that contain '../'" Note the change: on 2026-10-02 this line read "management interface". It now reads "webmail interface", which is a different surface and the one to restrict. The WAF rule also names the vulnerable endpoint, /ibe, which the advisory had not previously done. Files: WITHDRAWN FROM THE ADVISORY. The seven paths and fourteen hashes below were published in FG-IR-26-175 on 2026-10-01 and were still there when this bulletin was first written on 2026-10-02. They are NOT in the advisory as of 2026-10-04; its indicator section now begins at the IP addresses. Fortinet gives no reason for the removal and the advisory's timeline still shows only "2026-10-01: Initial publication". They are retained here for anyone who already hunted them, and because a hit on an ld.so.preload implant is worth investigating whoever published the hash. Do NOT treat them as vendor-current indicators, and do not treat their absence as a clean bill: Files (ADDED or MODIFIED), MD5 then SHA256, as published 2026-10-01 and since removed: [ADDED] /data/lib/liblog.so 64c90a00c7fda4d5c7973ed64c25783a 8015f34dc84922b03688399d7f9fe7a00361789f7e420c7e2a2cdb23e75cef84 [MODIFIED] /bin/smit 5241738a3e9988404239e12243f6d35b 77324ac428bde86d351fc5fc06f6d64a6bfe737dfb2743df1d4c5ac2418a5b6a [ADDED] /data/bin/webconsole ae0ea6502d3fa5f0664bceb73189eb54 7a6cea9f5c9e2e9994d4e3c4da73f86cf5acd05ea5d312c066c9d1dafd69ee38 [ADDED] /data/bin/mailservice f90fa81a5f521d785f2b2f765e3ab897 4000276a150a165d3c2537d1e19fb393c4de8333076a16655e28059cae82157b [MODIFIED] /data/etc/httpd.conf 61af1c4bce1c2eebc8ff689ca5337791 703e97c64e61e41dc3aaba580d82bb2aa7b6a11b54ee6fb467ed5d5a3bffdef5 [ADDED] /data/etc/ld.so.preload 8eb64f25d2a8e18e05aae058629473cf 8953ec7960b09f544a880b072ad4e6cfda7a8303f486251d3478dcfdfbac23b6 [MODIFIED] /data/migadmin.tar.gz 49a7156a7d043cc8f9f680579db22f86 d6fe51c22b91776f4c961ea58bcac5917f15d560a619d7ce726d3d51795609d3 IP addresses: 79.141.169.187 45.129.0.192 System event log lines: type=event subtype=system pri=debug user=system ui=cron msg="(root) CMD (/bin/sh -c 'O=/migadmin ... type=kevent subtype=admin pri=information user=admin ui=(null) action=logout status=success reason=unknown msg="User admin logged out from (null)." type=kevent subtype=config pri=information user=admin ui=cli msg="Added 'archive234' to 'archive account' : ... destination[remote] remote-ip[79.141.169.187] remote-username[archive234] ... Encryption (IBE) log lines: Caught BufferException(2), BufferImpl.cpp:973, 'Invalid Base64 Encoding at pos 0. Character=0x2a' Internal user *@domain.tld failed to log in.
The IP addresses, log lines and workaround above are transcribed from FG-IR-26-175 as it reads on October 4, 2026; the file hashes are transcribed from the same advisory as it read on October 2 and have since been removed from it, which is flagged in the block itself. The ellipsis in the cron line is the advisory's own; the one in the archive-account line is this bulletin's abridgement of an otherwise complete line, which also drops module=unknown submodule=unknown and spaces out the bracketed fields for legibility. The greps absorb that difference and were tested against the verbatim line. The admin logged out from (null) line is a weak indicator on its own: it is the shape of an administrative session that did not come from a normal interface, and Fortinet lists it among the indicators, but it will also appear benignly in some deployments. Treat the file hashes, the two IP addresses and the archive-account line as the strong ones. FortiMail does not give you a general shell, so hash comparison on a live appliance normally needs Fortinet support; the log checks below are the part you can run yourself today.

Am I affected?

# 1) Version, on the FortiMail CLI (a FortiMail command, not a shell one):
#      get system status
#    Look at the Version line and compare: 8.0.0-8.0.1, 7.6.0-7.6.6,
#    7.4.0-7.4.8 and 7.2.0-7.2.9 are all affected, and none of them has a
#    released fix as of 2026-10-02.

# 2) Is the IBE feature on? GUI: Encryption -> IBE -> IBE Service.
#    The advisory gives the disable commands but not a read command, so this
#    bulletin does not invent one: read the setting in the GUI, or run
#    `show system encryption ibe` and treat an absent `set status disable`
#    line the same way the NetScaler DTLS check does, as "enabled by default".

# 3) Hunt the indicators in the logs you already ship elsewhere. Run this on
#    your syslog collector or log store, NOT on the appliance. Set LOGS to
#    wherever your FortiMail logs land.
LOGS=${LOGS:-/var/log/fortimail}

# Collect the files first and say how many, so that "no files found" and "files
# found, no hits" cannot look identical. Use zgrep throughout: a log store
# rotates and compresses, and a plain grep -r silently skips every .gz, which on
# this item would mean a clean-looking report on an implanted appliance.
FILES=$(find "$LOGS" -type f 2>/dev/null)
echo "log files scanned: $(echo "$FILES" | wc -w)"
if [ -z "$FILES" ]; then
  echo "no files found under $LOGS: set LOGS to wherever your FortiMail logs land"
else

echo "== strong: the two IPs anywhere in FortiMail logs =="
zgrep -HnE '79\.141\.169\.187|45\.129\.0\.192' $FILES

echo "== strong: an archive account pointed at attacker infrastructure =="
zgrep -HnE "archive account.*remote-ip\[(79\.141\.169\.187|45\.129\.0\.192)\]" $FILES
# Broader form, in case the exfil target differs from the published IPs:
zgrep -HnE "Added '[^']+' to 'archive account'" $FILES

echo "== strong: the IBE decrypt exception the exploit path leaves behind =="
zgrep -HnF "Invalid Base64 Encoding at pos 0. Character=0x2a" $FILES

echo "== strong: the cron line that stages from /migadmin =="
zgrep -HnE "ui=cron.*CMD \(/bin/sh -c 'O=/migadmin" $FILES

echo "== strong: the IBE login failure Fortinet lists alongside it =="
zgrep -HnE "Internal user .*@.* failed to log in" $FILES

echo "== strong: traversal attempts against the endpoint the WAF rule names =="
# Fortinet's mitigation is to block POST requests to /ibe containing '../', which
# tells you the exploit path, so if you have web logs in front of FortiMail that
# is the request to look for. Do not grep for the literal '../' alone: the dots
# and the slash can each be percent-encoded independently, and '..%2f' (literal
# dots, encoded slash) is the form that defeats a naive pattern. The alternation
# below covers '../', '..\', '..%2f', '..%5c', '%2e%2e' and the mixed '.%2e' and
# '%2e.', and -i makes the uppercase encodings match too. Tested against all of
# them, including inside a .gz rotation.
zgrep -HniE '"[A-Z]+ [^" ]*/ibe[^" ]*(\.\.(/|\\|%2f|%5c)|%2e%2e|\.%2e|%2e\.)' $FILES

echo "== weak, assess against your own baseline =="
zgrep -HnE 'subtype=admin.*action=logout.*msg="User admin logged out from \(null\)' $FILES

fi

# 4) Is the WEBMAIL interface reachable from outside? Fortinet's revised
#    workaround names the webmail interface, not the management one, so that is
#    the surface to restrict. Same decision rule as item 1: the connection
#    outcome decides, not the HTTP status code. Run from outside your perimeter,
#    against your own appliance only. Adjust the paths to your deployment.
for u in "https://your-fortimail.example.com/ibe" "https://your-fortimail.example.com/webmail" "https://your-fortimail.example.com:9/"; do
  out=$(curl -sS -k -o /dev/null -m 10 -w '%{http_code}' "$u" 2>/dev/null); rc=$?
  if [ "$rc" -eq 0 ] && [ "$out" != "000" ]; then echo "REACHABLE $u (HTTP $out)"; else echo "no answer $u (curl exit $rc)"; fi
done

Note what the reachability probe does and does not tell you. A 401 or a 403 means the port answered, which means it is reachable, which is what matters here: an unauthenticated path traversal does not care that the login form rejected you. The control request to a port you know is closed exists so you can tell a real refusal from a network path that silently drops everything. The paths are deliberately plural and adjustable: Fortinet names /ibe in its WAF rule, and the webmail root differs by deployment, so confirm your own rather than trusting either example.

Response

  1. Run the log checks before you change anything. They run against the logs you already ship off the appliance, so they cost minutes and alter nothing. If the implant is present, your FortiMail is an incident, and the configuration change two steps down would be a change to a system you no longer control.
  2. If any strong indicator hit, stop here and treat the appliance as compromised. Do not apply the workaround to a box in this state, and do not reboot or reconfigure it. An added ld.so.preload plus a planted liblog.so means every process on the appliance was loading attacker code, so nothing the appliance reports about itself is trustworthy. Isolate it, preserve what you can, open a case with Fortinet for forensic support since you cannot image it yourself, rotate every credential it held (admin accounts, LDAP or RADIUS integration secrets, mail relay credentials, certificates and their private keys) from a clean host, and plan a rebuild rather than a cleanup. Also go looking for what was taken: an archive account pointed at a remote IP is a mail exfiltration channel, so the mail that passed through it is in scope.
  3. On appliances with no hit, disable IBE and restrict the webmail interface. config system encryption ibe / set status disable / end, or the GUI equivalent. If IBE is in production use for you, Fortinet now gives two alternatives, and note which surface the first one names: remove internet access to the webmail interface, or limit it to a trusted private network; or, if a web application firewall sits in front of FortiMail, block POST requests to /ibe that contain ../. The advisory said "management interface" when this bulletin was first written and now says "webmail", so if you acted on the earlier wording, check that you restricted the surface that actually matters. With no fixed build available, doing this as well as disabling IBE is worth the trouble.
  4. Watch FG-IR-26-175 for the builds to appear and install them when they do. 8.0.2, 7.6.7 and 7.4.9 are the numbers to wait for. If you are on 7.2, plan the branch move to 7.4 now so you are not doing a branch upgrade under pressure the day 7.4.9 ships.
  5. Do not treat "no hits" as clean. The indicators read as coming from a single intrusion and were published the day the advisory shipped; Fortinet does not say how many incidents they come from. A different operator using the same path traversal would leave different files. Absence of these indicators means these indicators are absent.

3. Zammad: a session flaw to remote code execution chained to a root escalation, used to breach DIVD, now in KEV, and the vendor disputes DIVD's scope (CVE-2026-102489 and CVE-2026-102490)

On September 21, 2026 the Dutch Institute for Vulnerability Disclosure was itself breached. DIVD made its first public statement on September 24, published the case file for the vulnerabilities (DIVD-2026-00015) on September 29 along with both CVE records at 20:00 UTC, and has been adding numbered statements to the incident case (DIVD-2026-00014) since September 24. Its own account of the attack is worth reading for what it says about method as much as mechanism: DIVD describes an agentic AI powered attack, with the attacker's scripts containing notes in which the agent justifies its own actions, and the chain running from unauthenticated to root "in seconds due to the agentic part of this hack". DIVD credits network segmentation and its incident response team for stopping the attacker going deeper, and states that volunteer data including DIVD email addresses and possibly contact details was exfiltrated.

CVE-2026-102489 is the entry point: a session hijack vulnerability in Zammad that leads to remote code execution as the zammad user. DIVD records it as affecting 6.3.0 up to but not including 6.5.4, and states it is "also present in version 7.0.0 to version 7.1.3, but not exploitable due to environment conditions". DIVD scores the standalone issue CVSS v4.0 8.7 and the chained scenario 9.4, and the record carries E:A, exploitation Attacked. CVE-2026-102490 is the escalation: the local zammad user becomes root. DIVD scores it 8.5 standalone and 9.4 chained.

The two CVEs need different handling, which is the practical point of this item. DIVD's recommendation is "Upgrade to Zammad version 7", and its affected-products table lists 7.0.0 and later as unaffected by CVE-2026-102489, while its text notes the code is still present in 7.0.0 to 7.1.3 and not exploitable there "due to environment conditions". It does not address CVE-2026-102490: the CVE page is titled "Undisclosed LPE in Zammad v1.5.0 to v7.1.0-alpha", and its description reads "All versions of Zammad including the latest alpha enable the local zammad user to escalate privileges to root." DIVD's case file lists patch status Available and workaround N/A while also stating "We have reported the vulnerability to Zammad who are working on a fix", and the case status is Open as of its last modification, October 1, 2026. Read together: upgrade, and keep assuming that anything that gets code execution as zammad gets root.

Zammad disputes part of this, and its statement is the second primary source on the item. On October 1, 2026 Zammad posted a statement to its own community forum, and this bulletin's first edition did not use it. On CVE-2026-102489 Zammad says it received a report in August 2026, that "Exploitation is only possible on Zammad 6.5 and older, because of the runtime environment those versions use", that those versions are already end of support, and that "Zammad 7.0 and later are not affected". It hardened the code anyway and says the change ships in 7.2.0, which is a later floor than DIVD's "version 7". On CVE-2026-102490 the sequence matters: at 12:16 UTC Zammad said DIVD had given it no technical details, that it "cannot verify a claim we have not been shown", and that it could not confirm the vulnerability, its scope or the affected versions; by 19:48 UTC it said it had received the details, was working on them, and added the one scoping claim that changes an operator's reading: "This issue cannot be exploited remotely on its own. An attacker would already need access to your server." It recommends updating to 7.2.0 and watching its GitHub security advisories. Zammad also objects to the disclosure process, noting DIVD's own timeline shows a report on September 24 followed by public scanning and disclosure on September 26.

Where they disagree, take the action that is safe under either. Zammad says 7.0 and later are not affected by the RCE; DIVD says the code is present through 7.1.3 but not exploitable there; both therefore point above 6.5, and 7.2.0 satisfies both while also carrying the hardening, so 7.2.0 is the floor this bulletin recommends rather than DIVD's "version 7". On the root escalation, DIVD says every version from 1.5.0 and gives no fix; Zammad says it needs prior server access and, as of today, has published no fixed version. Neither position lets you treat the zammad user as contained, so the architectural step stands whichever is right. What Zammad's "needs access to your server already" does change is the shape of the risk: it is the second half of a chain rather than a way in, which is exactly how it was used against DIVD.

Zammad: affected ranges and the indicator DIVD published (DIVD CSIRT, read 2 October 2026)
CVE-2026-102489 session hijack leading to RCE as the zammad user CVSS v4.0 8.7 HIGH standalone, 9.4 CRITICAL chained with CVE-2026-102490 Vector (NVD, CNA csirt@divd.nl): CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:A/AU:Y/V:C Affected: >= 6.3.0 to < 6.5.4 (Linux, Docker) Present but not exploitable "due to environment conditions": 7.0.0 to 7.1.3 Unaffected per DIVD's affected-products table: >= 7.0.0, and the catch-all row "everything else", which is where 6.5.4 and later 6.x fall UNKNOWN per that same table: < 6.3.0. DIVD puts it in the Unknown column, not the Unaffected one, so this bulletin does not call < 6.3.0 safe Fix: DIVD says "Upgrade to Zammad version 7". Zammad says 7.0 and later are not affected and that its hardening of this code ships in 7.2.0. This bulletin recommends 7.2.0, which satisfies both. CVE-2026-102490 local zammad user escalates to root CVSS v4.0 8.5 HIGH standalone, 9.4 CRITICAL chained with CVE-2026-102489 Affected: DIVD's structured range says >= 1.5.0 to < 7.1.0-alpha, while the CVE title says "v1.5.0 to v7.1.0-alpha" and the description says "All versions of Zammad including the latest alpha". Both readings are reproduced here because DIVD's own page carries both. Fix: none published as of 2026-10-04. DIVD case DIVD-2026-00015 is Open, workaround N/A. Zammad confirmed on 2026-10-01 19:48 UTC that it has received DIVD's details and is working on it, and says the issue "cannot be exploited remotely on its own. An attacker would already need access to your server." Watch Zammad's GitHub security advisories, which it names as the place a fix will appear. Exploitation: both CVEs were used against DIVD on 2026-09-21. DIVD calls them "known exploited vulnerabilities" and is scanning for and notifying owners of vulnerable instances. KEV: BOTH were added on 2026-10-02 with dueDate 2026-10-05, confirmed against catalog version 2026.10.02 on 2026-10-04. The first edition of this bulletin said neither was in KEV; that was true of catalog 2026.10.01, which was the current version when it was written, and became false the same day. CISA's own wording differs from DIVD's on the first one: CISA calls CVE-2026-102489 a "session fixation" vulnerability, DIVD a session hijack. CISA records CVE-2026-102490 as improper privilege management and notes the chaining in both directions. Indicator, from DIVD's own published log-check script: Zammad or nginx log lines at ERROR level that contain session material: ERROR -- : ... "Cookie"=>"..." ERROR -- : ... @clients={... Script: csirt.divd.nl/downloads/DIVD-2026-00015/cve-2026-102489_ioc_check_script_v2.sh Default search paths: /var/log/zammad and /var/log/nginx File patterns: production.log*, railsserver.log*, websocket.log*, scheduler.log*, nginx.log*, access.log*, error.log* (gzipped rotations included) Timeline (DIVD-2026-00015 and DIVD-2026-00014): 2026-09-21 First access by the attacker on DIVD systems 2026-09-22 DIVD detects, blocks datacenter access, starts forensics with Merlon Security 2026-09-24 Vulnerability reported to Zammad; first public statement 2026-09-26 DIVD scans for vulnerable instances, limited disclosure created, owner notification starts 2026-09-29 Case file and CVE records published, 20:00 UTC 2026-10-01 Statement on what data was exfiltrated Credited finders: Earth Grob, Luke Paris, Tijmen van der Spijk, Zohar Cochavi and Alje Woltjer (Merlon Security); Mischa Rick van Geelen, Ralph Horn and Max van der Horst (DIVD). Analysts: Victor Pasman, Frank Breedijk (DIVD).
The indicator pattern is taken from the detection script DIVD publishes on the case page, which this bulletin downloaded and read on October 2, 2026, rather than from secondary reporting. DIVD's script prints its own caveat on a clean result, and it is the right caveat: no indicators in these logs is not the same as not compromised, and the script tells you to investigate unfamiliar processes and files as well. The 8.7 and 8.5 standalone scores above are DIVD's "Scenario 1 GENERAL" figures; the single 9.4 that the NVD record surfaces for both CVEs is DIVD's "Scenario 2 chained" figure, which is why a secondary summary quoting one number and the NVD page quoting another are not in conflict.

Am I affected?

# 1) Which version is installed. Whichever of these answers is your version;
#    the last line makes sure you get an answer rather than a blank screen.
found=0
# Compare whatever you get against: 6.3.0 up to but not including 6.5.4 is the
# affected range for CVE-2026-102489; DIVD lists 7.0.0 and later as unaffected
# and puts anything below 6.3.0 in its Unknown column. CVE-2026-102490 (root)
# has no fixed version at all, so the version does not clear you of that one.
if [ -r /opt/zammad/VERSION ]; then echo "package or source install: $(cat /opt/zammad/VERSION)"; found=1; fi
v=$(dpkg-query -W -f='${Version}\n' zammad 2>/dev/null) && [ -n "$v" ] && { echo "deb package: $v"; found=1; }
v=$(rpm -q --qf '%{VERSION}\n' zammad 2>/dev/null) && [ -n "$v" ] && { echo "rpm package: $v"; found=1; }
if command -v docker >/dev/null 2>&1; then
  d=$(docker ps --format '{{.Image}}\t{{.Names}}' 2>/dev/null | grep -i zammad)
  [ -n "$d" ] && { echo "running containers:"; echo "$d" | sed 's/^/  /'; found=1; }
fi
[ "$found" -eq 1 ] || echo "no Zammad install found by any probe on $(hostname): run this on the Zammad host, or read your compose or helm image tag"

# 2) DIVD's indicator check, over rotated and gzipped logs. This is the pattern
#    from DIVD's published script; prefer running their script itself if you can
#    reach csirt.divd.nl, because they update it.
LOGS=$(find /var/log/zammad /var/log/nginx -type f \
  \( -name 'production.log*' -o -name 'railsserver.log*' -o -name 'websocket.log*' \
     -o -name 'scheduler.log*' -o -name 'nginx.log*' -o -name 'access.log*' \
     -o -name 'error.log*' \) 2>/dev/null)
echo "log files scanned: $(echo "$LOGS" | wc -w)"
if [ -n "$LOGS" ]; then
  zgrep -En 'ERROR -- :.*("Cookie"=>"|@clients=\{)' $LOGS 2>/dev/null
  echo "-- distinct session cookies exposed in error output --"
  zgrep -Eh 'ERROR -- :.*"Cookie"=>"' $LOGS 2>/dev/null | grep -oE '"Cookie"=>"[^"]*"' | sort -u
else
  echo "no Zammad or nginx logs found at the default paths: point LOGS at yours"
fi

# 3) Is it reachable from the internet? A self-hosted helpdesk usually is, which
#    is the point. Same decision rule as items 1 and 2.
for u in "https://your-zammad.example.com/" "https://your-zammad.example.com:9/"; do
  out=$(curl -sS -k -o /dev/null -m 10 -w '%{http_code}' "$u" 2>/dev/null); rc=$?
  if [ "$rc" -eq 0 ] && [ "$out" != "000" ]; then echo "REACHABLE $u (HTTP $out)"; else echo "no answer $u (curl exit $rc)"; fi
done

The version block deliberately tries four different install shapes and then says so if none matched, because a Zammad that runs in Docker leaves nothing at /opt/zammad and a check that silently prints nothing on that host reads as "not affected". The log scan reports how many files it looked at for the same reason: zero files scanned and zero hits look identical otherwise.

Response

  1. Run the indicator check first, while the logs are still the ones the attacker would have touched. An upgrade rotates and restarts services. If you find session material in error output, you have a candidate compromise with a known escalation to root available on the same host, so stop and treat it as an incident: isolate the instance, preserve the logs and the filesystem, and work from a clean host.
  2. If an indicator hit, assume root on that host and rotate accordingly. CVE-2026-102490 means code execution as zammad is code execution as root, with no fix to rely on, so the blast radius is the whole machine: database credentials, SMTP and IMAP credentials, API tokens, OAuth secrets, LDAP binds, any S3 or object storage keys, and every agent and customer session. Rebuild the host rather than cleaning it, and rotate from somewhere else.
  3. Upgrade to Zammad 7.2.0. DIVD says "version 7" and lists 7.0.0 and later as unaffected by CVE-2026-102489; Zammad says 7.0 and later are not affected and that its hardening of the same code ships in 7.2.0. 7.2.0 satisfies both readings and is the current release, so it is the floor to take rather than the lowest version either source would accept. If you cannot upgrade promptly, DIVD's stated alternative is to take the instance offline, which for an internet-facing helpdesk is a real option worth costing rather than dismissing.
  4. Do not treat the version 7 upgrade as closing the whole chain. The root escalation has no fix as of October 2, 2026. Until it does, reduce what reaching the zammad user is worth: run Zammad on a host that holds nothing else, segment it away from your authentication and internal services the way DIVD credits its own segmentation for limiting this breach, and keep its outbound network access narrow.
  5. Both are now in KEV, with a due date of October 5. CISA added CVE-2026-102489 and CVE-2026-102490 on October 2, 2026, so if you are bound by BOD 26-04 this is already overdue territory. For the root escalation, watch two places rather than one: DIVD's case page, and Zammad's GitHub security advisories, which Zammad itself names as where its fix will appear.

4. Cisco Catalyst SD-WAN Manager: one URL-encoded character bypasses authentication and hands over the admin API, exploitation confirmed (CVE-2026-76504)

Cisco published advisory cisco-sa-sdwan-webauth-xr8beuuU on September 30, 2026 at 13:00 GMT, rated Critical, CVSS 9.8, and says in its Exploitation and Public Announcements section: "In September 2026, the Cisco PSIRT became aware of active exploitation of this vulnerability." CISA added it to KEV the same day with a due date of October 3. There are no workarounds.

The mechanism is small enough to state exactly. An authentication rule restricts access to a specific API endpoint, and the request handling does not normalize URI encoding before that rule is applied (CWE-177). Percent-encode any single character in the path and the rule stops matching while the application still routes the request, so an unauthenticated attacker reaches the API as the admin user. The advisory's examples use %6a in place of the letter j in j_security_check, and it is explicit that this is only an example: "the vulnerability will allow any one character that is encoded in the request to be used to exploit this." That sentence is the difference between a detection that works and one that does not, and the command block below is built around it.

Cisco Catalyst SD-WAN Manager CVE-2026-76504: fixed releases and indicators (Cisco PSIRT, read 2 October 2026)
CVE: CVE-2026-76504 CWE-177 CVSS 3.1 9.8 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H Advisory: cisco-sa-sdwan-webauth-xr8beuuU, first published 2026 September 30 13:00 GMT, version 1.0 Final. Cisco bug ID CSCww79570. Snort rule 67179. Found: during the resolution of a Cisco TAC support case. Exploited: yes, per Cisco PSIRT, September 2026. KEV dateAdded 2026-09-30, dueDate 2026-10-03. Scope: affects Cisco Catalyst SD-WAN Manager regardless of system configuration. Workaround: none that addresses the vulnerability. Fixed releases, by train: Earlier than 20.9 migrate to a fixed release 20.9 20.9.10.1 20.12 20.12.8.2 20.15 20.15.6.1 20.18 20.18.4.1 26.1 26.1.2.1 26.2 26.2.1 Cisco SD-WAN Cloud (Cisco managed) was fixed in release 20.15.605 with no user action required. Indicators, from the advisory. Cisco notes these can also occur during normal operation and must be assessed against your own baseline. File: /var/log/nms/containers/service-proxy/serviceproxy-access.log Audit for j_security_check requests from unknown or unauthorized addresses. Cisco's example line: [2026-09-29T23:11:13.948-05:00] "POST /%6a_security_check HTTP/1.1" 200 - 48 0 4 - "10.10.10.47,192.168.1.174" "Mozilla/5.0" "92980fc6-bb3c-4b67-8a6d-af5ceb236d4c" "vmanage-9999.example.com" "127.0.0.1:8080" File: /var/log/nms/vmanage-server.log Audit for j_security_check called for users whose names start with viptela-reserved-. Cisco's example line: 29-Sep-2026 23:11:13,952 CDT [] [vManage-new] [UserUtils] (default task-127462) |default| Request Stored in Map is (/%6a_security_check) for user (viptela-reserved-..) For compromise assessment Cisco directs customers to open a TAC case at Severity 3 with CVE-2026-76504 in the title, after running `request admin-tech` on the vManage so the admin-tech file can be provided.
%6a is one example of the encoding, not the signature. Any single percent-encoded character anywhere in the request path works, so a detection that greps literally for %6a will miss /j_securit%79_check and every other variant. The checks below therefore flag any percent-encoding in a request path and then decode to confirm, which is why there are two passes rather than one. The viptela-reserved- accounts are documented Cisco system service accounts, so their appearance in a j_security_check context is the anomaly, not the account name itself.

Am I affected?

# 1) Version. In the vManage GUI this is under Help; from the vManage CLI
#    (again a device command, not a shell one):
#      show version
#    Compare against the fixed release for your train: 20.9.10.1, 20.12.8.2,
#    20.15.6.1, 20.18.4.1, 26.1.2.1, 26.2.1. Anything earlier than 20.9 has to
#    migrate. The flaw applies regardless of configuration, so the version is
#    the whole answer.

ACCESS=/var/log/nms/containers/service-proxy/serviceproxy-access.log
SERVER=/var/log/nms/vmanage-server.log

# 2) Broad pass: ANY percent-encoded character in a request path, on any HTTP
#    method. Do not pin the method list: an encoded path sent by OPTIONS, PATCH
#    or DELETE would slip past a (GET|POST|HEAD|PUT) alternation. This pass will
#    have false positives from legitimate encoded paths, which is the right
#    trade for a first pass.
echo "== any percent-encoded request path, any method =="
grep -nE '"[A-Z]+ /[^" ]*%[0-9a-fA-F]{2}' "$ACCESS"

# 3) Precise pass: decode the path and report only the ones that resolve to the
#    authentication endpoint while having been sent encoded. Very few false
#    positives, and the same method-agnostic pattern.
echo "== decoded path is the auth endpoint but the request was encoded =="
python3 - "$ACCESS" <<'PY'
import re, sys, urllib.parse
pat = re.compile(r'"[A-Z]+ (\S+)')
for n, line in enumerate(open(sys.argv[1], errors="replace"), 1):
    m = pat.search(line)
    if not m:
        continue
    path = m.group(1).split("?", 1)[0]
    if "%" not in path:
        continue
    if urllib.parse.unquote(path).rstrip("/") == "/j_security_check":
        print(f"line {n}: ENCODED AUTH BYPASS raw={path}")
        print("   " + line.rstrip()[:200])
PY

# 4) The server-side half: an encoded path inside the UserUtils map line, and
#    any viptela-reserved- account appearing in that context.
echo "== encoded path recorded server side =="
grep -nE 'Request Stored in Map is \(/[^)]*%[0-9a-fA-F]{2}[^)]*\)' "$SERVER"
echo "== viptela-reserved- accounts in j_security_check handling =="
grep -nE 'Request Stored in Map is \([^)]*\) for user \(viptela-reserved-' "$SERVER"

# 5) Rotated and compressed copies. vManage rotates these logs, so for many
#    readers the archive is where the evidence is. Use the SAME method-agnostic
#    patterns as steps 2 and 4: a pattern anchored on %XX immediately before
#    "security" would only match the advisory's own example position and would
#    miss /j_securit%79_check and /j_security_chec%6b in the archive.
for f in "$ACCESS"* "$SERVER"*; do
  case "$f" in
    *.gz) zgrep -HnE '"[A-Z]+ /[^" ]*%[0-9a-fA-F]{2}|Request Stored in Map is \(/[^)]*%[0-9a-fA-F]{2}' "$f" 2>/dev/null ;;
  esac
done

If step 3 prints anything, that is an authentication bypass attempt against the admin API and the source addresses in the line are where to start. If step 2 prints lines that step 3 does not, read them rather than dismissing them: they are encoded requests to other paths, which may be ordinary traffic or may be the same technique aimed somewhere this bulletin did not anticipate. Note that python3 is used for the decode because a shell pattern cannot decode; if your vManage does not offer it, copy the log off the appliance and run the decode where you can.

Response

  1. Check the two logs before upgrading. The upgrade restarts the service containers and rotates logs. Cisco's own incident path starts with request admin-tech and a TAC case, which is far easier to do before you have replaced the running software.
  2. If you see encoded authentication requests, treat the controller as compromised. An attacker with the admin API on an SD-WAN controller can reach the devices it manages, so the scope is your fabric, not one host. Isolate the controller's management access, preserve the admin-tech output, open the TAC case Cisco describes at Severity 3 with the CVE in the title, and rotate controller credentials and device onboarding secrets from a clean host before you rebuild.
  3. Take the controller off the internet now, before you schedule the upgrade. Cisco's mitigation for on-prem deployments is to restrict access from unsecured networks and put the control components behind a filtering device, allowing only known trusted hosts. It does not fix the flaw, and Cisco says so, but it removes the unauthenticated network attacker the advisory describes, and it takes minutes against a maintenance window you may not get today.
  4. Then upgrade to the fixed release for your train. 20.9.10.1, 20.12.8.2, 20.15.6.1, 20.18.4.1, 26.1.2.1 or 26.2.1. There is no workaround that fixes this, so the upgrade is the remediation and everything above it is containment.

5. Next.js and satori: improper SVG escaping in the Node.js ImageResponse from next/og, scored Critical by Vercel and Medium by the CVE record (GHSA-vcvr-r3jv-pc5j, CVE-2026-94545)

Vercel published GHSA-vcvr-r3jv-pc5j on September 30, 2026, rated Critical with a CVSS v4 vector of CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H. That GHSA carries no CVE alias of its own, but it has a related CVE that covers the same flaw in both packages: CVE-2026-94545, assigned by GitHub's CNA and published the same day, which OSV and NVD both serve. The affected range for next is 16.2.0 up to but not including 16.3.6, per the advisory's own range, so a 15.x deployment is not in scope for this issue.

The condition is specific and worth checking rather than assuming. The advisory states that the Node.js ImageResponse implementation from next/og is affected by an upstream vulnerability, and that affected applications are the ones that "pass attacker-controlled values into SVG content, attributes, or styles during image generation". Its own example is an Open Graph image route that drops a query-string parameter into an SVG <title>. Two things are explicitly not affected: applications using the Edge ImageResponse implementation, and applications that do not route attacker-controlled values into SVG. The workaround, if you cannot upgrade, is to stop doing that.

The upstream component is satori, and the Next.js advisory references a second advisory for it, GHSA-wx4j-mvgx-mqwp. That GHSA page is on github.com and so could not be opened from this environment, and it is not in OSV under its own identifier either. It did not have to be opened, because CVE-2026-94545 carries the satori range and this bulletin read that record instead. Per CVE-2026-94545 as published by GitHub's CNA and served by NVD and OSV: satori is affected from 0.0.27 up to but not including 0.33.5, and "Version 0.33.5 contains a patch." npm confirms 0.33.5 exists, published September 22, 2026 at 16:01 UTC, eighteen minutes before next 16.3.6. So if you depend on satori directly rather than only through next/og, your floor is 0.33.5, not the 16.3.6 that covers next.

The two primary records disagree about how bad this is, and the disagreement is worth carrying rather than resolving. Vercel's advisory calls it remote code execution and scores it Critical, with VC:H/VI:H/VA:H on the vulnerable system. CVE-2026-94545 describes the same defect as improper escaping (CWE-116) that lets "crafted values be interpreted as SVG markup", says "the impact depends on how the generated SVG is consumed", and scores it 5.3 Medium with CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N, that is, no impact on the vulnerable system and high impact on subsequent ones. Both are reproduced here because this bulletin cannot settle which is right from the outside, and the action is the same under either reading: take 16.3.6 or later, take satori 0.33.5 or later if you use it directly, and stop rendering attacker-controlled values into SVG. The priorities table places this at Higher on Vercel's reading and on the size of the installed base, not on the CVE's score.

Next.js and satori: affected and fixed (GitHub Advisory Database and CVE-2026-94545 via OSV, NVD, npm registry, read 2 October 2026)
Vercel advisory (next): GHSA: GHSA-vcvr-r3jv-pc5j CWE-1395 (dependency on a vulnerable component) Severity: Critical, CVSS v4.0 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H Published: 2026-09-30 14:48 UTC. GitHub-reviewed the same day. No CVE alias of its own; see the related CVE below. Related CVE, covering BOTH packages (CNA security-advisories@github.com): CVE: CVE-2026-94545 CWE-116 (improper encoding or escaping of output) Severity: 5.3 MEDIUM, CVSS v4.0 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N Published: OSV 2026-09-30 14:53 UTC; NVD 2026-09-30 15:22 UTC Text: "Starting in version 0.0.27 and prior to version 0.33.5, Satori does not properly escape certain values before including them in generated SVG output. This can allow crafted values to be interpreted as SVG markup. The impact depends on how the generated SVG is consumed. Version 0.33.5 contains a patch." The Critical and Medium ratings are both reproduced; this bulletin does not pick between them. See the paragraph above. next (npm) Affected: >= 16.2.0 < 16.3.6 Fixed: 16.3.6 published 2026-09-22 16:19 UTC Current: 16.3.8 published 2026-09-30 16:07 UTC Not in scope per the advisory's range: next < 16.2.0, including every 15.x. satori (npm), the upstream component Affected: >= 0.0.27 < 0.33.5 per CVE-2026-94545 Fixed: 0.33.5 published 2026-09-22 16:01 UTC Current: 0.35.0 published 2026-10-02 12:32 UTC Workaround per the CVE: none complete. "Applications that cannot immediately upgrade should not render attacker-controlled content with Satori." The GHSA for satori, GHSA-wx4j-mvgx-mqwp, could NOT be opened from this environment (github.com is restricted here and the id is not in OSV on its own). The range above is therefore taken from CVE-2026-94545, not from that GHSA, and is attributed accordingly. Conditions, per the Vercel advisory: AFFECTED Node.js ImageResponse from next/og, where attacker-controlled values reach SVG content, attributes or styles NOT affected Edge ImageResponse implementation NOT affected applications that never pass attacker-controlled values into SVG Fix commits referenced by the records: vercel/next.js 868fad38690d72088868f299fa2bef339b26838e vercel/satori 26a52affc031216fee5882b6e965c8dbc7ac1782 (PR vercel/satori#814)
Note the date order: both fixes shipped on September 22, satori 0.33.5 at 16:01 UTC and next 16.3.6 at 16:19 UTC, and the advisories published on September 30, so a project that was keeping current may already have been patched for over a week before the disclosure. The Vercel advisory text and the next range were read from the OSV mirror of the GitHub Advisory Database, which carries the advisory body verbatim, because github.com is not reachable from this environment. The satori range, the quoted text and the Medium score come from CVE-2026-94545 as served by both OSV and NVD. All four release timestamps were read from the npm registry on October 2, 2026. There are no indicators of compromise for this item: successful exploitation looks like ordinary traffic to your own Open Graph image route.

Am I affected?

# 1) Which next is actually installed, and is it in range. Run from the project
#    root. require.resolve is pinned to process.cwd() deliberately: a bare
#    require() resolves from the script's own directory and would report
#    "not installed" on an affected project, which is the worst possible answer.
node -e '
const fs = require("fs"), path = require("path");
// Locate a package manifest WITHOUT require("pkg/package.json"). A package with an
// "exports" map does not export ./package.json, so that form throws, and a catch
// around it would report an AFFECTED project as "not installed". Walk node_modules
// upward from the cwd, then fall back to resolving the module entry and walking up
// from there, which covers pnpm and linked layouts.
function manifest(name, from) {
  let dir = path.resolve(from);
  for (;;) {
    const p = path.join(dir, "node_modules", name, "package.json");
    try { if (fs.statSync(p).isFile()) return p; } catch (e) {}
    const up = path.dirname(dir); if (up === dir) break; dir = up;
  }
  try {
    let d = path.dirname(require.resolve(name, { paths: [from] }));
    for (;;) {
      const p = path.join(d, "package.json");
      try { if (fs.statSync(p).isFile() && JSON.parse(fs.readFileSync(p, "utf8")).name === name) return p; } catch (e) {}
      const up = path.dirname(d); if (up === d) break; d = up;
    }
  } catch (e) {}
  return null;
}
const RULES = {
  "next":                 [{from:"16.2.0", fixed:"16.3.6"}],
  "satori":               [{from:"0.0.27", fixed:"0.33.5"}],
  "vm2":                  [{from:"0.0.0",  fixed:"3.11.7"}],
  "piscina":              [{from:"0.0.0",  fixed:"4.9.4"},
                           {from:"5.0.0",  fixed:"5.3.2"},
                           {from:"6.0.0-rc.1", fixed:"6.0.0-rc.5"}],
  "@xhmikosr/decompress": [{from:"0.0.0",  fixed:"10.2.2"},
                           {from:"11.0.0", fixed:"11.1.4"}],
  "decompress":           [{from:"0.0.0",  fixed:null, note:"no fix exists: migrate to @xhmikosr/decompress"}]
};
const key = v => { const [c, p = ""] = String(v).split("-", 2);
  const n = c.split(".").map(x => parseInt(x, 10) || 0); while (n.length < 3) n.push(0); return {n, p}; };
const cmp = (a, b) => { const A = key(a), B = key(b);
  for (let i = 0; i < 3; i++) if (A.n[i] !== B.n[i]) return A.n[i] - B.n[i];
  if (A.p === B.p) return 0; if (!A.p) return 1; if (!B.p) return -1; return A.p < B.p ? -1 : 1; };
for (const [pkg, ranges] of Object.entries(RULES)) {
  const m = manifest(pkg, process.cwd());
  if (!m) { console.log(pkg.padEnd(24), "no manifest found under " + process.cwd()); continue; }
  const v = JSON.parse(fs.readFileSync(m, "utf8")).version;
  const hit = ranges.find(r => cmp(v, r.from) >= 0 && (r.fixed === null || cmp(v, r.fixed) < 0));
  console.log(pkg.padEnd(24), String(v).padEnd(12),
    hit ? ">>> AFFECTED: " + (hit.fixed ? "upgrade to " + hit.fixed + " or later" : hit.note)
        : "not in an affected range");
}'

# 2) Every copy in the tree, including duplicates at different depths. node -e
#    above reports the copy that resolves from the project root; this reports all
#    of them. Works for yarn and pnpm trees too, where grepping the lockfile does
#    not.
npm ls next satori vm2 piscina decompress @xhmikosr/decompress --all 2>/dev/null

# 3) Do you actually use next/og, and on which runtime? Both matter: the Edge
#    implementation is not affected.
grep -rEn "from ['\"]next/og['\"]|require\(['\"]next/og['\"]\)" \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' \
  --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=.git .

echo "-- routes that opt into the Edge runtime (not affected) --"
grep -rEn "export const runtime[[:space:]]*=[[:space:]]*['\"]edge['\"]" \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \
  --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=.git .

# 4) Does request data reach the SVG? This is a reading aid, not a verdict: it
#    lists the ImageResponse call sites so you can check each one by hand.
grep -rEn "new ImageResponse\(" \
  --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' \
  --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=.git . 

Step 4 is explicitly not a decision procedure. Whether a value is attacker-controlled depends on where it came from several frames earlier, and no grep settles that. What the list gives you is a bounded set of call sites to read, which for most projects is one or two. If you cannot answer the question quickly for a given route, upgrade and move on: 16.3.6 or later removes the question.

Response

  1. Upgrade next to 16.3.6 or later. 16.3.8 is current as of October 2, 2026. This is a patch-level move inside 16.3, so for most projects it is a lockfile re-resolve and a redeploy. A lockfile change that never reaches a running container has fixed nothing, so rebuild the image.
  2. While you are there, stop passing request data into SVG from an image route. This is the advisory's own workaround and it is also the durable fix: the route that renders a user-supplied string into an SVG <title> is doing template injection into a renderer, and the next renderer bug will land on the same code. Render from values you control, or escape and constrain hard.
  3. If you depend on satori directly, raise it to 0.33.5 or later as a separate change. That floor comes from CVE-2026-94545, not from the Next.js advisory, and the two packages are on separate release schedules, so upgrading next does nothing for a direct satori dependency and vice versa. Check both. The CVE says no complete workaround exists other than upgrading, and that applications which cannot upgrade should not render attacker-controlled content through satori.
  4. Check whether 16.3.6 is actually what you deployed, not just what you resolved. Run the step 1 check inside the running container or on the deployed instance. Next.js projects frequently have more than one copy of the framework in a monorepo, which is what step 2 is for.

6. PyJWT: a whitespace-mutated PEM slips past the HMAC confusion guard, and the public key becomes the signing secret (CVE-2026-102268)

GHSA-ffc3-869f-jxw9 is an incomplete-guard bypass of the CVE-2022-29217 family, rated Critical with CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N and CWE-347. NVD published it on September 28, 2026 and the GitHub Advisory Database reviewed it in on September 29. Every PyJWT release up to and including 2.13.0 is affected; the fix is 2.14.0, which shipped on September 11, 2026, more than two weeks before the advisory. 2.15.1 is current as of today.

The mechanism is a mismatch between two parsers. PyJWT's HMACAlgorithm.prepare_key refuses to use an asymmetric key as an HMAC secret, but only when is_pem_format() recognizes the key as PEM. That recognizer is a regular expression requiring the BEGIN and END markers to sit directly after a newline with mandatory LF line terminators, while cryptography's load_pem_public_key() accepts considerably more. Insert a tab before -----END, use CR-only line terminators, or fold the PEM onto a single line, and the guard does not fire while the key still loads. The public key is then used directly as the HMAC secret, and anyone who knows the public key, which is everyone, can mint a valid HS256 token. The advisory calls it universal forgery and includes a reproduction on 2.13.0 with cryptography 49.0.0 showing each mutation forging a token accepted as superadmin.

Both preconditions are deployment properties rather than things an attacker chooses at request time, which is what keeps this at Medium rather than Higher. First, the jwt.decode allow-list has to mix an HMAC algorithm with an asymmetric one, for example algorithms=["ES256", "HS256"], which is the RFC 8725 footgun the guard exists to backstop. Second, the key has to be passed as raw PEM text or bytes on the non-PyJWK path in one of those mutated forms. The advisory is specific about where such forms come from in practice, and the list should be familiar: an indented YAML or JSON block, a single-line environment variable, or a CR/LF round-trip through config tooling. If your allow-list mixes families and your key comes out of an environment variable, treat this as Higher for you.

PyJWT CVE-2026-102268: affected and fixed (GitHub Advisory Database via OSV, PyPI, read 2 October 2026)
CVE: CVE-2026-102268 GHSA-ffc3-869f-jxw9 PYSEC-2026-4145 CWE: CWE-347 (improper verification of cryptographic signature) Severity: Critical CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N NVD published: 2026-09-28 21:17 UTC Reviewed into GitHub Advisory DB: 2026-09-29 23:17 UTC Affected: pyjwt <= 2.13.0 (every release, back to 0.1.1) Fixed: 2.14.0 released on PyPI 2026-09-11 13:11 UTC Current: 2.15.1 Note the order: the fix predates the advisory by 17 days. This is a disclosure landing in this window, not a vulnerability introduced in it. Preconditions (both required, both deployment properties): 1. a jwt.decode algorithms= allow-list mixing HMAC with asymmetric, e.g. ["ES256","HS256"] or ["RS256","HS256"] 2. the verification key passed as raw PEM text or bytes on the non-PyJWK path, in a form cryptography loads but PyJWT's is_pem_format regex misses: - whitespace or indentation adjacent to the BEGIN/END markers - CR-only line terminators - the whole PEM folded onto a single line plus: cryptography installed, and the attacker knows the public key (which is public by definition) Code path named by the advisory: jwt/api_jws.py:386 allow-list check passes because HS256 is listed jwt/api_jws.py:407 non-PyJWK branch calls alg_obj.prepare_key(key) jwt/algorithms.py:331 guard gated on is_pem_format(key_bytes) or is_ssh_key jwt/utils.py:116-127 is_pem_format, the regex that accepts fewer forms jwt/algorithms.py:357 returns the PEM bytes unchanged as the HMAC secret Fix commit: jpadilla/pyjwt 8b4e233a22206b34ec1186e912e75c0b2396ac07
Version numbers and the CVSS vector are from the advisory as mirrored in OSV; the 2.14.0 upload timestamp and the current 2.15.1 are from the PyPI JSON API, read October 2, 2026. There are no indicators of compromise: a forged HS256 token that verifies looks exactly like a valid token to the application, which is the point. If you were vulnerable and care whether it was used, the place to look is your own authentication logs for sessions whose token algorithm header was HS256 on an endpoint that should only ever see asymmetric signatures.

Am I affected?

# 1) What is installed in the interpreter the service actually uses. Run this
#    inside the container or virtualenv, not on the host.
python3 -c "
from importlib.metadata import version, PackageNotFoundError
for name, fixed in (('pyjwt','2.14.0'),):
    try:
        v = version(name)
        bad = tuple(int(x) for x in v.split('.')[:3]) < tuple(int(x) for x in fixed.split('.'))
        print(f'{name}: installed {v}, fixed in {fixed}:', 'AFFECTED' if bad else 'ok')
    except PackageNotFoundError:
        print(f'{name}: not installed in this interpreter')
"

# 2) Does any decode allow-list mix an HMAC algorithm with an asymmetric one?
#    This is the precondition that turns the bug into universal forgery.
python3 - . <<'PY'
import os, re, sys
# Three shapes, because only matching algorithms=[...] at the call site misses
# the two most common real ones:
#   1. inline, list or TUPLE:  algorithms=["ES256","HS256"]  /  ("ES256","HS256")
#   2. a module-level constant: ALGS = ["ES256","HS256"] ... algorithms=ALGS
# A list assembled at runtime (appends, a config lookup, a comprehension) matches
# none of these and still has to be read by hand: step 3 lists the decode sites.
INLINE = re.compile(r'algorithms\s*=\s*[\[(]([^\])]*)[\])]', re.S)
ASSIGN = re.compile(r'^[ \t]*([A-Za-z_][A-Za-z_0-9]*)\s*=\s*[\[(]([^\])]*)[\])]', re.M | re.S)
NAMED  = re.compile(r'algorithms\s*=\s*([A-Za-z_][A-Za-z_0-9.]*)')
HMAC = re.compile(r'HS(256|384|512)')
ASYM = re.compile(r'(RS|ES|PS)(256|384|512)|EdDSA|Ed25519')
SKIP = {'.git', 'node_modules', '.venv', 'venv', '__pycache__', '.tox', 'site-packages'}
def mixed(body):
    return bool(HMAC.search(body) and ASYM.search(body))
hits = 0
for root, dirs, files in os.walk(sys.argv[1]):
    dirs[:] = [d for d in dirs if d not in SKIP]
    for f in files:
        if not f.endswith('.py'):
            continue
        p = os.path.join(root, f)
        try:
            s = open(p, errors='replace').read()
        except OSError:
            continue
        for m in INLINE.finditer(s):
            if mixed(m.group(1)):
                line = s.count('\n', 0, m.start()) + 1
                print(f'{p}:{line}: MIXED allow-list inline: {" ".join(m.group(1).split())}')
                hits += 1
        # constants that are mixed, reported when something passes them as the
        # allow-list anywhere in the same file
        names = {m.group(1): (s.count('\n', 0, m.start()) + 1, m.group(2))
                 for m in ASSIGN.finditer(s) if mixed(m.group(2))}
        for m in NAMED.finditer(s):
            ref = m.group(1).split('.')[-1]
            if ref in names:
                line = s.count('\n', 0, m.start()) + 1
                dl, body = names[ref]
                print(f'{p}:{line}: MIXED allow-list via {m.group(1)} '
                      f'(defined line {dl}): {" ".join(body.split())}')
                hits += 1
print(f'-- {hits} mixed allow-list(s) found --')
print('-- a list built at runtime matches none of these: read the decode sites '
      'that step 3 lists --')
PY

# 3) Where does the verification key come from? A PEM arriving through an
#    environment variable or an indented YAML block is the shape that triggers
#    this. These greps list candidates to read, they are not a verdict.
grep -rEn "BEGIN (PUBLIC KEY|RSA PUBLIC KEY|CERTIFICATE)" \
  --include='*.yml' --include='*.yaml' --include='*.json' --include='*.env' \
  --exclude-dir=.git --exclude-dir=node_modules .
grep -rEn "jwt\.decode\(" --include='*.py' --exclude-dir=.git --exclude-dir=node_modules . 

The importlib.metadata check is the authoritative one for a running service because it reports what the interpreter would import. Step 2 prints a count on the last line on purpose: zero mixed allow-lists found is a different statement from the scan not having run, and without the count those two look identical.

Response

  1. Raise the floor to pyjwt>=2.14.0 and re-resolve. 2.15.1 is current. Because the fix shipped on September 11, many lockfiles already satisfy it; confirm with the step 1 check inside the deployed environment rather than trusting the lockfile.
  2. Split the mixed allow-lists, whatever version you end up on. An endpoint verifying asymmetric signatures should list only asymmetric algorithms. The guard this advisory bypasses exists only because mixed lists are accepted at all, and removing the mix removes the whole class, including the next bypass of the same guard.
  3. Normalize or stop passing raw PEM. Prefer the PyJWK path, which does not reach the guard at all. If you must pass PEM text, load it from a file rather than an environment variable or an inline YAML block, and if it has been through config tooling, check that it still has LF terminators and no indentation next to the markers.
  4. If you had both preconditions, treat issued sessions as suspect. Universal forgery means an attacker could mint any identity. Rotate the signing keypair, invalidate outstanding sessions, and look for HS256 tokens in your authentication logs on endpoints that should only ever have seen asymmetric ones.

7. vm2: fifteen advisories at once, ten of them critical sandbox escapes, all fixed in a release that shipped five weeks earlier (CVE-2026-92935 and fourteen others)

On October 1, 2026 fifteen advisories for the npm package vm2 were published to the GitHub Advisory Database in a single batch. Ten are rated Critical, two High, three Moderate. Every one of them is fixed in vm2 3.11.7, which was published to npm on August 24, 2026. The current release is 3.12.2, published September 8, 2026. So the upgrade has been available for over a month and the disclosure is what is new, which is the correct way to read this item: if your scanners just lit up, the fix is a version bump you could have taken in August.

Taken together they say something more useful than any one of them does. The escapes come through node:sqlite, through node:test.run() and its execArgv, through the crypto builtin's setEngine loading attacker native code, through a stale protector on Node.js 26, through an array-shaped require that the nesting guard accepted, through node:-prefixed builtin names bypassing the denylist to reach child_process, through a custom module resolver loading a colliding host package, and through fs/promises despite -fs. Two more are not escapes but are arguably worse in a shared process: the sandbox can replace the host process TLS trust store (CVE-2026-92941) and can read host HTTPS credentials and TLS traffic through globalAgent (CVE-2026-92940). Three are allowlist, freeze and symbol-filtering weaknesses, and one, CVE-2026-92950, says the vm2 CLI provides no sandbox isolation at all, with host-realm require() reachable from sandboxed scripts.

This audience hits vm2 most often in two places: plugin or template evaluation in a self-hosted app, and the sandbox somebody reached for when they needed to run model-generated or user-supplied JavaScript. The second is the one worth being blunt about. A batch like this is what a mature in-process JavaScript sandbox looks like under sustained scrutiny, and the pattern of a guard being bypassed by a shape the guard did not anticipate repeats across eight of the fifteen. Upgrade, and then plan to stop relying on an in-process sandbox as the boundary between untrusted code and your host.

vm2: the fifteen advisories published 2026-10-01, all fixed in 3.11.7 (GitHub Advisory Database via OSV, npm registry, read 2 October 2026)
Affected: vm2 < 3.11.7 (every advisory in this batch) Fixed: 3.11.7 published to npm 2026-08-24 22:22 UTC Current: 3.12.2 published to npm 2026-09-08 16:28 UTC Previous: 3.11.6 published to npm 2026-08-14 20:11 UTC All fifteen advisories published to the GitHub Advisory Database 2026-10-01. The fix therefore predates the disclosure by 38 days. CRITICAL (10) CVE-2026-92935 GHSA-8hr7-r645-pc6w NodeVM nesting guard accepts array-shaped require, permits host RCE (from 3.11.4) CVE-2026-92937 GHSA-647f-g98j-qq25 bypass of the GHSA-m283-3h24-438v fix, host RCE via call/apply (from 3.11.6) CVE-2026-92938 GHSA-6w8r-xxw2-g3hx sandboxed plugin executes native code through node:sqlite (from 3.11.3) CVE-2026-92939 GHSA-46pr-c5wc-xffx crypto builtin loads attacker native code through setEngine (from 3.11.3) CVE-2026-92940 GHSA-h85j-hv3c-qfgq exposes host HTTPS credentials and TLS traffic through globalAgent (from 3.11.3) CVE-2026-92941 GHSA-98xx-8mx4-x7cm NodeVM can replace the host process TLS trust store (from 3.11.3) CVE-2026-92944 GHSA-27g9-p43v-cw3v sandbox escape on Node.js 26 via a stale PromiseThenLookupChain (from 3.10.2) CVE-2026-92948 GHSA-qhwx-74w5-xhxq builtin allowlist bypass via node:test.run() execArgv (from 3.9.6) CVE-2026-92951 GHSA-c48m-32m9-vx93 custom module resolver bypasses the external allowlist (all versions) CVE-2026-92957 GHSA-8686-vhfx-7r3j node:-prefixed negative builtin deny bypass exposes child_process (all versions) HIGH (2) CVE-2026-92950 GHSA-jxxv-8r27-vm4p the vm2 CLI provides no sandbox isolation: host-realm require() is reachable CVE-2026-92958 GHSA-6rh5-qq4q-97xh NodeVM builtin denylist bypass via fs/promises despite -fs MODERATE (3) CVE-2026-92945 GHSA-7q3f-wx44-378m allowlist prefix test treats a prefix-sharing sibling as allowlisted CVE-2026-92949 GHSA-633r-hq9m-c4ff vm.freeze() / vm.readonly() bypass via an accessor descriptor (from 3.9.6) CVE-2026-92952 GHSA-jf8q-945g-9q4c incomplete nodejs.* symbol filtering lets the sandbox override host WebStream state (from 3.11.4) "from X" is the advisory's introduced version where OSV records one; the five without it are introduced at 0, meaning every release below 3.11.7. Upgrading to 3.11.7 or later clears all fifteen, so the per-advisory introduced versions matter only if you are deciding how long you were exposed.
Advisory identifiers, severities, introduced versions and the common 3.11.7 fix were read from the OSV mirror of the GitHub Advisory Database; the three npm publication timestamps were read from the npm registry, both on October 2, 2026. The severities are the GitHub Advisory Database ratings. There are no indicators of compromise: a successful sandbox escape runs as your Node process, so the evidence is whatever the escaped code then did, not a signature in vm2 itself.

Am I affected?

# 1) Version check. The node -e block in item 5 covers vm2 as well; this is the
#    single-package form if that is all you need.
node -e '
const fs = require("fs"), path = require("path");
// Same manifest walk as item 5, for the same reason: require("vm2/package.json")
// throws on any package with an exports map, and a silent catch reads as "clean".
let dir = path.resolve(process.cwd()), m = null;
for (;;) {
  const p = path.join(dir, "node_modules", "vm2", "package.json");
  try { if (fs.statSync(p).isFile()) { m = p; break; } } catch (e) {}
  const up = path.dirname(dir); if (up === dir) break; dir = up;
}
if (!m) { console.log("no vm2 manifest found under " + process.cwd() + " (see the find fallback below)"); }
else {
  const v = JSON.parse(fs.readFileSync(m, "utf8")).version;
  const n = v.split("-")[0].split(".").map(Number);
  const bad = n[0] < 3 || (n[0] === 3 && (n[1] < 11 || (n[1] === 11 && n[2] < 7)));
  console.log("vm2", v, bad ? ">>> AFFECTED: upgrade to 3.11.7 or later (3.12.2 is current)"
                            : "not in an affected range");
}'

# 2) Every copy in the tree, including transitive ones you did not ask for.
#    vm2 is frequently pulled in by something else, which is how it survives in
#    projects that never chose it.
npm ls vm2 --all 2>/dev/null
#    If npm ls is unavailable (pnpm store layouts, vendored trees), find the
#    installed manifests directly:
find . -path '*/node_modules/vm2/package.json' -not -path '*/.git/*' \
  -exec sh -c 'printf "%s  " "$1"; grep -o "\"version\"[^,]*" "$1"' _ {} \;

# 3) Where is it used, and is the input untrusted? These are the call shapes.
grep -rEn "require\(['\"]vm2['\"]\)|from ['\"]vm2['\"]|new (VM|NodeVM|VMScript)\(" \
  --include='*.js' --include='*.cjs' --include='*.mjs' --include='*.ts' \
  --exclude-dir=node_modules --exclude-dir=.git .

# 4) Is the CLI in use anywhere? CVE-2026-92950 says it offers no isolation at all.
grep -rEn "(^|[^a-z])vm2($|[^a-z-])" --include='*.json' --include='*.sh' \
  --include='Makefile' --include='*.yml' --include='*.yaml' \
  --exclude-dir=node_modules --exclude-dir=.git . \
  | grep -vE '"vm2"[[:space:]]*:|"resolved"|"integrity"|/vm2/-/vm2-|node_modules/vm2'

Step 2 matters more than usual here. vm2 is the kind of dependency that arrives through a template engine or a rules engine three levels down, so a project whose package.json has never mentioned it can still ship it. The find fallback is there because npm ls reports what npm installed, and a pnpm or vendored tree can hold copies it does not see.

Response

  1. Upgrade to 3.11.7 or later. 3.12.2 is current as of October 2, 2026. One bump clears all fifteen, including the ten critical escapes. If the copy is transitive, raise it with an override or a resolution and confirm with step 2 afterwards rather than assuming the override took.
  2. Work out what an escape would have reached, and rotate it. Two of the critical issues are not escapes but disclosure: CVE-2026-92940 exposes host HTTPS credentials and TLS traffic through globalAgent, and CVE-2026-92941 lets the sandbox replace the host trust store. If you ran untrusted code in vm2 below 3.11.7 in a process that also held API keys or made outbound TLS calls, those secrets should be treated as exposed and rotated, independently of whether you can show an escape happened.
  3. Stop treating vm2 as the boundary for genuinely untrusted code. Eight of the fifteen are a guard being defeated by an input shape it did not anticipate, which is the failure mode of in-process sandboxing rather than a bug in this library. If you are running model-generated or user-supplied JavaScript, move it to a separate process with an OS-level boundary, or to a runtime designed for it, and keep vm2 for code you merely want to isolate from your own globals.
  4. If you use the vm2 CLI, treat it as providing no isolation at all. CVE-2026-92950 is explicit that host-realm require() is reachable from scripts run through it. That is not a configuration to tighten, it is a tool to stop using for untrusted input.

8. Apple CoreGraphics: an out-of-bounds write exploited against targeted individuals, patched for macOS and iOS development machines (CVE-2026-86950)

Apple published security notes 149226, 149228 and 149229 on September 28, 2026 for iOS 26.7.1 and iPadOS 26.7.1, macOS Tahoe 26.7.1, and macOS Sequoia 15.8.1. Each carries a single CoreGraphics entry: an out-of-bounds write (CWE-787), fixed with improved bounds checking, where "Processing a maliciously crafted file may lead to arbitrary code execution." Apple adds the sentence that puts it in this bulletin: "Apple is aware of a report that this issue may have been exploited in an extremely sophisticated attack against specific targeted individuals on versions of iOS before iOS 27." The issue is credited to Meta Product Security. CISA added it to KEV on September 29 with a due date of October 2, which is today. NVD records a secondary CVSS 3.1 score of 8.8 with user interaction required.

This is in the bulletin because a large share of this audience does its work on a Mac, and a CoreGraphics file-parsing bug is reached by opening an image or a PDF, which is an ordinary thing to do with an attachment or a design asset. It sits at Medium rather than Higher because Apple's own characterization is a sophisticated attack against specific targeted individuals, not commodity exploitation, and because the update is a routine one most readers will take anyway. If you are a plausible target for that kind of attack, which for this audience usually means maintaining widely-depended-upon software or holding production signing keys, move it up your list.

Apple CVE-2026-86950: affected and fixed (Apple security notes, NVD, CISA KEV, read 2 October 2026)
CVE: CVE-2026-86950 CWE-787 (out-of-bounds write), CoreGraphics Credit: Meta Product Security NVD published: 2026-09-28 20:17 UTC. Secondary CVSS 3.1 8.8 HIGH, CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H The UI:R (user interaction required) is why this is 8.8 and not 9.8: someone has to open the crafted file. Apple assigns no CVSS score of its own. KEV: dateAdded 2026-09-29, dueDate 2026-10-02 Exploited: Apple states it "may have been exploited in an extremely sophisticated attack against specific targeted individuals on versions of iOS before iOS 27". That is Apple's wording and is narrower than commodity exploitation. Fixed in, all released 2026-09-28: iOS 26.7.1 and iPadOS 26.7.1 support.apple.com/en-us/149226 Available for: iPhone 11 and later, iPad Pro 12.9-inch 3rd generation and later, iPad Pro 11-inch 1st generation and later, iPad Air 3rd generation and later, iPad 8th generation and later, iPad mini 5th generation and later macOS Tahoe 26.7.1 support.apple.com/en-us/149228 macOS Sequoia 15.8.1 support.apple.com/en-us/149229 NVD CPE ranges for the same CVE: iphone_os up to 26.7.1 (excluding) ipados up to 26.7.1 (excluding) macos up to 15.8.1 (excluding) macos 26.0 up to 26.7.1 (excluding) Not covered: Apple published no note for an older macOS train in this set, so a machine on macOS Sonoma 14.x or earlier has no listed fix for this CVE. That is what the notes say; it is not a statement that such a machine is unaffected.
Version strings, release date, affected device list and the exploitation sentence are quoted from Apple's three security notes; the CVSS figure and CPE ranges are from the NVD record, and the KEV dates from the catalog, all read October 2, 2026. There are no indicators of compromise: Apple publishes none for this class of issue, and a successful CoreGraphics exploit leaves nothing a user can grep for. Apple's notes name no mitigation for this CVE. Lockdown Mode is this bulletin's own suggestion, because it narrows the file-parsing surface this class of bug lives in, and it is a posture change rather than a patch or a vendor statement about CVE-2026-86950.

Am I affected?

# macOS only. There is no /proc and no ss on a Mac, and nothing in this block
# applies on Linux. For iOS and iPadOS, read Settings, General, About, Software
# Version and compare against 26.7.1.
V="$(sw_vers -productVersion 2>/dev/null)"
if [ -z "$V" ]; then
  echo "sw_vers not available: this check is macOS only, and this host is not a Mac"
else
  case "$V" in
    15.*) NEED=15.8.1; TRAIN="Sequoia" ;;
    26.*) NEED=26.7.1; TRAIN="Tahoe" ;;
    *)    NEED=""; TRAIN="" ;;
  esac
  if [ -z "$NEED" ]; then
    echo "macOS $V is on neither the Sequoia 15.x nor the Tahoe 26.x train that this CVE was patched on. Apple published no note for other trains in this set, so there is no listed fix for yours: check Apple's security releases page."
  else
    awk -v have="$V" -v need="$NEED" -v train="$TRAIN" 'BEGIN{
      nh=split(have,h,"."); nn=split(need,n,".");
      for(i=1;i<=3;i++){ hv=(i<=nh?h[i]+0:0); nv=(i<=nn?n[i]+0:0);
        if(hv<nv){ printf "macOS %s (%s): AFFECTED, patched in %s\n", have, train, need; exit }
        if(hv>nv){ printf "macOS %s (%s): at or above the patched %s\n", have, train, need; exit } }
      printf "macOS %s (%s): at the patched version %s\n", have, train, need; }'
  fi
fi

# What the update mechanism thinks is pending
softwareupdate -l 2>&1 | sed -n '1,20p'

# Fleet view: if you manage Macs with MDM, the same question asked once. For a
# handful of machines over ssh:
# for h in mac1 mac2 mac3; do printf '%s: ' "$h"; ssh "$h" sw_vers -productVersion; done

The version comparison is written out rather than left to the reader because the two supported trains need different targets and 26.7.1 sorts below 15.8.1 under a naive string comparison. The "neither train" branch deliberately does not say you are safe: Apple shipped notes for Sequoia, Tahoe, iOS and iPadOS only, so an older macOS has no listed fix rather than no exposure.

Response

  1. Update to macOS Sequoia 15.8.1, macOS Tahoe 26.7.1, or iOS and iPadOS 26.7.1. These are small security updates on existing trains, not a major version move, so they belong in this week's workstation cadence rather than a planning cycle.
  2. Prioritize the machines that hold signing keys, deploy credentials, or maintainer access. Apple's own framing is targeted attacks, and in this audience the people who get targeted are the ones whose workstation is a path into something widely used. A developer laptop with npm or PyPI publish rights is that machine.
  3. If you are a plausible target and cannot update immediately, consider Lockdown Mode. Apple's notes for this CVE name no mitigation, so this is this bulletin's suggestion and not a vendor recommendation: Lockdown Mode narrows the file-parsing surface that this class of bug lives in. It is not a patch, it has real functional costs, and it is a stopgap.
  4. Check the older Macs rather than assuming they inherited a fix. A machine on a train Apple did not publish a note for has no fix for this CVE. If it cannot move to Sequoia 15.8.1 or later, it should not be the machine opening untrusted attachments.

9. decompress: a symlink chain walks the extraction out of the output directory, and the package most projects actually depend on will never be fixed (CVE-2026-101894)

GHSA-hrh2-vp3x-79xf is rated Critical, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N, CWE-22 and CWE-59. NVD published it on September 28, 2026; the GitHub Advisory Database reviewed it in on September 29. The fixes, @xhmikosr/decompress 11.1.4 and 10.2.2, both shipped on August 5, 2026, so like items 5, 6, 7 and 10 this is a disclosure landing in this window rather than a new flaw.

The bug is a good illustration of why lexical path checks are not enough. Extracting an untrusted archive with the default decompress(input, output) API, an archive containing a chain of symlink entries can make a later entry resolve outside output. The containment check passes because it is performed on the path as written, and then the kernel follows the symlinks the earlier entries planted. The advisory notes this is a bypass of the hardening in GHSA-mp2f-45pm-3cg9, and that overwriting startup scripts or configuration gets you from arbitrary write to remote code execution. Any application that extracts attacker-controlled archives is affected, and the advisory says there is no workaround beyond not extracting untrusted archives.

The part to act on is the second package. The advisory states plainly: "The unmaintained upstream decompress package shares this flaw and will not be patched." OSV records decompress with a last_affected of 4.2.1, and 4.2.1 is still the latest dist-tag on npm as of October 2, 2026, with 41 versions and nothing newer. decompress is a very common transitive dependency, so the realistic finding for most readers is not "we use decompress" but "something we use uses decompress", and the remediation is a migration or a removal rather than a bump.

decompress CVE-2026-101894: affected and fixed (GitHub Advisory Database via OSV, npm registry, read 2 October 2026)
CVE: CVE-2026-101894 GHSA-hrh2-vp3x-79xf CWE: CWE-22 (path traversal) and CWE-59 (link following) Severity: Critical CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N NVD published: 2026-09-28 17:17 UTC Reviewed into GitHub Advisory DB: 2026-09-29 23:49 UTC @xhmikosr/decompress (maintained fork) Affected: >= 11.0.0 < 11.1.4 (last known affected 11.1.3) Affected: < 10.2.2 (last known affected 10.2.1) Fixed: 11.1.4 published to npm 2026-08-05 15:16 UTC dist-tag: latest Fixed: 10.2.2 published to npm 2026-08-05 15:16 UTC dist-tag: release-v10 The fixes predate the disclosure by 54 days. decompress (unmaintained upstream) Affected: all versions, last affected 4.2.1 Fixed: NONE. The advisory states it "shares this flaw and will not be patched". 4.2.1 is still the latest dist-tag on npm as of 2026-10-02, with no release after it. Action: migrate to @xhmikosr/decompress 11.1.4 (or 10.2.2), or remove it. Trigger: the default decompress(input, output) API on an untrusted archive containing a chain of symlink entries Prior fix bypassed: GHSA-mp2f-45pm-3cg9 Workaround: none. If you cannot upgrade, do not extract untrusted archives; validating entries out of band and rejecting any whose RESOLVED path escapes the target is what the advisory suggests instead. Fix commits (XhmikosR/decompress): 5f4b2f64abb31bbaf1fef8975b595fd7df2558d8 f6c88c668216d6a12c6cecf4fe0b6c70bf77050a
Ranges and the "will not be patched" statement are from the advisory as mirrored in OSV; publication timestamps and the current dist-tags for both packages are from the npm registry, read October 2, 2026. There are no indicators of compromise published for this item. If you have been extracting untrusted archives on an affected version, the thing to look for is files modified outside the extraction directory around the time of an upload, which is a filesystem timeline question rather than a signature.

Am I affected?

# 1) Both package names, every copy in the tree. The node -e block in item 5
#    covers both; this is the targeted form.
npm ls decompress @xhmikosr/decompress --all 2>/dev/null
#    ...and the verdict, not just the inventory. Fixed: @xhmikosr/decompress
#    11.1.4 on the 11.x line and 10.2.2 below it; upstream decompress has no fix.
node -e '
const fs = require("fs"), path = require("path");
function manifest(name, from) {
  let dir = path.resolve(from);
  for (;;) {
    const p = path.join(dir, "node_modules", name, "package.json");
    try { if (fs.statSync(p).isFile()) return p; } catch (e) {}
    const up = path.dirname(dir); if (up === dir) return null; dir = up;
  }
}
const cmp = (a, b) => { const A = a.split(".").map(Number), B = b.split(".").map(Number);
  for (let i = 0; i < 3; i++) if ((A[i]||0) !== (B[i]||0)) return (A[i]||0) - (B[i]||0); return 0; };
let m = manifest("@xhmikosr/decompress", process.cwd());
if (!m) console.log("@xhmikosr/decompress  not found under " + process.cwd());
else {
  const v = JSON.parse(fs.readFileSync(m, "utf8")).version;
  const floor = cmp(v, "11.0.0") >= 0 ? "11.1.4" : "10.2.2";
  console.log("@xhmikosr/decompress ", v,
    cmp(v, floor) < 0 ? ">>> AFFECTED: upgrade to " + floor + " or later" : "not in an affected range");
}
m = manifest("decompress", process.cwd());
console.log("decompress           ", m ? JSON.parse(fs.readFileSync(m, "utf8")).version
  + " >>> AFFECTED: no fix exists for this package, migrate to @xhmikosr/decompress"
  : "not found under " + process.cwd());'

# 2) Fallback that does not depend on npm resolving the tree, for pnpm store
#    layouts and vendored node_modules.
find . \( -path '*/node_modules/decompress/package.json' \
       -o -path '*/node_modules/@xhmikosr/decompress/package.json' \) \
       -not -path '*/.git/*' \
  -exec sh -c 'printf "%s\n  " "$1"; grep -oE "\"(name|version)\": *\"[^\"]*\"" "$1" | tr "\n" " "; echo' _ {} \;

# 3) Who pulled it in. For the unmaintained package this is the whole question,
#    because you probably did not choose it.
npm why decompress 2>/dev/null || npm ls decompress --all 2>/dev/null

# 4) Do you extract archives you did not create? These are the call sites to read.
grep -rEn "require\(['\"](@xhmikosr/)?decompress['\"]\)|from ['\"](@xhmikosr/)?decompress['\"]" \
  --include='*.js' --include='*.cjs' --include='*.mjs' --include='*.ts' \
  --exclude-dir=node_modules --exclude-dir=.git .

# 5) Do NOT try to settle this with a hand-built archive. A single symlink entry
#    pointing outside the output directory is rejected by BOTH the affected and the
#    patched versions, with the same message, because that case was already closed
#    by the earlier hardening in GHSA-mp2f-45pm-3cg9. This was tested against real
#    @xhmikosr/decompress 11.1.3 and 11.1.4 while preparing this bulletin: both
#    answered "Refusing to create a link pointing outside the output directory",
#    and nothing escaped on either. The published flaw needs a CHAIN of symlink
#    entries, which the advisory does not provide an archive for and this bulletin
#    did not reconstruct. So a naive test tells you nothing, and reading it as
#    "we are fine" is exactly the wrong conclusion. Use the version checks above.

Step 5 is a warning rather than a check, and it is there because the obvious way to reassure yourself here does not work. The version answer is the reliable one for this item: 11.1.4 or 10.2.2 on the fork, and no fixed version at all on the unmaintained decompress. If a transitive copy is the one your code actually calls, step 3 is how you find out which, and step 2 is how you find copies npm ls does not report.

Response

  1. Stop extracting untrusted archives on affected versions first. There is no workaround, so if a service takes uploaded archives and expands them, the cheap containment is to disable that path or queue the uploads until the upgrade lands. That is a smaller outage than the alternative.
  2. On a host that has extracted untrusted archives, capture the evidence before you redeploy. The impact the advisory names is writing outside the extraction directory, typically over startup scripts or configuration, and a container rebuild discards exactly that. At minimum record modification times for the plausible targets (init and unit files, cron directories, shell profiles, the app's own config) and compare them against your upload timeline. A match is a compromise of that host, not a corrupted file, and it goes down the incident path rather than the upgrade path.
  3. Upgrade @xhmikosr/decompress to 11.1.4, or to 10.2.2 if you are held on the v10 line. Both have been available since August 5, so for many projects this is already satisfied; confirm with step 1 rather than the lockfile.
  4. Replace the unmaintained decompress package. It has no fix and will not get one. Where it is transitive, the options in order of preference are: upgrade the dependency that pulls it in, if a newer version has moved to the fork; add an override or resolution pointing decompress at the fork, and test, because the fork's API is compatible but your dependency's expectations may not be; or drop the feature that needs it.
  5. Treat a hit from the evidence step as a host compromise. An arbitrary write outside the extraction directory, on a service that expands uploads, is remote code execution as soon as the overwritten file runs. Isolate the host, keep what you captured, rotate the credentials that process held from a clean machine, and rebuild rather than repairing the overwritten file.

10. piscina: a prototype-pollution gadget reaches execArgv and preloads attacker code into every worker (CVE-2026-102992)

GHSA-67c8-pqhq-4rmx was published on October 1, 2026, rated Critical with CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N and CWE-1321. The fixes, 5.3.2, 4.9.4 and 6.0.0-rc.5, all shipped on August 28, 2026.

ThreadPool.options is built as a plain object, so it inherits from Object.prototype, and any option that has no explicit default in kDefaultOptions can therefore be supplied through the prototype chain. The worst of the reachable options is execArgv, which is passed straight to new Worker(..., { execArgv }): setting Object.prototype.execArgv = ['--require', '/tmp/attacker.js'] makes every worker preload the attacker's module on startup. Also reachable are loadBalancer, an attacker-supplied function called during task scheduling in the main thread, and env, injected into each worker. The advisory is explicit that this survived the fix for CVE-2026-55388, which hardened the filename reads but left ThreadPool.options without a null prototype.

It sits at Lower because it is a gadget, not an entry point: you need a prototype-pollution primitive somewhere else in the application first, typically a vulnerable merge() or an unguarded deep-merge of parsed JSON. That is a real caveat and also a thin one, because prototype pollution is among the most common findings in a Node dependency tree, and this gadget upgrades it from "unexpected behaviour" to code execution. Treat the combination as the risk rather than piscina alone.

piscina CVE-2026-102992: affected and fixed (GitHub Advisory Database via OSV, npm registry, read 2 October 2026)
CVE: CVE-2026-102992 GHSA-67c8-pqhq-4rmx CWE-1321 Severity: Critical CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N NVD published: 2026-09-30 20:17 UTC Published to the GitHub Advisory Database: 2026-10-01 15:03 UTC Affected and fixed, by branch: >= 5.0.0 < 5.3.2 fixed 5.3.2 published 2026-08-28 08:43 UTC < 4.9.4 fixed 4.9.4 published 2026-08-28 08:38 UTC >= 6.0.0-rc.1 < 6.0.0-rc.5 fixed 6.0.0-rc.5 published 2026-08-28 08:44 UTC npm dist-tags as of 2026-10-02: latest 5.3.2, legacy 4.9.4, rc 6.0.0-rc.5. All three fixes predate the disclosure by 34 days. Precondition: the application must already have a prototype-pollution primitive, for example a vulnerable merge() or an unguarded deep-merge of parsed JSON. Without one, this gadget is not reachable. Reachable options via the prototype chain (any option without an explicit default in kDefaultOptions): execArgv -> passed to new Worker({execArgv}); --require preloads a module in every worker: remote code execution loadBalancer -> an attacker-supplied function called during scheduling, in the MAIN thread env -> environment injected into each worker argv, workerData, resourceLimits, niceIncrement, closeTimeout, recordTiming, stricterFIFO, workerHistogram, trackUnmanagedFds -> DoS and unexpected behaviour Incomplete prior fix: GHSA-x9g3-xrwr-cwfg / CVE-2026-55388 hardened the Piscina constructor's filename read and run()'s filename/name reads, but did not give ThreadPool.options a null prototype, so the class remained reachable. Fix commits (piscinajs/piscina): 0cb12fca37f526065b072592afe954574dcc656f 2f69f67159a0e48b38fd61fa4a91c2fdc19fff72 5be7bbb19e3787bb698862cd516121a578d690f7 bebbda255c2981cecddd36b171b94be2fd41c9a6
Ranges, the reachable-option list and the CVE-2026-55388 relationship are from the advisory as mirrored in OSV; publication timestamps and dist-tags are from the npm registry, read October 2, 2026. There are no indicators of compromise published. The advisory links a third-party reproduction repository for the execArgv, loadBalancer and env vectors; this bulletin did not run it, and the mechanism above is taken from the advisory text rather than from independent reproduction.

Am I affected?

# 1) Version, every copy. piscina is frequently transitive: build tooling and
#    framework internals use it, so a project that never named it can ship it.
npm ls piscina --all 2>/dev/null
node -e '
const fs = require("fs"), path = require("path");
// Manifest walk, not require("piscina/package.json"): that form throws on any
// package with an exports map, and a silent catch would read as "not installed".
let dir = path.resolve(process.cwd()), m = null;
for (;;) {
  const p = path.join(dir, "node_modules", "piscina", "package.json");
  try { if (fs.statSync(p).isFile()) { m = p; break; } } catch (e) {}
  const up = path.dirname(dir); if (up === dir) break; dir = up;
}
if (!m) console.log("no piscina manifest found under " + process.cwd());
else {
  const v = JSON.parse(fs.readFileSync(m, "utf8")).version;
  const n = v.split("-")[0].split(".").map(Number);
  const pre = v.includes("-");
  let floor;
  if (n[0] >= 6) floor = "6.0.0-rc.5";
  else if (n[0] === 5) floor = "5.3.2";
  else floor = "4.9.4";
  const cmp3 = (a, b) => { const A = a.split("-")[0].split(".").map(Number), B = b.split("-")[0].split(".").map(Number);
    for (let i = 0; i < 3; i++) if ((A[i]||0) !== (B[i]||0)) return (A[i]||0) - (B[i]||0); return 0; };
  let bad;
  if (floor === "6.0.0-rc.5") bad = pre && (parseInt((v.split("rc.")[1] || "99"), 10) < 5);
  else bad = cmp3(v, floor) < 0;
  console.log("piscina", v, bad ? ">>> AFFECTED: upgrade to " + floor + " or later"
                                : "not below the fix for its branch (" + floor + ")");
}
'

# 2) The other half of the precondition: a prototype-pollution primitive. These
#    are the usual shapes. Each hit is something to read, not a verdict.
grep -rEn "Object\.assign\(.*req\.(body|query|params)|_\.merge\(|_\.defaultsDeep\(|deepmerge\(|\.\.\.(JSON\.parse|req\.body)" \
  --include='*.js' --include='*.cjs' --include='*.mjs' --include='*.ts' \
  --exclude-dir=node_modules --exclude-dir=.git .

# 3) Let your auditor answer it: any advisory with CWE-1321 in the tree is the
#    other half of this chain.
npm audit --json 2>/dev/null | python3 -c "
import json,sys
try: d=json.load(sys.stdin)
except Exception: print('npm audit emitted nothing parseable here'); raise SystemExit
# ENOLOCK is VALID json with exit 0, so this check cannot be left to the except
# above: without it, a project with no package-lock.json (pnpm, yarn) prints a
# clean count it never earned.
if 'error' in d:
    print('npm audit did not run:', (d['error'] or {}).get('summary', d['error']))
    print('   no lockfile means no audit: generate one with'
          ' npm i --package-lock-only, or use your own package manager audit')
    raise SystemExit
vias=d.get('vulnerabilities',{})
found=0
for name,v in vias.items():
    for via in v.get('via',[]):
        if not isinstance(via,dict): continue
        t=(via.get('title') or '')
        if 'rototype' in t:
            print('prototype pollution:', name, '->', t); found+=1
print(f'-- {found} prototype-pollution advisory/advisories in the tree --')
"

# 4) Confirm the gadget is closed on YOUR installed copy. This one does
#    discriminate: it was run against real piscina 5.3.1 and 5.3.2 while preparing
#    this bulletin, and 5.3.1 printed the canary line while 5.3.2 did not. Run it in
#    a scratch directory, because it writes canary.js and worker.js into the cwd.
node -e '
const path = require("path"), fs = require("fs");
fs.writeFileSync("canary.js", "console.log(\"CANARY: inherited execArgv was used\");");
fs.writeFileSync("worker.js", "module.exports = () => 1;");
Object.prototype.execArgv = ["--require", path.join(process.cwd(), "canary.js")];
let mod;
try { mod = require(require.resolve("piscina", { paths: [process.cwd()] })); }
catch (e) { console.log("piscina not loadable from " + process.cwd() + ": " + (e.code || e.message)); process.exit(0); }
const P = mod.Piscina || mod.default || mod;
if (typeof P !== "function") { console.log("could not find the Piscina constructor on this export shape"); process.exit(0); }
const pool = new P({ filename: path.join(process.cwd(), "worker.js"), minThreads: 1, maxThreads: 1 });
pool.run(1)
  .then(() => { console.log("pool ran: if no CANARY line appeared above, the inherited execArgv was NOT used"); return pool.destroy(); })
  .catch(e => { console.log("error:", e.message); return pool.destroy(); });
'

Step 4 is the honest version of this check. A canary line means the inherited execArgv reached the worker, which is the vulnerability. No canary line means it did not on this code path, which is good evidence and not a proof that every reachable option is closed: the advisory lists a dozen of them and this exercises one. Run it in a scratch directory, since it writes canary.js and worker.js into the working directory.

Response

  1. Upgrade to the fix for your branch: 5.3.2, 4.9.4, or 6.0.0-rc.5. All three have been on npm since August 28, so a re-resolve is likely all this takes. Confirm with step 1, because a transitive copy will not move just because your direct dependency did.
  2. Close the prototype-pollution primitive, which is the part that actually matters. piscina was one gadget; fixing it leaves the pollution reachable by whatever else inherits options from Object.prototype, and in a real dependency tree there is usually something else. Fix the merge, and prefer Object.create(null) or a Map for anything built from parsed input.
  3. If you had both halves, treat it as code execution and rotate. execArgv preloading means arbitrary code in every worker, and loadBalancer means arbitrary code in the main thread, so the credentials and tokens in that process are in scope. The advisory notes this class survived the earlier CVE-2026-55388 fix, so "we patched that one" is not the same as never having been exposed.

What this window actually shows

Five products with confirmed exploitation in seven days. Four of them are infrastructure, and in three of those four the attacker needed no credentials: NetScaler by default configuration, Cisco by one percent-encoded character, FortiMail by a crafted HTTP request. The fourth, Zammad, was reached unauthenticated by hijacking a session. The fifth is Apple's CoreGraphics bug, which is a workstation issue and, on Apple's own account, a targeted one. Three of the four infrastructure items are the boxes that sit at the edge of a self-hosted network and hold the credentials for everything behind them, which is why the response sections above spend more words on rotation and rebuild than on the upgrade itself. The upgrade is the easy part. The hard part is that a NetScaler holds your LDAP service account, a FortiMail holds your mail, and a Zammad holds your customers' correspondence and, thanks to CVE-2026-102490, root on its own host.

The second pattern is the one the dependency half of this bulletin is really about, and it is the opposite shape to the September 25 edition, which found advisories lagging their fixes by two to three months. Here the lag is still there but shorter and more uniform: vm2 fixed August 24 and disclosed October 1, piscina fixed August 28 and disclosed October 1, decompress fixed August 5 and disclosed September 28, PyJWT fixed September 11 and disclosed September 28, Next.js fixed September 22 and disclosed September 30. Five items, five fixes that already existed when the advisory appeared, with a median gap of around five weeks. If you keep your dependencies current you were already patched for all five before you heard about any of them. If you patch only when a scanner opens a pull request, your exposure window for each was the gap plus your own patch time, and the gap was not yours to control.

One more number from this window, for scale rather than action. OSV recorded 265 malicious package advisories published between September 26 and October 2, 2026: 243 in npm, 20 in PyPI, 2 in Go and none in crates.io. That is the background rate of typosquats and credential stealers in the public registries, roughly 38 a day, and it does not represent a single campaign. It is the reason an install from a registry is a trust decision and not a download.

Caveats and what this bulletin does not establish

On the window itself. The procedure this edition follows computes the window from the date of the top entry on the index, which is September 28, 2026. That would give a four-day window and a direction to stop. The September 28 entry is the KEV supplement, and it covers September 13 to 25, as does the September 25 catch-up before it, so content coverage ends September 25 and the uncovered period is September 26 to October 2, which is seven days. This edition uses the uncovered period. The KEV additions in it, none of them covered anywhere in this project before today, are the evidence that this was the right reading, but the deviation is stated here so it can be judged rather than discovered. There were five when this bulletin was written on October 2; the October 4 update brings it to seven, because CISA added both Zammad CVEs on October 2 itself, after the catalog version this edition first fetched.

Two sources could not be reached, and the first one costs content. The Citrix community blog that carries the NetScaler indicators of compromise returns a Cloudflare interstitial to every request from this build environment, with and without a browser user agent. That is origin bot protection rather than a network policy denial: the proxy's own failure log records no rejected connection for it, and support.citrix.com answered normally, which is where CTX697096 itself was read from. Item 1 therefore contains no indicators of compromise, and that absence is a gap in this bulletin, not a finding about NetScaler. CISA's KEV note states that running Citrix's IOCs in the NetScaler console may help identify exploitation, so fetch that blog yourself before concluding an appliance is clean. Separately, github.com is restricted to this session's own repository, so GitHub advisory pages could not be opened directly; the advisory text, ranges, severities and CWEs for items 5, 6, 7, 9 and 10 were read from the OSV mirror of the GitHub Advisory Database, which carries the advisory bodies verbatim, and the version and date facts were independently confirmed against the npm registry and the PyPI JSON API.

On the satori companion, and a correction made during review. The Next.js advisory references GHSA-wx4j-mvgx-mqwp for the upstream satori package. That GHSA page is on github.com, which is restricted here, and the identifier is not in OSV on its own: a direct lookup returns not found and an OSV query for pkg:npm/satori returns zero vulnerabilities, both checked October 2, 2026. An earlier draft of this bulletin concluded from those two facts that the advisory could not be opened anywhere and gave no satori floor. That conclusion was wrong, and the review caught it: the OSV record for GHSA-vcvr-r3jv-pc5j carries a related identifier, CVE-2026-94545, which both OSV and NVD serve, which is assigned by GitHub's own CNA, and which carries the satori range and the sentence "Version 0.33.5 contains a patch". The one-hop link in a record this bulletin had already read was not followed. Item 5 now states the satori floor of 0.33.5 and attributes it to CVE-2026-94545 rather than to the unopened GHSA. The GHSA itself remains unread, so anything in it beyond what the CVE record carries is not reflected here.

On dates, and on not implying anything is new. Items 5, 6, 7, 9 and 10 each disclose a vulnerability whose fix shipped before this window opened, in one case 54 days before. Each section gives both the advisory date and the release date of the fix, and none of them should be read as a vulnerability introduced in this window. Where the GitHub Advisory Database review date and the NVD publication date differ, both are stated: the review date is what gates GitHub-native scanners, and it is not the publication date of the underlying advisory. The Next.js GHSA has no CVE alias of its own, but its related CVE-2026-94545 is in both OSV and NVD, published 2026-09-30.

On exploitation claims, which are quoted rather than inferred. Item 1 rests on Citrix's sentence that exploits of CVE-2026-88771 and CVE-2026-88772 have been observed, and makes no exploitation claim for the other six CVEs in the same bulletin. Item 2 rests on Fortinet's "reported to be exploited in the wild" and its Known Exploited: Yes metadata. Item 3 rests on DIVD's own account of being breached on September 21, on the E:A in its CVSS v4 vectors, and on CISA adding both CVEs to KEV on October 2; Zammad does not dispute that the chain was used against DIVD, and on the root escalation says only that it cannot be exploited remotely on its own. Item 4 rests on Cisco PSIRT's statement of active exploitation in September 2026. Item 8 rests on Apple's narrower wording, "may have been exploited in an extremely sophisticated attack against specific targeted individuals", which is deliberately not upgraded here into commodity exploitation. Items 5, 6, 7, 9 and 10 carry no exploitation claim because none of their advisories makes one, and that is an absence of statements rather than a statement of absence.

On internal inconsistencies in primary sources, reproduced rather than resolved. Citrix writes the FIPS and NDcPP fix as 13.1-37.279 in its affected-versions list and 13.1.37.279 in its remediation list and NVD's CPE records; these are the same build and both renderings appear above. DIVD's CVE-2026-102490 page gives a structured affected range of 1.5.0 up to but excluding 7.1.0-alpha while its description says all versions including the latest alpha, and its case file says patch status Available and workaround N/A while also saying Zammad is working on a fix. Larger than any of those, DIVD and Zammad disagree with each other: DIVD puts the RCE in 6.3.0 to 6.5.4 and says it is present but not exploitable through 7.1.3, while Zammad says exploitation is only possible on 6.5 and older because of the runtime those versions use and that 7.0 and later are not affected; and on the root escalation DIVD says every version from 1.5.0 while Zammad, having now received the details, says it cannot be exploited remotely on its own and publishes no scope of its own. Item 3 reproduces both and recommends the action safe under either: 7.2.0, which clears both readings of the RCE and carries Zammad's hardening, and continuing to treat code execution as the zammad user as equivalent to root. Citrix's CVSS v4 9.5 for CVE-2026-88772 and NVD's CVSS 3.1 8.1 for the same issue are both given, as are DIVD's standalone (8.7, 8.5) and chained (9.4) scores for the Zammad pair.

On the command blocks. Every block was syntax checked with bash -n and executed against fixtures representing both an affected and a patched system, including, where the package exists on a public registry, against real installs of an affected and a patched version rather than hand-built stand-ins. The exception is item 8, whose version comparison was tested by feeding it known version strings behind a stub sw_vers but which was not run on a Mac, because this bulletin was not built on one. Nine defects were found and fixed during that verification, and they are named here because every one of them would have produced a false all-clear. The Cisco detection first grepped the advisory's literal %6a example and missed /j_securit%79_check; then, after that was fixed in the live passes, the same literal survived in the pass over rotated .gz archives, which is where vManage keeps the evidence, and missed it there too. Both passes are now method-agnostic and position-agnostic, tested against the encoding in three positions and under POST, OPTIONS and DELETE. The FortiMail hunt used grep -r, which silently skips every compressed rotation: against a log store holding only a .gz, it printed five clean-looking headers on an implanted appliance. It now uses zgrep, reports how many files it read, and covers the fifth indicator Fortinet publishes. Every Node version check read the installed version with require("pkg/package.json"), which throws on a package declaring an exports map: confirmed against real @xhmikosr/decompress and piscina installs, both of which would have reported an affected project as not installed. All of them now walk node_modules and read the manifest from disk. When the satori floor was added to this item's prose it was not added to the check, so a project on satori 0.33.4 read as entirely clean; it is now in the rules and tested against real 0.33.4 and 0.33.5. The PyJWT allow-list scanner matched only a bracketed literal at the call site and reported zero on the two commonest real shapes, a tuple and a named constant; it now covers both, and says so for a list built at runtime, which no scanner can settle. Item 10's npm audit parser treated a missing lockfile as a clean tree, because npm audit emits valid JSON with an ENOLOCK error and exit 0, which is the normal case for pnpm and yarn projects. A behavioural test originally written for item 9 was removed rather than shipped: a hand-built archive with one symlink pointing outside the output directory is rejected identically by 11.1.3 and 11.1.4, because that case was closed by the earlier hardening, so it could not distinguish and would have read as reassurance. Item 10's canary was kept because it does distinguish, confirmed firing on real piscina 5.3.1 and silent on 5.3.2. Two blocks are deliberately not verdicts and say so in place: the ImageResponse call-site listing in item 5 and the prototype-pollution shapes in item 10 produce a set of lines to read, because no pattern match can decide whether a value is attacker-controlled. Port numbers and log paths are vendor defaults and vary by deployment. The appliance CLI commands in items 1, 2 and 4 are commented out inside the blocks because they are device commands, not shell ones. The reachability probes in items 1, 2 and 3 are to be run against your own systems only, and their decision rule is the connection outcome, not the HTTP status: a 401 or 403 means the interface answered and is reachable. The canary in item 10 writes files into the working directory and should be run in a scratch directory.

What this bulletin did not do. It reproduced none of the published exploits. Item 10's canary exercises your installed copy rather than demonstrating the published exploit, and that advisory's linked third-party reproduction repository was not run. Item 9's symlink chain was not reconstructed at all, which is why that item offers no behavioural test. No FortiMail, NetScaler, vManage or Zammad appliance was available to test against, so every appliance-side command is validated as text against the vendor's own documented configuration syntax and log formats rather than against a live device. The 265 malicious-package figure is a count of OSV MAL advisories published in the window and is not a claim about how many distinct campaigns or maintainer compromises that represents.

On review. The adversarial review required by this project ran as two independent reviewers in parallel, one on each of two model families, because the Codex path is unavailable in a cloud run. Two models from one vendor are two independent reads, not vendor independence, and that is weaker than the usual arrangement. Both returned substantive findings and each caught defects the other missed. The document reviewer found the satori error recorded above, a wrong KEV count, Apple's hedge upgraded into a confirmation, and four items whose response steps put remediation before containment. The verification reviewer, which ran every command block against real packages and fixtures, found the five detection defects named in the command-blocks caveat, plus two DIVD table columns this bulletin had transposed and a missing introduced version on one vm2 advisory. Every finding was checked against the primary source before being applied, and none was rejected. The full record, including the findings and what was done with each, is posted on the pull request for this bulletin.

One-line takeaway

Between September 26 and October 2, 2026, five products were confirmed under exploitation: Citrix NetScaler CVE-2026-88771, which needs no feature enabled and so hits every unpatched ADC and Gateway, FortiMail CVE-2026-104286, which still has no released fix, Zammad CVE-2026-102489 chained to CVE-2026-102490, which breached DIVD, reached KEV on October 2, and whose root escalation is still unfixed, Cisco Catalyst SD-WAN Manager CVE-2026-76504, and Apple CoreGraphics CVE-2026-86950 on development workstations, which Apple describes narrowly as a targeted attack. Preserve evidence on any exposed NetScaler and then install 14.1-73.37 or 13.1-64.23; hunt FortiMail for the indicators, then disable its IBE feature and get its webmail interface off the internet, because there is still nothing to upgrade to; run DIVD's log check and move Zammad to 7.2.0 while assuming the zammad user is root; upgrade vManage to the fixed release for your train; and in one dependency pass raise next to 16.3.6 (and satori to 0.33.5 if you use it directly), pyjwt to 2.14.0, vm2 to 3.11.7, piscina and @xhmikosr/decompress to their branch fixes, and replace the unmaintained decompress. The NetScaler indicators of compromise are not in this bulletin because the Citrix blog that holds them was unreachable; fetch them yourself before calling an appliance clean.

All Bulletins ↑ Lead item primary source: Citrix CTX697096 →