Threat Model

Your random numbers run on their machine

The question usually arrives in a specific form: we run a prize draw in the browser, or we pick which discount a visitor gets, or we assign users to a pricing experiment, and someone wants to know whether obfuscating that code is enough. It is a fair question and it has a clean answer, but the answer is not about the obfuscator. It is about which machine computes the number, and that turns out to settle the whole thing before the transform is even relevant.

Two different properties both get called random

Start by separating them, because most of the confusion in this area comes from one word doing two jobs. Math.random is a general-purpose generator built for speed. It produces values that spread out well, which is what you want for jitter, sampling and animation. It is not built to resist an adversary, browser vendors document it as unsuitable for security purposes, and its internal state is a small amount of data that determines every value it will emit for the rest of the page's life.

crypto.getRandomValues is a different thing: it fills a typed array from the platform's cryptographic generator. Its output is unpredictable in the sense a cryptographer means. If your code is picking a value that a third party must not be able to guess, this is the correct function and Math.random is a defect.

So the first fix is real, and it is worth doing. It is also not the fix that the question at the top of this page is asking for.

The property you need is unpredictable to a specific person

Randomness on its own is not a requirement. The requirement is always unpredictable to somebody, and it is worth naming who. There are three candidates and they are not interchangeable.

A passive third party, watching traffic or guessing at values, is the case cryptographic generators are designed for. Against that adversary crypto.getRandomValues is exactly right.

Your own server is the second, and it is easy: the server can simply generate the value itself.

The user is the third, and it is the one that breaks. A value produced by client-side code is produced inside a process the user owns. They can pause it, inspect memory, read the variable, watch the request that carries it, or replace the function that produced it. No generator changes this, because the problem is not the quality of the number. It is the location of the computation. If the person who benefits from a favourable outcome is the same person whose machine computes it, you have asked the wrong party to roll the dice.

What protection changes here, and what it does not

Obfuscation does something real: it makes the call site harder to find. Identifiers no longer say selectWinner, control flow does not read top to bottom, and string literals that named the feature are no longer sitting there as landmarks. Somebody skimming the bundle for the draw logic has to work for it. That is the cost the transform is designed to raise, and it is raised.

What it does not do is move the computation. The value is still produced on the user's machine, and there are three ways to reach it that do not require reading the code at all. You can take it after the fact, because the result has to be used, which means it lands in a variable, a rendered element or a request body. You can break on it, because a debugger stops on the state rather than the source. Or you can replace the source of the value before the bundle ever runs, which is the cheapest of the three and the one that usually matters.

That last one is a single assignment in the console, or in an extension, or in a userscript, executed before your code loads. Nothing in the protected bundle has to be understood for it to work.

The watch list does not cover the random sources

This is the part worth checking against the engine rather than assuming, because it is a reasonable thing to assume and it is wrong. The anti-tampering option watches a default set of twenty-four paths and reports the first replacement it sees. That set is aimed at the boundary where application code talks to the outside world: fetch, XMLHttpRequest, WebSocket, navigator.sendBeacon, EventTarget.prototype.addEventListener, setTimeout and clearTimeout, Promise.prototype.then, the Storage accessors, several Array.prototype methods, JSON.parse and JSON.stringify, Object.defineProperty and its descriptor reader, Function, Function.prototype.toString, eval, and crypto.subtle.digest.

Read that list for what is missing. crypto.subtle.digest is watched; crypto.getRandomValues is not. Math.random is not there either. A page running the option at its defaults will not notice a replaced random source, and will notice a replaced fetch immediately.

You can close that specific gap. AntiMonkeyPatchingIncludeGlobals takes additional dotted paths and validates them against an identifier-path pattern, which both Math.random and crypto.getRandomValues satisfy. Adding them is a one-line configuration change and it is a reasonable thing to do.

It is also worth being honest about what you have bought. The check compares the current function against a reference captured earlier and, where a clean realm is available, against a pristine copy of the built-in. When it fails, it runs your configured failure action, which defaults to throwing. So you have a signal, generated on the user's machine, about tampering performed by the user, delivered by code that same user can edit. As telemetry about how common the behaviour is across your install base, that is genuinely informative. As the basis for deciding whether somebody won something, it is circular.

Draw the number where you can defend it

The fix is unglamorous and it is the same fix as every other item in this family. Generate the value on the server, record it there, and let the server decide what it means. The client is allowed to ask; it is not allowed to answer. A request arrives, the server draws, the server writes down what it drew and what it granted, and the response tells the browser what happened. The browser's job is to render the outcome, not to determine it.

This usually turns out to be less work than the client-side version, because the audit trail you need for support and disputes has to exist server-side anyway. A prize draw that happened in a browser leaves you with no record of the draw at all.

If your requirement is not just integrity but demonstrable fairness to a sceptical user, the established approach is commitment: the server picks a seed, publishes a hash of it before the event, and reveals the seed afterwards so anyone can recompute the outcome and check it against the published hash. That gives a participant a way to verify the result was fixed in advance rather than chosen after seeing their entry. It is more machinery than most products need, and it belongs on the server in every version of it.

A build seed is an unrelated thing with the same word in it

One clarification, because the vocabulary collides and it causes real confusion in review meetings. This product has a Seed option, and it makes protected output reproducible: the same input, the same options and the same seed produce the same bytes, which is what lets you diff two builds and verify an artefact against a manifest.

That is deliberately the opposite property from the one discussed above. It removes variation from your build so the result is repeatable. It has nothing to do with values your application generates at runtime, it is not a source of randomness for your code, and the documentation is explicit that a fixed seed is not a security control. When someone in a review says the build is seeded, that is a statement about reproducibility. It is not an answer to a question about how a winner is chosen.

A short checklist

  • List every place your client code calls a random source, and write next to each one what the value decides.
  • Anything that decides something a user benefits from moves to the server. Anything cosmetic can stay.
  • Where the value stays client-side and must resist a third party, use crypto.getRandomValues rather than Math.random.
  • If you run the anti-tampering option and want the signal, add Math.random and crypto.getRandomValues to the include list, and treat what it reports as telemetry.
  • Choose the failure action deliberately. A guard that throws on a machine you do not control converts a curious user into a support ticket.
  • Do not let a reproducible-build seed appear in a discussion about runtime randomness. They are different mechanisms that share a noun.

The short version

Obfuscation raises the cost of finding your draw logic and leaves the location of the draw exactly where it was. A value generated in a browser is a value the user obtained first, whichever function produced it. Use the cryptographic generator where the adversary is a third party, move the decision to the server where the adversary is the user, and keep the two meanings of the word seed apart while you do it.

Frequently asked questions

Is Math.random good enough if the code is obfuscated?

No, and the two properties are unrelated. Math.random is a fast general-purpose generator that is not designed to be unpredictable to someone observing its output, and browser implementations document it as unsuitable for anything security related. Obfuscation changes how hard the surrounding code is to read; it does not change where the number is produced or who can see it. Swapping to crypto.getRandomValues fixes the generator quality problem and still leaves the problem this page is about, because the value is produced inside a process the user controls.

Does crypto.getRandomValues solve it?

It solves one half. That function draws from the platform cryptographic generator, so the output is unpredictable in the sense that matters against a third party who is guessing. It does nothing about the user, because the number materialises in their browser, in their memory, on a machine where they can pause execution and read it. If the person who benefits from a favourable value is the same person running the code, a stronger generator changes nothing about who learns the value first.

Can the anti-tampering option detect a replaced Math.random?

Not with the default configuration, because neither Math.random nor crypto.getRandomValues is on the default watch list. That list covers twenty-four paths and is aimed at the places application code talks to the outside world: fetch, XMLHttpRequest, WebSocket, navigator.sendBeacon, addEventListener, the timer functions, storage accessors, several Array and JSON methods, Object.defineProperty, Function and eval, and crypto.subtle.digest. The random sources are absent. You can add them with AntiMonkeyPatchingIncludeGlobals, which accepts dotted identifier paths, so Math.random and crypto.getRandomValues are both valid entries.

If I add them to the watch list, is the draw then trustworthy?

No, and this is the distinction worth being precise about. Detection is not authority. Adding those paths means the guard notices when the function identity changes and runs your configured failure action, which defaults to throwing. What you have built is a tamper signal produced by code running on the same machine as the tampering, reported by a party who can edit the reporter. That is genuinely useful as telemetry about how often it happens, and it is not a basis for paying out a prize or granting a discount.

Which values genuinely have to come from the server?

Any value where the user gains from a particular outcome, and any value another system will later treat as authoritative. That covers prize and lottery outcomes, which discount or coupon a session receives, assignment to a pricing experiment, session identifiers, CSRF and authentication nonces, invite and referral codes, and idempotency keys that grant something. The test is not whether the number looks random in the bundle. It is whether you would accept the value if the user typed it in by hand, because functionally that is what a client-generated value is.

What is still fine to generate in the browser?

Plenty, and the distinction is whether anything is being decided. Retry backoff jitter, animation and visual variation, cache-busting suffixes, local sampling for performance instrumentation, and shuffling a list the browser already holds are all reasonable. The last one has a caveat worth stating: shuffling data client-side reveals nothing new only because the data was already delivered. If the ordering itself is the thing you did not want disclosed, the leak happened when you sent the full set, not when you shuffled it.

Related reading