# Deno: Miller-Rabin Primality Test Allows Zero Rounds

Deno's node:crypto checkPrime ran zero Miller-Rabin rounds at default checks=0, so composites with factors above 17,863 were reported prime.

- Payout: Research
- Severity: high
- Program: Deno
- Found by: HaoPham23
- Disclosed: 2026-06-16
- CVE: CVE-2026-49440
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-9xg4-qhm4-g43w
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-9xg4-qhm4-g43w
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs tech:rust technique:parser-differential technique:source-review vuln:crypto

**Target:** node:crypto checkPrime / checkPrimeSync polyfill and op_node_check_prime Rust ops
**Root cause:** Deno's Rust is_probably_prime treated caller-supplied checks=0 as 'skip the Miller-Rabin loop entirely', leaving only trial division by primes up to 17,863; Node forwards 0 to OpenSSL BN_check_prime which substitutes FIPS 186-4 default rounds by bit length.
**Steps:**
1. Call `checkPrimeSync(17881n * 17891n)` with default options (or `{ checks: 0 }`).
2. Both factors exceed the trial-division ceiling of 17,863, so no small-prime divisor is found.
3. Miller-Rabin loop bound is `count = 0`, so zero rounds execute and the function returns `true`.
4. Patched version clamps rounds to `count.max(min_miller_rabin_rounds_for_bits(bits))` and returns `false`.
**How found:** Reporter compared Deno's Rust primality implementation against Node's OpenSSL-backed BN_check_prime and noticed Deno had no bit-length-based default round fallback, so checks=0 meant the loop never ran.
**Exploitation:** Application-level only: a Deno program validating an attacker-supplied bignum (peer DH prime, config prime, RSA factor sanity check) with default options accepts a crafted composite; downstream protocol guarantees collapse to whatever the attacker engineered into the composite.
**Insight:** When a polyfill reimplements a crypto primitive, diff its handling of sentinel/default parameter values (0, null, undefined) against the upstream library — upstream may substitute a safe default where the reimplementation treats the value literally.

## Proof of concept (copied exactly from the original report)

**checkPrimeSync accepts composite as prime**

```
import { checkPrimeSync } from "node:crypto";

// 17881 and 17891 are both prime and both above the trial-division
// ceiling used by Deno's implementation.
const composite = 17881n * 17891n;

// Affected versions print `true`; the patched version prints `false`.
console.log(checkPrimeSync(composite));
```

**Rust is_probably_prime zero-round assertion**

```
use num_bigint::BigInt;
let composite = BigInt::from(17881u32) * BigInt::from(17891u32);
assert!(!is_probably_prime(&composite, 0)); // fails on affected versions
```

