How a 171-Character JavaScript Payload Runs Code With No Parentheses in V8
Software / explainer
How a 171-Character JavaScript Payload Runs Code With No Parentheses in V8
A blog post that reached 262 points on Hacker News chains Error.prepareStackTrace, the Function constructor and a polluted prototype, and each link in the chain has a documented purpose.
A blog post by an author who signs as bartosz shows how to run alert(1337) in JavaScript without typing a parenthesis, using three documented features of V8, the engine in Chrome and Node.js. It reached 262 points and 105 comments on Hacker News. The post carries no date.
The payload, quoted from the post, is a single line:
Object.prototype.message='alert\x281337\x29';Array.prototype.toString=Error.prototype.toString;Error.prepareStackTrace = Function;const x = new Error;x.valueOf=x.stack;+x;
The \x28 and \x29 are hex escapes for the two parenthesis characters, so the string is only turned into code by the engine, never typed. The Terminal did not run it; each step below is checked against the post and two reference pages.
The three moving parts
| Step | What it does | Where documented |
|---|---|---|
Error.prepareStackTrace = Function | Makes V8 call the Function constructor when stack is read | V8 stack trace API |
Object.prototype.message and Array.prototype.toString | Turns the stack array into the payload string | The blog post |
x.valueOf=x.stack; +x | Calls the new function through type coercion | The blog post |
Why prepareStackTrace is the entry point
V8's stack trace API formats a stack "on demand, the first time the stack property is accessed." A custom Error.prepareStackTrace receives the error and "an array of CallSite objects, each of which represents a stack frame", and whatever it returns becomes the value of stack, including non-string values.
That last detail is what the trick uses. Assigning the built-in Function as the formatter means reading x.stack calls Function(error, frames) and returns a new function instead of a string.
Why Function takes the payload as code
Per MDN, the last argument to the Function constructor is the function body and every earlier argument is a parameter name. The post notes the generated function works "as long as the second argument is valid javascript code." A stack array is not code, so the author controls how it converts to a string.
The post gives two routes. One overwrites Array.prototype.toString with a function returning a constant. The other, which the author prefers, uses prototype pollution: set Object.prototype.message to the payload and point Array.prototype.toString at Error.prototype.toString, which the author says keeps the chain "in the Error realm." The post does not fully explain why the resulting string parses as valid JavaScript, and that gap is the part a reader should test in a browser console rather than take on trust.
Why valueOf finishes the job
The last two statements store the generated function as x.valueOf and then apply the unary plus operator. +x asks the object for a primitive value, which calls valueOf, which now runs the payload. The author calls this the valueOf trick and chose it over the other option the post lists, overwriting toString and concatenating the object with a string. The post also links a Stack Overflow thread of other methods without listing them.
Statement order matters in the payload. Because V8 formats stack on first access, the pollution and the Error.prepareStackTrace assignment both come before x.stack is read, and the read itself is what produces the function that x.valueOf then holds.
What it needs, and what it does not
The technique depends on V8 behaviour, and the post tests no other engine. It needs a write primitive on Object.prototype and Array.prototype, which is why the author frames it around prototype pollution. It also needs Function to be allowed to compile strings.
That last requirement is where defenders have a documented control. MDN calls such constructors "injection sinks" that are "a potential vector for cross-site-scripting (XSS) attacks", and says a Content Security Policy keyword unsafe-eval also allows Function(). MDN recommends trusted-types-eval instead, with the example policy require-trusted-types-for 'script'; script-src '<your_allowlist>' 'trusted-types-eval'.
The practical reading for a team that filters input for parentheses: the 171 characters above contain no literal parenthesis anywhere, because the two that matter exist only as escapes inside a string. A blocklist on that character would pass the payload untouched, so it is not a control. A policy that refuses to compile strings is. The author says "i am sure there are more ways to achieve this", so the next thing to check is whether the Stack Overflow thread's methods also pass through Function, which would make them subject to the same policy. The related OpenBSD root exploit write-up shows the same pattern at the system level, where a small primitive is chained into code execution, and the Atlassian CVE-2026-21589 report shows what follows once a public exploit exists.
Sources
More in Software
- 01Rust's Two Biggest Compiler Projects Carry No Dates; the Measured Speed-Up Came From LLVM 23A compiler contributor's October 19 roundup and the Rust project's own goal pages agree on the list. They disagree on how much of it is measured.
- 02Mastodon 5.0 Moves the Composer to Bottom-Centre and Puts Stable Release 'a Few Weeks' OutThe project's October 19 design post lists what changed since the August preview and what was cut. Search, Settings, Messages and Notifications are still on the old interface.
- 03iPadOS 27 Gives the AirPlay Screen to Whichever App You Tapped Last, and Keynote Quits the ShowAn educator's Feedback report, FB23615646, describes presenter notes and unrevealed poll results appearing on a classroom screen. Apple's Keynote guide documents a presenter display for notes, a timer and a next-slide preview.
- 04Google Cloud's London Zone Went Down at 06:08 UTC. Its Status Page Waited 2 Hours 27 MinutesGoogle's incident log for europe-west2-b names a power event as the cause only at 11:12 UTC, five hours into an outage that ended at 14:00 UTC and reached Revolut, Lloyds and Royal Mail.