DEV Community

Cover image for TryHackMe: Prototype Pollution
Sean Lee
Sean Lee

Posted on

TryHackMe: Prototype Pollution

In JS, Prototype functions similarly to Classes

Difference Between Class and Prototype in JavaScript

Classes

  • Act like blueprints for creating objects.
  • Ensure that all objects created from the class share the same properties and methods.
  • Provide a structured and easy-to-understand way of object creation.

Prototypes

  • Allow objects to inherit behaviors by linking them to a prototype object.
  • More dynamic and flexible than classes, as methods can be added or modified at runtime.
  • Objects are connected via the prototype chain, enabling inheritance without explicitly defining classes.

Analogy

  • Class: Like a car blueprint; all cars built from it have the same structure.
  • Prototype: Like modifying a basic car model by adding custom features directly.

Example of Prototype Pollution

Let's assume, we have a basic prototype for Person with an introduce method. The attacker aims to manipulate the behaviour of the introduce method across all instances by altering the prototype.

// Base Prototype for Persons
let personPrototype = {
  introduce: function() {
    return `Hi, I'm ${this.name}.`;
  }
};

// Person Constructor Function
function Person(name) {
  let person = Object.create(personPrototype);
  person.name = name;
  return person;
}

// Creating an instance
let ben = Person('Ben');
Enter fullscreen mode Exit fullscreen mode

When we create a new object, ben, and call the introduce method, it displays Hi, I'm Ben, as shown in the following figure.

image of console

In JavaScript, the __proto__ property is a common way to access the prototype of an object, essentially pointing to the object from which it inherits properties and methods.

// Attacker's Payload
ben.__proto__.introduce=function(){console.log("You've been hacked, I'm Bob");}
console.log(ben.introduce()); 
Enter fullscreen mode Exit fullscreen mode

Breakdown of whole process:

  • Prototype Definition: The Person prototype (personPrototype) is initially defined with a harmless introduce method, introducing the person.
  • Object Instantiation: An instance of Person is created with the name 'Ben' (let ben = Person('Ben');).
  • Prototype Pollution Attack: The attacker injects a malicious payload into the prototype's introduce method, changing its behaviour to display a harmful message.
  • Impact on Existing Instances: As a result, even the existing instance (ben) is affected, and calling ben.introduce() now outputs the attacker's injected message.

This example shows how an attacker can alter the behaviour of shared methods across objects, potentially causing security risks. Preventing prototype pollution involves carefully validating input data and avoiding directly modifying prototypes with untrusted content.


Exploitation - XSS

Overview

The submit review feature allows users to submit a review for a friend, which is then saved in the database. Below is a breakdown of the client-side and server-side code along with potential security risks.

Client-Side Code

The review form:

<form action="/submit-friend-review" method="post">
  <h2>Submit a Review</h2>
  <input type="hidden" name="friendId" value="1">
  <div class="form-group">
    <textarea class="form-control" name="reviewContent" placeholder="Write your review here" rows="3"></textarea>
  </div>
  <button type="submit" class="btn btn-primary">Submit Review</button>
</form>
Enter fullscreen mode Exit fullscreen mode
  • The form submits a POST request to /submit-friend-review.
  • It includes a hidden friendId field, which can be manipulated by an attacker.
  • The review content is sent as input.

Server-Side Code

app.post("/submit-friend-review", (req, res) => {
  if (!req.session.user) {
    return res.redirect("/signin");
  }

  const { friendId, reviewContent } = req.body;
  const friend = friends.find((f) => f.id === parseInt(friendId));

  if (!friend) {
    return res.status(404).send("Friend not found");
  }

  try {
    const input = JSON.parse(reviewContent);
    _.set(friend, input.path, input.value);
  } catch (e) {}

  res.redirect(`/friend/${friendId}`);
});
Enter fullscreen mode Exit fullscreen mode

Key Actions:

  1. Session Validation – Ensures the user is logged in.
  2. Friend Validation – Checks if the friendId exists.
  3. Review Insertion – Uses _.set(friend, input.path, input.value) from Lodash to insert the review.
  4. Potential Vulnerability – Since Lodash’s _.set function dynamically sets properties, malicious input can modify unintended properties.

Potential Exploit: Prototype Pollution

The attacker can deliver a paylaod such as {"path": "reviews[0].content", "value": "<script>alert('Hacked')</script>"} that alerts whenever someone visits the page.

Other parameter such as isAdmin and isloggedIn are potential attack vectors too.

Security Risks

  • Prototype Pollution – Allows modifying global object properties.
  • Privilege Escalation – Attackers could gain unauthorized access.
  • Data Tampering – Malicious users could inject unexpected properties.

Next Steps

  • Test the vulnerability by adding a simple review and checking if it's successfully inserted.
  • Explore further exploits like XSS by injecting script tags in the review content.
  • Mitigation: Implement strict input validation and avoid direct JSON parsing with Lodash’s _.set().

Exploitation - Property Injection

1. Object Recursive Merge (Merging Objects Safely)

Imagine you have a settings page where users can update their preferences (like changing themes or notification settings). The application might use a function to merge the new settings with the existing ones.

Here's a vulnerable merge function:

// Vulnerable recursive merge function
function recursiveMerge(target, source) {
    for (let key in source) {
        if (source[key] instanceof Object) {
            if (!target[key]) target[key] = {};
            recursiveMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • This function takes two objects:
    • target (existing settings)
    • source (new settings from user input)
  • It recursively copies each property from source into target.

If used correctly, this function updates user settings normally.

If misused, an attacker can inject a property into the prototype, affecting all objects.

How an Attacker Exploits This

An attacker sends this input:

jsonCopyEdit{ "__proto__": { "isAdmin": true }}

  • Since the function does not validate inputs, it merges this into the global settings.
  • Now, all objects in the app inherit isAdmin: true, potentially giving unauthorized admin access.

2. Object Cloning (Copying Objects Safely)

Another risky function is object cloning, where one object is copied to create another.

Normal Use: When creating a new user profile, the system might copy default settings.

Attack Risk: If cloning does not filter out special properties like __proto__, constructor, etc., an attacker can inject malicious properties.

For example, if the app clones an object like this:

jsonCopyEdit{ "__proto__": { "hasFreeSubscription": true }}

Now, every cloned user object inherits hasFreeSubscription: true, allowing free access to premium features.

Example

app.post("/clone-album/:friendId", (req, res) => {
  const { friendId } = req.params;
  const { selectedAlbum, newAlbumName } = req.body;
  const friend = friends.find((f) => f.id === parseInt(friendId));
  if (!friend) {
    console.log("Friend not found");
    return res.status(404).send("Friend not found");
  }
  const albumToClone = friend.albums.find(
    (album) => album.name === selectedAlbum
  );
  if (albumToClone && newAlbumName) {
    let clonedAlbum = { ...albumToClone };
    try {
      const payload = JSON.parse(newAlbumName);
      merge(clonedAlbum, payload);
    } catch (e) {
    }

function merge(to, from) {
  for (let key in from) {
    if (typeof to[key] == "object" && typeof from[key] == "object") {
      merge(to[key], from[key]);
    } else {
      to[key] = from[key];
    }
  }
  return to;
}
Enter fullscreen mode Exit fullscreen mode

In the above code, the servers receive a JSON object containing the album's name, copy the album that needs to be copied into another object, and change the name of the newly created copy by calling the merge function.

We know the merge function is an ideal candidate for prototype pollution if it blindly copies all the objects and properties without sanitising based on keys. We can see that the merge function made by the developer lacked any such sanitisation filters. What if we send a request that contains __proto__ with a newProperty and value as mentioned below:

{"__proto__": {"newProperty": "hacked"}}

The merge function will consider the __proto__ as a property and will call obj.__proto__.newProperty=value. By doing this, newProperty is not added directly to the friend object. Instead, it's added to the friend object's prototype.

image after clonning an album


Exploitation - Denial of Service

Causes of DoS via Prototype Pollution

  1. Altering a Critical Function – If an attacker changes Object.prototype.toString, any part of the app that relies on it may start behaving incorrectly.
  2. Unexpected Crashes – Many parts of a complex app call toString automatically. If the function is modified, the app might crash or enter an infinite loop, using up system resources.
  3. Breaking Business Logic – Changing built-in functions can cause unexpected errors, stopping the app from working and making the server unresponsive to real users.

Bottom line: Prototype pollution can disrupt services by modifying essential functions, leading to crashes or slowdowns that prevent users from accessing the app. 🚨

Example

Server-side code:

<form action="/clone-album/1" method="post" class="mb-4">
        <h2 class="mb-3">Clone Album of Josh</h2>
        <div class="form-group">
            <label for="selectedAlbum">Select an Album to Clone:</label>
            <select class="form-control" name="selectedAlbum" id="selectedAlbum">
                    <option value="Trip to US">
                        Trip to US
                    </option>
            </select>
        </div>
        <div class="form-group">
            <label for="newAlbumName">New Album Name:</label>
            <input type="text" class="form-control" name="newAlbumName" id="newAlbumName"
                placeholder="Enter new album name">
        </div>
        <button type="submit" class="btn btn-primary">Clone Album</button>
    </form>
Enter fullscreen mode Exit fullscreen mode

Example payload:

{"__proto__": {"toString": "Just crash the server"}}

clone album image

  • Let's decode the payload once the app.js receives the request, parses the JSON, and assigns the toStringfunction value in the __proto__ property of the friend object.
  • This creates an abrupt behaviour as toString is widely used among different objects. When we click on Clone Album, the application crashes, as shown below:

error screenshot after overriding the string

  • The TypeError we get is Object.prototype.toString.call is not a function, as we have already overridden that function using Prototype pollution.
  • You can override several other built-in objects/functions like toJSON, valueOf, constructor, etc., but the application won't crash in all behaviours. It entirely depends on the function that you are overriding.

Automating Vulnerability Detection Process

Important Scripts for Detecting Prototype Pollution

Several tools and open-source projects help automate the detection of prototype pollution vulnerabilities in JavaScript applications. Below are some key tools available on GitHub:

  • NodeJsScan – A static security scanner for Node.js applications that detects various vulnerabilities, including prototype pollution.
  • Prototype Pollution Scanner – Scans JavaScript codebases for patterns vulnerable to prototype pollution, helping developers find and fix issues.
  • PPFuzz – A fuzzer that automates testing for prototype pollution in web applications by injecting inputs that interact with object properties.
  • BlackFan’s Client-Side Detection – Focuses on client-side prototype pollution, demonstrating how it can be exploited for XSS attacks and other browser-based threats.

How Pentesters Can Use These Tools:

Pentesters should analyze how user input influences object properties in JavaScript applications. The key is to check whether inputs are properly sanitized and validated to prevent unauthorized modifications to the prototype chain. 🚨


Mitigation Measures

Prototype pollution allows attackers to manipulate an object's prototype, leading to unexpected behavior and security vulnerabilities. Below are key mitigation measures for pentesters and secure code developers.

For Pentesters

  • Fuzzing & Input Manipulation – Test user inputs extensively with different payloads to identify prototype pollution vulnerabilities.
  • Context Analysis & Payload Injection – Analyze how user inputs interact with prototype-based structures and inject payloads to test for vulnerabilities.
  • CSP Bypass & Payload Injection – Assess Content Security Policy (CSP) restrictions and attempt to bypass them for prototype manipulation.
  • Dependency Analysis & Exploitation – Check third-party libraries for outdated or vulnerable dependencies that could introduce prototype pollution risks.
  • Static Code Analysis – Use static analysis tools to detect insecure coding patterns during development.

For Secure Code Developers

  • Avoid __proto__ – Use Object.getPrototypeOf() instead to prevent direct prototype manipulation.
  • Use Immutable Objects – Prevent unintended prototype modifications by designing immutable objects.
  • Encapsulation – Restrict access to object prototypes by exposing only necessary functionalities.
  • Use Safe Defaults – Initialize objects securely without relying on user input for prototype properties.
  • Input Sanitization – Validate and sanitize user inputs to prevent prototype pollution attacks.
  • Manage Dependencies – Regularly update libraries and frameworks to prevent vulnerabilities.
  • Implement Security Headers – Use CSP and other security headers to mitigate the risk of loading malicious scripts.

By combining rigorous testing, secure coding, and ongoing security awareness, both pentesters and developers can effectively mitigate prototype pollution risks. 🚨

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)