-
Notifications
You must be signed in to change notification settings - Fork 977
Misuse of Math.random() to generate random integers #6944
Comments
We do use random-lib in our ledger (payments) code; it wouldn't be hard to rework other instances to call that instead Docs on Math.random() explaining the exclusivity issue shown in the first example (4 never being possible because 1 is excluded from Math.random results) Good find, thanks @dimitri-xyz! |
Which instances are worth fixing? https://github.com/brave/browser-laptop/search?l=JavaScript&q=random&type=Code&utf8=%E2%9C%93 @@ -19,7 +19,7 @@ const urlutils = require('../lib/urlutil')
const siteTags = require('../constants/siteTags')
const config = require('../constants/config')
const backgrounds = require('../data/backgrounds')
-const {random} = require('../../app/common/lib/randomUtil')
+const random = require('random-lib')
const ipc = window.chrome.ipcRenderer
@@ -65,7 +65,7 @@ class NewTabPage extends React.Component {
return this.state.showImages && !!this.state.backgroundImage
}
get randomBackgroundImage () {
- const image = Object.assign({}, backgrounds[Math.floor(random() * backgrounds.length)])
+ const image = Object.assign({}, backgrounds[random.randomInt({ min: 0, max: backgrounds.length })])
image.style = {backgroundImage: 'url(' + image.source + ')'}
return image
} |
@liunkae , Fixing this turns out to be more complicated than I thought. There are 2 separate issues here:
I think we should fix (1) now, but leave (2) for later. To fix the first issue, fix all instances where a floating-point random value obtained through The second problem (non-uniform distribution) turns out to be worse than I thought. The Mozilla Docs are suggesting an incorrect way to do it ( The correct way to generate random integers is Rejection Sampling. |
I'm the author of @liunkae for the options passed to As to fixing the bias issue, @dimitri-xyz has written up a very thorough blog post detailing the issue with the algorithm mozilla details and i'll be attempting to fix this issue over the next week or so (if anyone has the time to get to this faster, pull requests are very welcome!). I'll update here once a version is available that removes the bias. Edit: and after reading their blog post thoroughly, it looks like this bias is really really small; so while I'll definitely want to get it fixed, as @dimitri-xyz highlights it's probably not a huge concern in the interim. |
Hi! I think I have corrected all the flooring and all the rounding, but I'm new and I don't know what should I do now. |
@liunkae wanted to let you know this hasn't been forgotten; i've written up a separate module—rejection-sampled-int—which implements the rejection sampling algorithm and should make for unbiased integers from nodejs/browser crypto. However, I haven't had this audited yet, so I haven't integrated it into random-lib yet. It is also significantly slower, as you might imagine, so for areas where the bias is not as much of a concern, you may find it faster to use a biased method such as the ones already in Anyhow, if anyone has a chance to audit that module, it'd be hugely appreciated. Then, brave can just use that module as necessary; it doesn't seem to use any functions of (n.b. there is actually a PR up for |
@dimitri-xyz thanks for pointing out this issue and the detailed explanations. Although we are not currently using Math.random and random-lib for purposes that require cryptographic randomness (such as generating crypto keys), I labeled this issue under security to make sure that these libraries are not misused in the future. @333aleix333 thanks. please fork our repository, push your changes to the fork, then open a pull request against our repository. see here for some instructions: https://gist.github.com/Chaser324/ce0505fbed06b947d962 |
random-lib has been updated to version 3.0, which should solve this issue; see here for the changes. It's now backed by rejection-sampled-int for integers, which I also wrote. I would definitely appreciate additional sets of eyes on it, as it's not gone through any formal audit, yet I have tested the features in both libraries heavily. The API also changed in this version as the old one had behaviors that were very awkward, however conversion to the new API is super-straightforward since there wasn't much surface there to begin with. Let me know if you have any questions! |
If that's the case, you guys should close this issue! @bsclifton |
@arsalankhalid looks like we're still using version |
There were API changes between v2 and v3. It should not be very involved, but it will require a few more changes. See https://github.com/fardog/node-random-lib/blob/master/CHANGELOG.md#300 I had intended to submit a pr, but wasn't able to get the brave browser tests working on my local machine in the short time I was able to allocate. If someone has the chance though, it should not be a significant change set. |
@bsclifton Do we need an update for the |
* fixes brave#6944
I just put up a PR for it. tests aren't running right on my local machine, but looks like they're passing in CI; should be good? Note that this still needs to be updated in other brave projects (bat-publisher, ledger-publisher, etc); i'll open tickets there, but unsure when/if i'll get to it, so someone definitely should if they have the time! |
submitted prs to bat-publisher and bat-client, since the others are deprecated ^ |
fix #6944 Some of these changes change the _distribution_ of probabilities to the possible outcomes, where it seemed obvious that a uniform distribution was intended but not previously attained. Some of these changes also change the _support_, the set of possible outcomes, where it made things simpler and was not obviously intended one way or another. Many of these are not _necessarily_ bugs, but if deviations from uniform are actually intended they should be clearly justified. Full details: - Change Math.floor(Math.random() * n) to crypto.random.uniform(n). - app/extensions/brave/content/scripts/adInsertion.js - js/about/newtab.js (with care to preserve test spying in newTabPageTest.js) - js/about/preferences.js (which used |0 rather than Math.floor) - test/about/bookmarksManagerTest.js - test/lib/brave.js - This was obviously intended to be a uniform distribution on {0,1,2,...,n-1}. It is nonuniform only because of IEEE 754 binary64 floating-point approximation. - Change Math.round(Math.random() * (n-1)) to crypto.random.uniform(n) when used as an index into an n-element array. - tools/lib/randomHostname.js, _chooseRandomLetter / ALPHABET - tools/lib/randomHostname.js, tld / TLDS - tools/lib/synopsisHelpers.js, publisher / PROTOCOL_PREFIXES - tools/lib/transactionHelpers.js, generateContribution: amount - (Adjusted to use the actual length of the array, not just the first four elements of an eight-element array.) - This gave half the probability to 0 and n - 1 that it gave to 1, 2, 3, ..., n - 3, n - 2, a much bigger deviation from uniform than IEEE 754 binary64 floating-point approximations of the above. There was no obvious reason why this nonuniformity was desirable. - Change Math.round(Math.random() * n) to crypto.random.uniform(n) when _not_ used as an index into an array. - tools/lib/synopsisHelpers.js, numHosts - tools/lib/synopsisHelpers.js, numVisits - tools/lib/transactionHelpers.js, generateTransaction: count - tools/lib/synopsisHelpers.js, duration - This actually changes the _support_ of the distribution to exclude n, but in the only places where this was used, it is not otherwise obvious that the caller intended to include n in the support. It could have been a typo for Math.floor(Math.random() * n) without adverse consequences in these contexts. - Change Math.round(Math.random()) to crypto.random.uniform(2). - tools/lib/randomHostname.js, numParts - This was obviously intended to be a uniform distribution on {0,1}, and actually it may even be uniform under Math.random as typically implemented, but writing crypto.random.uniform(2) is clearer to flip a coin and this way we just avoid Math.random() altogether. - Obscure corner cases: - app/renderer/rendererTabEvents.js, nonce - This used Math.random().toString() to generate a string `that is unlikely to collide'. There are only 2^64 64-bit floating-point values, of which Math.random() returns a small subset. A collision is expected after only a few billion uniform random samples, which is far from impossible. I replaced it by a uniform random 256-bit quantity in hex, which will never collide even in the view of a conservative paranoid cryptographer unless there is a deeper bug breaking the PRNG. - tools/lib/randomHostname.js, partLen - The distribution originally used here puts higher probability on the minimum part length than everything else, and lower probability on the maximum part length than anything else. I see no reason why this should not just be uniformly distributed on part lengths {minLength, minLength + 1, ..., maxLength - 1, maxLength}, so I replaced it by minLength + uniform(maxLength - minLength + 1). - tools/lib/transactionHelpers.js, generateBallots: votesToCast - The previous expression, Math.min(Math.round(Math.random() * votesRemaining) + 1, votesRemaining), assigned half weight to 1 and half-again weight to votesRemaining - 1 (as compared to all other numbers) for no obvious reason. I changed it to use 1, if only 1 vote is remaining, or 1 + uniform(votesRemaining - 1), if more than one vote is remaining. P.S. It is a bit confusing that we have a module named `brave-crypto` which is usually used as `const crypto = require('brave-crypto')` while there is also a standard nodejs module called `crypto` which is usually used as `const crypto = require('crypto')`. In one case these are used in the same file, so I named ours `braveCrypto`.
In
browser-laptop/app/extensions/brave/content/scripts/adInsertion.js
Lines 74 to 75 in 57e6965
That is:
The last value 'IAB20' is excluded from the range of random() and will never be chosen.
Here's another bug:
browser-laptop/tools/lib/transactionHelpers.js
Line 48 in 57e6965
In this case, both 0 and 100 are part of the results, but only occur half as frequently as the other numbers. To see why this happens consider:
all numbers from 0 to 0.49999 will return 0
all numbers from 0.5 to 1.4999 will return 1
all numbers from 1.5 to 1.9999 will return 2
So 1 occurs with probbility 1/2 and 0 and 2 are returned with probability 1/4 each.
This is very different from the expected uniform distribution (1/3,1/3,1/3).
The problem here is the flooring or rounding of floating point random numbers to obtain integers.
I think the "proper" way to obtain random integers in a given range is to use
randomInt
fromrandom-lib
There are many more instances of this in the code. Just do a search for random() to find them.
The text was updated successfully, but these errors were encountered: