{"id":"VND-nodejs-march-2026-hashdos","kind":"vulnerability","aliases":[],"related":["CVE-2025-27209","CVE-2026-21717"],"title":"Node.js: Developing a minimally HashDoS resistant, yet quickly reversible integer hash for V8","description":"Commercial support for versions past the Maintenance LTS phase is available through our OpenJS Ecosystem Sustainability Program partners Developing a minimally HashDoS resistant, yet quickly reversible integer hash for V8 JC Joyee Cheung Developing a minimally HashDoS resistant, yet quickly reversible integer hash for V8 What happens when a hashing scheme needs to be both HashDoS resistant and quickly reversible? That&#x27;s the puzzle we tried to solve for addressing CVE-2026-21717 in the March 2026 Node.js security release . This led to the development of an integer hash that we believe is unpredictable enough to prevent a blind attacker from reliably triggering severe performance degradation in our threat model. At the same time, it is also a permutation that can be efficiently inverted to recover the original integer value by the runtime holding the secret random keys, which is important for maintaining V8&#x27;s performance optimizations.\nIn this post, we will go into the details of this vulnerability, explain why and how the design tried to meet the two seemingly contradictory requirements, and sample the statistical analysis we performed to evaluate the quality of the hash. Along the way, we will also discuss some specifics of how V8 stores and uses string hashes internally that lead to these constraints, the implementation techniques, and the performance analysis.\nWhat is HashDoS and why does it matter for Node.js?\nHash tables are one of the most important data structures in software, and Node.js/V8 are no exceptions. With hash tables, there comes the problem of collisions - while hash tables are designed to have O(1) average time complexity for, e.g., single lookup and insertion, when there are so many keys whose hashed values can be mapped to the same slot that the table has to walk through many locations to insert a new key, it&#x27;s theoretically possible to degrade to O(n 2 ) in total for inserting n keys.\nIn many applications, the worst-case performance of hash tables is usually just a theoretical concern and maybe an occasional nuisance, but for servers that need to process untrusted input, this can be an attack vector. If an attacker manages to find a path that allows them to cause a large number of collisions in the server&#x27;s internal hash table with a small number of inputs, they can effectively &quot;freeze&quot; the thread without much effort. And, with enough threads frozen, they could render the service unavailable for a sustained period of time. This is commonly called a hash flooding or Hash-DoS (Denial of Service) attack .\nHashDoS vulnerabilities can show up in a wide range of platforms. In the case of V8, when it&#x27;s used in a browser, DoS issues are not considered vulnerabilities because they typically only affect the tab that processes the malicious input, and users can simply close the tab. But when it&#x27;s used in event-loop-based server-side runtimes, a single malicious request that manages to block the entire event loop for a sustained period of time has a much bigger impact. As one of the most widely deployed server-side runtimes, Node.js draws extensive scrutiny from security researchers, which helps us find and address weak spots like these.\nUnlike other DoS attack vectors that can be more easily mitigated by userland restrictions, HashDoS vulnerabilities are particularly tricky because they lurk inside widely used internal data structures and well-defined operations trusted by developers. For V8 embedders, this means fixes for HashDoS vulnerabilities usually fall on the runtime maintainers, and they are a critical part of maintaining the security and reliability of the ecosystem.\nMitigating HashDoS with seeded hashes\nThe core of HashDoS vulnerabilities lies in deterministic hash functions - if the attacker can predict the hash values of their inputs, they can craft inputs that reliably trigger worst-case performance in the target server. The standard mitigation is to mix a seed value into the computation of the hash, and make sure that the seed is randomly generated at program initialization . This way, even if the attacker knows the hash function, they can&#x27;t predict the hash values without knowing the seed that changes whenever the server restarts, and therefore can&#x27;t craft inputs that will reliably collide.\nIn Node.js/V8, HashDoS vulnerabilities tend to center around misconfigurations that lead to unseeded hashes or hashes seeded by a constant value. For an early history of HashDoS vulnerabilities in Node.js/V8 and their mitigations, check out this V8 blog post . A more recent example is CVE-2025-27209 , reported by Mate Marjanović and addressed in Node.js&#x27;s July 2025 security release: during V8&#x27;s switch to rapidhash , a configuration gap left the constants used by rapidhash hard-coded in V8 without seeding. To mitigate this, Node.js temporarily reverted back to a seeded hash in its copy of V8. Later, Gus Caplan from Deno ported rapidhash&#x27;s secret generation code to V8 and wired it up to seed the constants used by rapidhash at runtime , which became the eventual mitigation that shipped in newer versions of Node.js. The generation of rapidhash secrets turned out to be important to our later design for the seeded integer hash, which we will discuss below.\nUntil recently, most HashDoS discoveries in V8 had centered on hashing of regular strings, since they are more commonly found in paths exposed to server handlers. It wasn&#x27;t until the recent HackerOne report by Mate Marjanović that our attention was drawn to an elephant in the room: array index strings (those that look like non-negative integers and fit within 24 bits) in V8 used a different deterministic hash that can be trivially predicted.\nWhat string hashes look like in V8\nTo understand the vulnerability, let&#x27;s look at how V8 stores string hashes internally.\nEvery v8::internal::Name object (the superclass of v8::internal::String in V8) carries a 32-bit raw_hash_field . The composition of this field varies depending on the type of string. The bottom 2 bits ( HashFieldTypeBits ) determine the interpretation, and two of the most common types are:\nRegular strings (type kHash = 0b10 ): bits 2-31 store a 30-bit hash value computed by rapidhash, seeded with randomly generated secrets. This is considered to be &quot;minimally HashDoS resistant&quot; , which we will discuss later.\nArray index strings (type kIntegerIndex = 0b00 ): these are decimal-integer-looking strings whose numeric value fits in 24 bits. bits 2-25 (24 bits) hold the numeric value of the integer ( ArrayIndexValueBits ), and bits 26-31 (6 bits) hold the string length ( ArrayIndexLengthBits ).\nThe layout of the hash fields looks like this (the least significant bits are on the right):\n&quot;hello&quot; (type: kHash = 0b10):\n+-----------------------------------+---+\n| 30-bit rapidhash |1 0|\n+-----------------------------------+---+\n31 2 1 0\n&quot;1234&quot; (length=4, value=1234, type: kIntegerIndex = 0b00):\n+--------+--------------------------+---+\n| 000100 | 000000000000010011010010 |0 0|\n| length | numeric value | |\n+--------+--------------------------+---+\n31 26 25 2 1 0\nAs you can see, the hash for array index strings is fully deterministic and there&#x27;s no seeding involved. An attacker can easily predict their hash values, and with a bit of effort, craft colliding inputs.\nExploiting the deterministic hash for array index strings\nOne of the most commonly used hash tables in V8 is the string table , which is used for string internalization - deduplicating and caching strings in a global table for time and memory efficiency. The string table uses open addressing with quadratic probing to deal with collisions. The probing sequence is:\nslot 0 = hash &amp; ( capacity - 1 ) , capacity = 2 n slot k = ( slot k - 1 + k ) &amp; ( capacity - 1 ) = ( slot k - 1 + k ) ( mod capacity )\nThe string table grows by adding 50% slack and rounding the capacity to a power of two , so hash &amp; (capacity - 1) equals hash mod capacity , and two strings with congruent hashes land on the same first-probe slot. For array index strings, the hash is (length &lt;&lt; 24) | value - but because the capacity is usually smaller than 2 24 , the length bits are usually masked away and the slot depends only on the numeric value mod capacity. Take a table with initial capacity 32768 for example:\nString Hash Hash &amp; (capacity - 1) Slot &quot;1234&quot; (4 &lt;&lt; 24) | 1234 0x040004D2 &amp; 32767 1234 &quot;34002&quot; (5 &lt;&lt; 24) | 34002 0x050084D2 &amp; 32767 1234 &quot;525522&quot; (6 &lt;&lt; 24) | 525522 0x060804D2 &amp; 32767 1234\nAll three collide on slot 1234. The attacker can fill the probing sequence from the target value&#x27;s first-probe slot to cause severe performance degradation, as demonstrated in the PoC in the HackerOne report .\nconst payload = [] ;\nconst val = 1234 ;\nconst MOD = 2 ** 19 ;\nconst CHN = 2 ** 17 ; // chain length\nconst REP = 2 ** 17 ; // repetitions of the target value\n// Build the quadratic probing chain\nlet j = val + MOD ;\nfor ( let i = 1 ; i &lt; CHN ; i ++ ) {\npayload . push ( ` ${ j } ` ) ;\nj = ( j + i ) % MOD ;\n}\n// Repeat the target value to force lookups through the chain\nfor ( let k = 0 ; k &lt; REP ; k ++ ) {\npayload . push ( ` ${ val } ` ) ;\n}\n// On the client side: attacker crafts an adversarial JSON payload\n// and sends it to the remote server.\nconst string = JSON . stringify ( { data : payload } ) ;\n// On the server side: V8 inserts the numeric strings in a hash table\n// for internalization, the collisions lead to extreme amplification\n// in resource consumption.\nJSON . parse ( string ) ; JavaScript Copy to clipboard\nWith a ~2 MB payload, this can significantly slow down the JSON parsing - even on a powerful MacBook, this can hang for about half a minute. This means for servers that parse JSON from untrusted sources, a remote attacker can cause significant disruption via extreme asymmetric resource consumption . Since the same hashing scheme is also used for many other V8 internals, e.g., Map keys, this has a wide attack surface.\nHashDoS resistant vs. efficiently reversible\nAs mentioned above, the standard mitigation for HashDoS is to seed the hash function, so we need to look for a more robust, seeded hashing scheme for array index strings.\nWhen we talk about hashes for security purposes, we often naturally think of cryptographic hashes - which are, by design, irreversible. And here we have a dilemma: V8&#x27;s array index hash is not just a hash - it&#x27;s a reversible encoding. This enables an important optimization that happens everywhere in V8: for example, in many fast paths that involve string-to-integer conversion, like parseInt(&quot;42&quot;) or obj[&quot;42&quot;] = 1 , instead of trying to parse the number from the string (whose content is not necessarily in CPU cache), V8 simply reads the raw_hash_field of the string and extracts the numeric value directly from the hash field. V8 also takes advantage of this encoding in e.g., string equality checks, where it would just compare two integer strings by their hashes. By nature, an irreversible cryptographic hash would break these optimizations and could lead to significant performance regressions in many hot paths.\nSo it looks like we are in a bind: do we have to either live with this vulnerability because of the reversibility requirement, or accept the performance regression?\nFinding a middle ground: minimal HashDoS resistance\nNow is the time to take a step back and think about what exactly makes a hash resistant to HashDoS attacks. It turned out that those who looked into rapidhash wondered the same and proposed the idea of &quot;minimal HashDoS resistance&quot; , with constraints that apply to our case:\nThe attacker cannot observe the hash output: The hash values are internal to V8 and not exposed directly to JavaScript or network responses. The attacker has to work blind.\nNode.js trusts the ","summary":"V8 hashes array-index-looking strings with a fixed, unseeded function, so an attacker can predict collisions and craft inputs that stall the event loop. A remote client sending a crafted JSON payload to a Node.js server can cause severe slowdowns or a sustained denial of service.","severity":"critical","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"node","fixed":[],"affected":"","ecosystem":"node"}],"ecosystems":["node"],"tags":["api-servers","node","node.js","runtime","self-hosted"],"not_affected_if":["browser-only-use","no-untrusted-input"],"how_to_check":"- Run `node --version` and compare against the patched releases listed in the Node.js March 2026 security release notes.\n- unknown","fix":"- Upgrade Node.js to the patched release from the March 2026 security release (CVE-2026-21717) for your release line.\n- unknown","status":"open","published_at":"2026-03-24T20:50:00+00:00","first_seen_at":"2026-10-23T19:13:00+00:00","triaged_at":"2026-10-25T05:37:26+00:00","updated_at":"2026-10-25T05:37:26+00:00","category":"runtime","confidence":0.7,"triage":{"at":"2026-10-25T05:37:26Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade Node.js to the patched release from the March 2026 security release (CVE-2026-21717) for your release line.\n- unknown","reach":"internet","title":"Node.js: array index string hashes are predictable, enabling hash flooding slowdowns","impact":"medium","summary":"V8 hashes array-index-looking strings with a fixed, unseeded function, so an attacker can predict collisions and craft inputs that stall the event loop. A remote client sending a crafted JSON payload to a Node.js server can cause severe slowdowns or a sustained denial of service.","category":"runtime","nicknames":[],"who_cares":["node.js","self-hosted","api-servers"],"confidence":0.7,"needs_auth":"no","worst_case":"Remote requests stall the event loop, causing sustained denial of service.","analyst_note":"Confirm patched Node.js versions and the exact fix range from https://nodejs.org/en/blog/vulnerability/march-2026-hashdos.","how_to_check":"- Run `node --version` and compare against the patched releases listed in the Node.js March 2026 security release notes.\n- unknown","needs_analyst":true,"not_affected_if":["browser-only-use","no-untrusted-input"],"who_is_affected":"Servers built on Node.js that parse or store untrusted input, such as JSON from clients or object keys built from request data, are exposed because V8 uses the same string table and Map internals for these values. Browser use is not considered a vulnerability in this context. Teams that only run Node.js on trusted internal data may skip it.","user_interaction":"no","needs_special_setup":"no","severity_if_unknown":"critical"},"cost_usd":0.001031,"input_tokens":7461,"output_tokens":570,"severity_before":"critical"},"analyst_status":null,"analyst_note":null,"search":"'-2026':149C '-21717':150C '/v8':320C '1':341C '2':390C '2026':18A,154C 'address':147C,614C 'affect':539C 'allow':443C 'along':265C 'also':194C,270C 'analysi':255C,294C 'api':22B 'api-serv':21B 'applic':399C 'array':33B 'array-index-look':32B 'attack':44B,177C,432C,436C,505C,622C,702C 'avail':88C 'averag':342C 'base':563C 'believ':169C 'bigger':589C 'blind':176C 'block':575C 'browser':528C 'call':494C 'case':403C,519C,720C 'caus':69B,446C 'cheung':110C 'client':58B 'close':551C 'collis':47B,332C,451C 'come':328C 'commerci':78C 'common':493C 'complex':344C 'concern':413C 'consid':533C 'constraint':287C 'contradictori':249C 'core':691C 'could':480C 'craft':49B,61B,713C 'critic':674C 'cve':148C 'data':313C,645C 'defin':650C 'degrad':183C,386C 'denial':75B,502C 'deploy':597C 'design':242C,337C 'detail':233C 'determinist':697C 'develop':2A,96C,111C,162C,654C 'discuss':271C 'dos':501C,529C,621C 'draw':603C 'e.g':346C 'easili':628C 'ecosystem':92C,684C 'effect':467C 'effici':200C 'effort':473C 'embedd':657C 'enough':172C,476C 'entir':577C 'evalu':259C 'event':54B,561C,578C 'event-loop-bas':560C 'except':323C 'explain':237C 'extens':604C 'fall':665C 'find':439C,612C 'fix':39B,660C 'flood':497C 'freez':468C 'frozen':478C 'function':41B,699C 'go':230C 'happen':124C 'hash':11A,31B,105C,120C,127C,166C,264C,281C,305C,325C,334C,358C,406C,457C,496C,500C,689C,698C,706C 'hash-do':499C 'hashdo':5A,99C,114C,133C,297C,506C,633C,662C,686C,693C 'hashdos':19A 'help':610C 'hold':211C 'host':29B 'impact':590C 'implement':289C 'import':218C,312C 'index':34B 'input':50B,427C,464C,546C,710C,714C 'insert':350C,377C,394C 'insid':641C 'integ':10A,104C,119C,165C,206C 'intern':282C,456C,644C 'invert':201C 'issu':530C 'jc':108C 'joye':109C 'json':62B 'key':215C,356C,380C,396C 'larg':448C 'lead':284C 'led':159C 'lie':695C 'like':617C 'locat':375C 'look':35B 'lookup':348C 'loop':55B,562C,579C 'lts':85C 'lurk':640C 'maintain':220C,669C,677C 'mainten':84C 'malici':545C,570C 'manag':437C,573C 'mani':355C,374C,398C 'map':362C 'march':17A,153C 'matter':302C 'mayb':415C 'mean':659C 'meet':245C 'minim':4A,98C,113C 'mitig':629C,685C,728C 'mix':731C 'model':187C 'much':472C,588C 'n':389C,395C 'need':129C,423C 'new':379C 'node':20A,24B 'node.js':1A,25B,66B,155C,304C,319C,602C 'node.js/v8':318C 'nodejs':16A 'nuisanc':418C 'number':449C,462C 'o':340C,388C 'occasion':417C 'one':308C,592C 'openj':91C 'oper':651C 'optim':224C 'origin':205C 'part':675C 'particular':636C 'partner':95C 'past':82C 'path':441C 'payload':63B 'perform':182C,223C,257C,293C,404C,721C 'period':488C,583C 'permut':196C 'phase':86C 'platform':516C 'possibl':384C 'post':227C 'predict':46B,704C 'prevent':174C 'problem':330C 'process':425C,543C 'program':94C 'puzzl':141C 'qualiti':261C 'quick':8A,102C,117C,136C 'random':214C 'rang':514C 'recov':203C 'releas':157C 'reliabl':179C,681C,716C 'remot':57B 'render':481C 'request':571C 'requir':250C 'research':608C 'resist':6A,100C,115C,134C 'restrict':632C 'revers':9A,103C,118C,137C 'runtim':26B,210C,567C,601C,668C 'sampl':252C 'scheme':128C 'scrutini':605C 'secret':213C 'secur':156C,607C,679C 'seed':688C,733C 'seem':248C 'self':28B 'self-host':27B 'send':59B 'server':23B,67B,421C,454C,565C,599C,725C 'server-sid':564C,598C 'servic':77B,483C,504C 'sever':70B,181C 'show':509C 'side':566C,600C 'simpli':550C 'singl':347C,569C 'slot':366C 'slowdown':71B 'small':461C 'softwar':316C 'solv':145C 'specif':273C 'spot':616C 'stall':52B 'standard':727C 'statist':254C 'store':277C 'string':36B,280C 'structur':314C,646C 'support':79C 'sustain':74B,93C,487C,582C 'tab':541C,553C 'tabl':306C,326C,335C,369C,407C,458C 'target':724C 'techniqu':290C 'theoret':383C,412C 'thread':470C,477C 'threat':186C 'time':191C,343C,490C,585C 'total':392C 'tri':143C,243C 'tricki':637C 'trigger':180C,717C 'trust':652C 'two':247C 'typic':537C 'unavail':484C 'unlik':619C 'unpredict':171C 'unseed':40B 'untrust':426C 'us':611C 'use':279C,525C,558C,643C 'user':548C 'userland':631C 'usual':409C,664C 'v8':13A,30B,107C,122C,221C,276C,521C,656C 'val':734C 'valu':207C,359C,707C 'vector':433C,623C 'version':81C 'vnd':15A 'vnd-nodejs-march':14A 'vulner':236C,507C,534C,634C,663C,694C 'walk':372C 'way':267C 'weak':615C 'well':649C 'well-defin':648C 'whose':357C 'wide':513C,596C,642C 'without':471C 'worst':402C,719C 'worst-cas':401C,718C 'yet':7A,101C,116C","nicknames":[],"mitigations":[],"who_affected":"Servers built on Node.js that parse or store untrusted input, such as JSON from clients or object keys built from request data, are exposed because V8 uses the same string table and Map internals for these values. Browser use is not considered a vulnerability in this context. Teams that only run Node.js on trusted internal data may skip it.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-03-24T20:50:00+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"nodejs","external_id":"march-2026-hashdos","url":"https://nodejs.org/en/blog/vulnerability/march-2026-hashdos","severity":"critical","published_at":"2026-03-24T20:50:00+00:00","modified_at":null,"first_seen_at":"2026-10-23T19:13:00+00:00","fetched_at":"2026-10-25T14:54:47+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}