CVE-2025-7783 is a very recent vulnerability affecting a lot of applications in the Node.js ecosystem including those which use axios or the deprecated request library. In all honesty, this vulnerability is really an edge case that is extremely unlikely to be exploited: it is dependent upon a number of events that are not normally present. One of those events is the attacker having access to five consecutive outputs of JavaScript Math.random( ) , which allows the attacker to predict future outputs of Math.random( ) using the z3 solver as a predictor.
When I looked into this attack, I couldn’t believe that z3 was the best one can do to “invert” (determine the internal seed) of the Math.random( ) generator. As a former cryptographer, I said to myself surely it is enough to only have 2 or 3 outputs to invert it. So I decided to prove it.
This blog is about my first step in the journey to find an improved algorithm. Math.random( ) uses an algorithm called Xorshift128+ under the hood, but it only outputs 52 of the 64-bits that Xorshift128+ generates. Below I will show a simple and efficient (226 operations) way to invert Xorshift128+ if at least two complete 64-bit outputs are given. This can be turned into an algorithm that inverts the full Math.random( ) , but it will require 3 outputs and currently it is somewhat inefficient (250 operations). I expect this will be improved later by either me or somebody else, perhaps you.
Ten years ago I wrote a blog called So, You Want to Learn to Break Ciphers which has had about 30,000 views. This blog aligns nicely with that previous one: nothing below is particularly complicated. I expect that a competent computer science graduate could understand it and potentially improve on it. So to the aspiring or amateur cryptographer, I invite you to give it a crack! You can download my source code from GitHub and work to improve it.
The Xorshift128+ algorithm
The source code for the Xorshift128+ used in Math.random( ) (v8 engine) can be found here. Yep, it is written in C++. The code is transcribed below:
static inline void XorShift128(uint64_t* state0, uint64_t* state1) { uint64_t s1 = *state0; uint64_t s0 = *state1; *state0 = s0; s1 ^= s1 << 23; s1 ^= s1 >> 17; s1 ^= s0; s1 ^= s0 >> 26; *state1 = s1; }
It takes in two 64-bit states, state0 and state1. At the end of the algorithm, the new state0 becomes the old state1 and the new state1 is entirely determined by exclusive-or operations of various bits from the old state0 and state1. Note that the bit shifts essentially select which bits get mixed into each position of the new state1. Math.random( ) performs an integer addition of the new state values and converts it to a double, which is the output. This is also where bits are dropped because a 64-bit unsigned int cannot be represented exactly as a double.
In our analysis, we are going to assume that we have the full 64-bit output of the new state0 + state1. We will show at the end how to deal without having that full output.
We’re going to need some notation rather than saying “old state” and “new state” all the time. Also, in my first write-up of this attack, I came to believe that the whole “state0” and “state1” gets too confusing and intimidating when I bring in other notation, so I decided to rename it. Going forward, we will refer to state0 as L (for left) and state1 as R (for right).
... continue reading