CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



GHSA-9C48-W39G-HM26
2.70.02%

The Lonely Number: How '1' Crashed the Rust RSA Crate

Alon Barad
Alon Barad
Software Engineer

Feb 27, 2026·7 min read·9 visits

PoC Available

Executive Summary (TL;DR)

The Rust `rsa` crate (< 0.9.10) failed to reject `1` as a prime factor during private key construction. Because the check was `x < 1` instead of `x <= 1`, the value `1` slipped through. Later arithmetic operations involving `(p-1)` resulted in zero, triggering a panic (DoS) when the library attempted modular inversion or division.

In the world of safe systems programming, Rust is often touted as the silver bullet for memory corruption. However, logic bugs remain the domain of human error. CVE-2026-21895 in the popular Rust `rsa` crate demonstrates this perfectly: a subtle off-by-one error in input validation allowed the number `1` to mask itself as a prime factor. This seemingly harmless integer bypasses checks, infiltrates the cryptographic core, and triggers a division-by-zero panic, effectively crashing any application that processes untrusted private keys.

The Hook: Even Safe Languages Trip Over Math

Rust is the bouncer of the programming world. It checks your ID, pats you down for memory leaks, and ensures you aren't carrying any null pointers. It promises safety, and for the most part, it delivers. But here's the catch: Rust protects memory, not logic. It can stop a buffer overflow, but it can't stop a developer from writing a math equation that divides by zero.

Enter the rsa crate, a pure Rust implementation of the Rivest–Shamir–Adleman cryptosystem. It is widely used in the Rust ecosystem for everything from JWT signing to TLS handshakes. It’s supposed to be the bedrock of security. But prior to version 0.9.10, it had a glass jaw.

The vulnerability isn't a complex heap groom or a race condition. It’s a simple failure to respect the definition of a prime number. In RSA, you need two distinct large prime numbers, $p$ and $q$. By definition, a prime number must be greater than 1. The developer knew this. The code tried to enforce this. But due to a tiny syntactic slip, the number 1 was allowed into the VIP section. Once inside, 1 acts like a saboteur, turning vital cryptographic constants into zeros and causing the entire application to commit suicide via panic!.

The Flaw: The Off-By-One That Killed the Process

Let's talk about number theory for a second. The security of RSA relies on the difficulty of factoring the modulus $n$, which is the product of two large primes $p$ and $q$. When constructing a private key from its components, the library must validate that $p$ and $q$ are valid.

The flaw resided in src/key.rs within the RsaPrivateKey::from_components function. The code iterates over the provided prime factors to ensure they are sane. The check looked something like this:

// The logic prior to the fix
if *prime < BigUint::one() {
    return Err(Error::InvalidPrime);
}

Do you see the problem? BigUint is an unsigned large integer. It cannot be negative. The condition *prime < 1 only catches one value: 0.

If an attacker provides 1 as a prime factor, the check 1 < 1 evaluates to false. The error is skipped. The library accepts 1 as a valid prime number. The chaos begins when the library calculates the Euler's totient function, $\phi(n) = (p-1)(q-1)$. If $p=1$, then $(p-1) = 0$. Consequently, $\phi(n) = 0$.

RSA relies heavily on modular arithmetic modulo $\phi(n)$ or similar derived values for the Chinese Remainder Theorem (CRT). When the code eventually tries to perform a modular inverse or a division using this zeroed-out value, the underlying num-bigint library hits a hard wall: Division by Zero. In Rust, this isn't an undefined behavior that leads to RCE; it's a panic, which immediately aborts the thread or process. It is the ultimate table flip.

The Code: A One-Character Fix

Usually, fixing a cryptographic vulnerability involves checking constant-time implementations or padding oracles. Here, it involved adding an equals sign. The fix was pushed in commit 2926c91bef7cb14a7ccd42220a698cf4b1b692f7.

Here is the diff that saved the crate:

// src/key.rs
 
  for prime in &self.primes {
-     if *prime < BigUint::one() {
+     if *prime <= BigUint::one() {
          return Err(Error::InvalidPrime);
      }
      m *= prime;
  }

That’s it. By changing < (strictly less than) to <= (less than or equal to), the developer ensured that 1 is properly rejected alongside 0.

The fix also included a regression test to ensure this specific scenario—passing 1 as a prime—never happens again without triggering a proper Result::Err instead of a crash:

#[test]
fn test_key_invalid_primes() {
    let e = RsaPrivateKey::from_components(
        // ... params ...
        vec![
            BigUint::from_u64(1).unwrap(), // The saboteur
            BigUint::from_u64(239).unwrap(),
        ],
    )
    .unwrap_err();
    assert_eq!(e, Error::InvalidPrime);
}

This highlights a critical lesson in input validation: boundaries matter. When dealing with integers, the difference between < and <= is the difference between a stable system and a denial of service.

The Exploit: Weaponizing the Number One

Exploiting this does not require a debugger, heap spraying, or ROP chains. It requires a JSON editor. If an application accepts RSA keys from a user (for example, uploading a custom signing key, importing a JWK, or handling PKCS#8 files), it is vulnerable.

The Attack Chain:

  1. Target Identification: Find an endpoint that parses RSA private keys using the Rust rsa crate. This could be a server accepting JWT signing keys or a CLI tool processing key files.
  2. Payload Construction: Create a malformed key structure. You need a valid-looking RSA tuple $(n, e, d, p, q)$, but set $p = 1$.
  3. Delivery: Submit the key to the application.

When the application attempts to use this key—perhaps immediately upon load to precompute CRT parameters, or lazily when the first signature is requested—the process hits the division by zero.

In a web server context (e.g., actix-web or axum), if the panic occurs in the main thread or if the panic handler isn't configured to catch unwinds effectively, the entire service goes down. Even if it's caught, repeated requests can resource-exhaust the logging infrastructure or trigger restart loops.

The Impact: Why Denial of Service Matters

You might look at the CVSS score of 2.7 and laugh. "It's just a DoS, who cares?"

In modern microservices and serverless architectures, a crashing process is a nuisance. In a monolith or a critical infrastructure component, it's a disaster. If this crate is used in a blockchain node validating transaction signatures, a single malicious transaction could take the node offline. If it's used in an authentication gateway processing user-uploaded keys, the auth service dies.

While this vulnerability does not allow for data exfiltration (you can't steal the server's private keys) or remote code execution (you can't run shellcode), availability is a core pillar of the CIA triad. The "Low" severity rating is technically accurate due to the lack of Confidentiality/Integrity impact, but do not underestimate the annoyance of a service that crashes every time it sees the number 1.

Furthermore, because this is a panic (an abort) rather than a returned Error, it bypasses standard error handling logic. Developers expect Result<T, E> to handle bad inputs. They do not expect their thread to simply cease existing.

The Fix: Remediation Strategy

The remediation is straightforward: Update your dependencies.

Run the following command in your project root:

cargo update -p rsa

Ensure that Cargo.lock reflects a version of rsa greater than or equal to 0.9.10.

If you cannot update immediately, you must implement a sanitation layer before the data reaches the rsa crate. Manually inspect any components used to construct keys:

// Temporary workaround middleware
fn validate_primes(primes: &[BigUint]) -> Result<(), MyError> {
    for p in primes {
        if p <= &BigUint::one() {
            return Err(MyError::BadPrime);
        }
    }
    Ok(())
}

However, patching the library is the only robust solution. This bug serves as a reminder to audit Cargo.lock files regularly. Just because Rust is memory-safe doesn't mean it's immune to logic bombs.

Official Patches

RustCryptoCommit fixing the prime validation logic

Fix Analysis (1)

Technical Appendix

CVSS Score
2.7/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:U
EPSS Probability
0.02%
Top 95% most exploited

Affected Systems

Rust applications using `rsa` crate < 0.9.10Systems processing user-provided RSA keys (JWK, PKCS#8)Authentication services using RSA for signing/verification

Affected Versions Detail

Product
Affected Versions
Fixed Version
rsa
RustCrypto
< 0.9.100.9.10
AttributeDetail
CWE IDCWE-703
Secondary CWECWE-369 (Divide by Zero)
CVSS v4.02.7 (Low)
Attack VectorNetwork
ImpactDenial of Service (Panic)
Exploit StatusPoC Available

MITRE ATT&CK Mapping

T1499.004Endpoint Denial of Service: Application Exhaustion
Impact
CWE-703
Improper Check or Handling of Exceptional Conditions

Improper Check or Handling of Exceptional Conditions

Known Exploits & Detection

GitHubAdvisory containing description of the panic trigger

Vulnerability Timeline

Fix committed to master branch
2026-01-06
GHSA Advisory Published
2026-01-08
CVE Assigned
2026-01-08

References & Sources

  • [1]GHSA Advisory
  • [2]NVD Entry
Related Vulnerabilities
CVE-2026-21895

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.