Own Property vs Inherited
Let’s take an example:
const a = { nama: "Budi" };
a.umur = 25;
Does a have a property called nama? The answer is yes, they have. And this is called Own Property.
Does a have an own property called toString? The answer is no. So, toString is not own property of a.
Prototype Chain
Now let’s move to Inherit, for example:
const a = { nama: "Budi" };
const b = Object.create(a);
b.umur = 25;
b has an own property with the key called umur.
And what about b.nama does b automatically makes the nama as its own property? The answer is No, because those property are inherit from a.
The interesting part is, if a doesn’t have the property of “something”, it will go up to Object.prototype, then until null. The chain would be:
flowchart LR
B["b"] --> A["a"]
A --> C["Object.prototype"]
C --> D["null"]
Before continue, let’s deep dive into GETTER and SETTER — you’ll need this to understand how __proto__ access works next.
Getter & Setter
Getter is a function that runs automatically every time the property is accessed.
For example:
const person = {
namaDepan: 'Budi',
namaBelakang: 'Santoso'
get namaLengkap(){ //This is GETTER
return this.namaDepan + ' ' + this.namaBelakang;
}
// This is normal function
namaLengkaps(){
return this.namaDepan + ' ' + this.namaBelakang;
}
};
// Output: 'Budi Santoso'
console.log(person.namaLengkap)
console.log(person.namaLengkaps) // Output: ƒ () { ... } (function reference)
console.log(person.namaLengkaps()) // Output: 'Budi Santoso'
As we can see, after we called the property, it immediately execute the function. And there’s another concept called SETTER, SETTER is a function that accepts exactly one argument (the assigned value), but it’s still accessed as a property assignment (like GETTER); not called with ().
For example:
const person = {
namaDepan: 'Budi',
namaBelakang: 'Santoso'
set namaLengkap(value){ //This is SETTER
const parts = value.split(' ');
this.namaDepan = parts[0];
this.namaBelakang = parts[1];
}
};
person.namaLengkap = 'Adolf Hitler';
console.log(person.namaDepan); //Output: 'Adolf'
console.log(person.namaBelakang); //Output: 'Hitler'
Example Vulnerable Code 1
Here is an example of a vulnerable code (prototype pollution) regarding this:
function deepMerge(target, source) {
for (let key in source) {
if (
source[key] &&
typeof source[key] === "object" &&
!Array.isArray(source[key])
) {
if (!target[key]) target[key] = {};
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Attacker Flow
Here’s the twist — __proto__ is not an actual key in the object, it’s a getter/setter on Object.prototype that accesses the object’s prototype. So when the attacker sends this:
let source = {
favoriteCorp: "wheat",
experienceLevel: "intermediate",
farmSize: "501",
__proto__: { isAdmin: true },
};
let target = {
favoriteCorp: "wheat",
experienceLevel: "intermediate",
farmSize: "60",
};
The first 3 iterations work as normal (copying string values). On the 4th iteration (key = "__proto__"):
Step 1 — The outer condition passes:
if({isAdmin: true} && typeof {isAdmin: true} === 'object' && !Array.isArray({isAdmin: true}))
// → true && true && true → enters the branch
Step 2 — The guard check:
if (!target[key]) target[key] = {};
// → if (!target["__proto__"]) target["__proto__"] = {};
target has no own property called "__proto__", so target["__proto__"] walks the prototype chain and resolves to Object.prototype — which is truthy.
So !target["__proto__"] is false → the if is skipped. Good, right?
Step 3 — But the recursion still runs:
deepMerge(target["__proto__"], source["__proto__"]);
// ↕ resolves via prototype chain
// deepMerge(Object.prototype, { isAdmin: true })
This sets Object.prototype.isAdmin = true, polluting every object. The if guard only blocked reassigning target["__proto__"], but the recursive call modifies the prototype anyway.
So why is __proto__ a getter/setter?
Because __proto__ is a getter/setter on Object.prototype, not a real key. Accessing target["__proto__"] runs the getter which returns the internal prototype (Object.prototype). That’s why it feels like “reading a property” but actually triggers hidden behavior — and why the guard fails.
Normal Flow (for contrast)
With a normal source object (no __proto__), every key is a plain string property. Each iteration hits the else branch and copies the value directly — no prototype chain shenanigans.