Skip to content

REPRO-2026-00225: Verified Repro With Script Download

REPRO-2026-00225: Node.js WebCrypto EdDSA verification accepted small-order points, allowing signature verification bypass for Ed25519/Ed448 before the 29890721 fix.

REPRO-2026-00225 is verified against nodejs · other. Affected versions: All Node.js versions before the fix commit; affects all supported release lines (22.x, 24.x, 26.x). This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00225.

REPRO-2026-00225 nodejs · other Jul 4, 2026 .txt
Severity
HIGH
Confidence
HIGH
Reproduced in
7m 45s
Tool calls
91
Spend
$1.51
01 · Overview

What Is REPRO-2026-00225?

Node.js's WebCrypto subtle.verify() for Ed25519/Ed448 (implemented in src/crypto/crypto_sig.cc) did not reject small-order points supplied as the public key or as the signature's R component. The identity/neutral (order-1) point satisfies the cofactorless EdDSA verification equation for any message when S=0, enabling universal signature forgery without knowledge of the private key.

02 · Severity & CVSS

REPRO-2026-00225 Severity

REPRO-2026-00225 is rated high severity.

HIGH threat level
Weakness CWE-347: Improper Verification of Cryptographic Signature

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

Affected nodejs Versions

nodejs · other versions All Node.js versions before the fix commit; affects all supported release lines (22.x, 24.x, 26.x) are affected.

How to Reproduce REPRO-2026-00225

$ pruva-verify REPRO-2026-00225
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00225/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for REPRO-2026-00225

Authorization bypass — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Small-order Ed25519 public key (identity point 0x01+31 zeros, or order-8 point c7176a70...037a) and crafted signature with S=0 and low-order R component, supplied to subtle.importKey and subtle.verify

Attack chain
  1. subtle.importKey(raw, smallOrderPubKey, {name:'Ed25519'})
  2. subtle.verify({name:'Ed25519'}, key, craftedSig, message)
  3. SignTraits::DeriveBits Mode::Verify in crypto_sig.cc
  4. context.verify() (OpenSSL EVP_DigestVerifyFinal, cofactorless)
  5. returns 1 for small-order points
  6. buf[0]=1 (verify result true)
Runnable proof: reproduction_steps.sh
Captured evidence: fixed test output
How the agent worked 222 events · 91 tool calls · 8 min
8 minDuration
91Tool calls
61Reasoning steps
222Events
1Dead-ends
Agent activity over 8 min
Support
14
Hypothesis
2
Repro
169
Judge
33
0:0007:45

Root Cause and Exploit Chain for REPRO-2026-00225

Versions: Node.js builds prior to commit 29890721cda51eeb64f4079143d55b69333599c2 (dated 2026-06-20). The vulnerability existed in the v27.0.0-pre development branch and any release built from source before the fix was merged.

Node.js WebCrypto's EdDSA (Ed25519/Ed448) signature verification via globalThis.crypto.subtle.verify accepted signatures where the public key or the signature's R component is a small-order (low-order) point, before commit 29890721cda51eeb64f4079143d55b69333599c2. This allowed an attacker to forge signatures that verify as valid without knowing the private key, by exploiting the mathematical properties of small-order points in the Ed25519/Ed448 groups. The most severe variant uses the identity/neutral element (order-1 point), which satisfies the cofactorless verification equation for ANY message with S=0, enabling universal signature forgery.

  • Package/component affected: Node.js built-in WebCrypto API (globalThis.crypto.subtle.verify for Ed25519 and Ed448), implemented in src/crypto/crypto_sig.cc
  • Affected versions: Node.js builds prior to commit 29890721cda51eeb64f4079143d55b69333599c2 (dated 2026-06-20). The vulnerability existed in the v27.0.0-pre development branch and any release built from source before the fix was merged.
  • Risk level: High. Any application using subtle.verify for Ed25519/Ed448 signature verification could accept forged signatures, enabling authentication bypass, authorization bypass, and integrity violation. An attacker who controls the public key (e.g., by submitting it during key exchange or registration) can produce valid-looking signatures for arbitrary messages without the private key.
  • Consequences: Signature verification bypass — the core security property of EdDSA is defeated. Any system relying on WebCrypto Ed25519/Ed448 verification for authentication, authorization, or data integrity is vulnerable.

Impact Parity

  • Disclosed/claimed maximum impact: Signature verification bypass (authz_bypass) — subtle.verify returns true for mathematically invalid signatures using small-order points.
  • Reproduced impact from this run: Full verification bypass confirmed. The vulnerable Node.js build returns true for:
    • The ticket's specific order-8 point forgery (Test A)
    • The identity point (order-1) forgery for two different arbitrary messages (Tests B1, B2)
    • The fixed build returns false for all forged signatures, and both builds correctly reject a garbage signature (negative control, Test C).
  • Parity: full
  • Not demonstrated: No code execution, privilege escalation, or memory corruption — this is a pure logic/verification bypass vulnerability, consistent with the claimed impact.

Root Cause

Technical Explanation

Ed25519 signature verification checks the equation S*B = R + k*A where:

  • S is the scalar from the signature (second 32 bytes)
  • B is the base point (group generator)
  • R is the point from the signature (first 32 bytes)
  • A is the public key point
  • k = SHA-512(R || A || M) mod l (where l is the prime group order ~2^252)

The Ed25519 group has cofactor 8, meaning the full group has order 8*l, with a small-order subgroup of order 8 (containing the identity, one order-2 point, two order-4 points, and four order-8 points). When the verification equation is checked without explicitly rejecting small-order points (cofactorless verification), certain special cases arise:

  1. Identity point (order-1): When A = identity and R = identity with S = 0:

    • SB = 0B = O (identity)
    • k*A = k * identity = identity = O (identity has order 1, so any scalar multiple is identity)
    • R + k*A = identity + identity = identity = O
    • Therefore SB = O = R + kA → verification passes for ANY message
  2. Order-8 point: When A and R are order-8 points with S = 0, and the message is chosen so that k*A = -R:

    • SB = O, R + kA = R + (-R) = O
    • Therefore SB = O = R + kA → verification passes

Before the fix, Node.js's crypto_sig.cc passed the public key and signature directly to OpenSSL's EVP_DigestVerifyFinal() for EdDSA verification. Depending on the OpenSSL variant, this cofactorless verification could accept these small-order point signatures. The Node.js code at line 757 (pre-fix) was simply:

if (context.verify(params.data, params.signature)) {
    static_cast<char*>(buf.get())[0] = 1;
}

No check was performed on whether the public key or R component was a small-order point.

Fix

The fix (commit 29890721cda51eeb64f4079143d55b69333599c2, PR #64026) adds a HasSmallOrderEdDsaPoint() function that checks both the signature's R component and the public key against hardcoded tables of all known small-order points for Ed25519 (14 points: 8 canonical + 6 non-canonical encodings) and Ed448 (4 points). The verification result is ANDed with this check:

if (context.verify(params.data, params.signature) &&
    !HasSmallOrderEdDsaPoint(key, params.signature)) {
    static_cast<char*>(buf.get())[0] = 1;
}

This ensures verification returns false whenever a small-order point is detected, regardless of OpenSSL's behavior.

Reproduction Steps

  1. Script: bundle/repro/reproduction_steps.sh
  2. What the script does:
    • Locates the project cache (prepared with Node.js source and build infrastructure)
    • Uses two pre-built Node.js binaries from the same base commit (387332fbf3), differing ONLY in whether src/crypto/crypto_sig.cc contains the small-order point check from fix 29890721:
      • node-vuln-true: crypto_sig.cc reverted to fix parent (68f14c2ee6) — VULNERABLE
      • node-fixed-true: crypto_sig.cc with fix applied — FIXED
    • Runs a JavaScript test (ed25519_forgery_test.js) with each binary that:
      • Test A: Imports a small-order (order-8) Ed25519 public key and verifies a crafted signature with S=0 and small-order R, using the ticket's specific data
      • Test B: Imports the identity point (order-1) as both public key and R, with S=0, and verifies two different arbitrary messages
      • Test C: Negative control — verifies a garbage signature with a random public key (must fail on both builds)
    • Compares results: vulnerable build must accept forged signatures (true), fixed build must reject them (false), negative control must fail on both
    • Writes runtime manifest and proof logs
  3. Expected evidence: Vulnerable build returns true for Tests A and B; fixed build returns false for all; both return false for Test C (negative control). Script exits 0.

Evidence

  • Log files:
    • bundle/logs/vuln_test_output.log — raw JSON output from vulnerable binary: {"A_ticket":true,"B_identity_msg1":true,"B_identity_msg2":true,"C_negative_control":false}
    • bundle/logs/fixed_test_output.log — raw JSON output from fixed binary: {"A_ticket":false,"B_identity_msg1":false,"B_identity_msg2":false,"C_negative_control":false}
  • Runtime manifest: bundle/repro/runtime_manifest.json
  • Test script: bundle/repro/ed25519_forgery_test.js
  • Key excerpts:
    • Vulnerable build: A_ticket: true (order-8 point forgery accepted), B_identity_msg1: true (identity point forgery accepted for message 1), B_identity_msg2: true (identity point forgery accepted for message 2)
    • Fixed build: All forged signatures rejected (false)
    • Negative control: Both builds correctly reject garbage signature (C_negative_control: false)
  • Environment: Node.js v27.0.0-pre, built from source with bundled OpenSSL, x86_64 Linux, GCC. Both binaries built from the same base commit with identical compiler/toolchain, differing only in crypto_sig.cc (the fix).

Recommendations / Next Steps

  • Upgrade guidance: Update to a Node.js version that includes commit 29890721cda51eeb64f4079143d55b69333599c2. Any application using WebCrypto Ed25519/Ed448 verification should ensure they are running a patched build.
  • Suggested fix approach: The fix already implemented (explicit small-order point rejection in crypto_sig.cc) is the correct approach. It provides defense-in-depth by checking at the Node.js layer regardless of OpenSSL behavior.
  • Testing recommendations: Add regression tests that verify all known small-order points for both Ed25519 and Ed448 are rejected during subtle.verify. The fix commit already adds these tests in test/parallel/test-webcrypto-sign-verify-eddsa.js.
  • Additional hardening: Consider also rejecting small-order points during key import (subtle.importKey), though the fix intentionally keeps import behavior unchanged.

Additional Notes

  • Idempotency: The reproduction script was run twice consecutively, both runs producing identical results and exiting with code 0.
  • A/B comparison: Both binaries were built from the same base commit (387332fbf3) with identical compiler, toolchain, and build flags. The only difference is whether src/crypto/crypto_sig.cc contains the HasSmallOrderEdDsaPoint() function and its call-site check. This provides a clean negative control.
  • Mathematical note on order-2/order-4 points: Tests with order-2 and order-4 points using S=0 do NOT automatically pass verification because R + k*A ≠ O unless k is specifically crafted to make k*A = -R. The identity point (order-1) is the strongest forgery primitive because k*identity = O for any k, making the forgery work for ANY message without any precomputation. The order-8 point from the ticket works because the specific data was chosen so that k*A = -R.
  • Scope: This vulnerability affects the library_api surface (WebCrypto subtle.verify). The reproduction exercises the real Node.js binary executing the real globalThis.crypto.subtle.verify function with crafted inputs, not a mock or simulation.

REPRO-2026-00225 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:30
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · url-2026-06-23-vulnerability-spoiler-alert-295 · REPRO-20
0:03
0:05
web search
0:07
web search
0:09
0:10
0:11
web search
0:20
0:20
extract_facts
no facts extracted
0:25
0:25
0:25
supportrepro
1:11
1:13
1:13
1:13
1:14
1:14
1:14
1:14
1:17
1:17
1:22
1:22
$cd /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/repo && echo "=== repo HEAD ===" && git rev-parse HEAD && echo "=== worktrees ===" && git worktree list && echo "=== wt-vuln HEAD ===" && git -C /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/wt-vuln rev-parse HEAD && echo "=== wt-fixed HEAD ===" && git -C /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/wt-fixed rev-parse HEAD && echo "=== fixed commit parent ===" && git rev-parse 29890721cda51eeb64f4079143d55b69333599c2^ 2>&1 | head -5 && echo "=== fixed commit ===" && git rev-parse 29890721cda51eeb64f4079143d55b69333599c2 2>&1 | head -5
=== repo HEAD ===
387332fbf33ae9e54d8f774aa3d94fb5ca0f722c
=== worktrees ===
/data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/repo          387332fbf3 (detached HEAD)
/data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/repo/wt-repro 87d63869f3 (detached HEAD)
/data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/wt-6292a06e   387332fbf3 (detached HEAD)
/data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/wt-fixed      592219798f (detached HEAD)
/data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/wt-vuln       87a6aa674a (detached HEAD)
1:22
$ls -la /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/node-vuln /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/node-fixed && file /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/node-vuln
-rwxr-xr-x 1 vscode vscode 144484240 Jul  4 18:49 /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/node-fixed
-rwxr-xr-x 1 vscode vscode 144484240 Jul  4 18:50 /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/node-vuln
1:27
1:27
$/data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/node-vuln --version 2>&1 | head -3; echo "---fixed---"; /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/node-fixed --version 2>&1 | head -3
v27.0.0-pre
---fixed---
v27.0.0-pre
1:27
$cd /data/pruva/project-cache/6f7cfbc6-90dc-4b8e-bd75-c0200a6d1bf9/repo && echo "=== fixed commit msg ===" && git log -1 --format='%H %s%n%b' 29890721cda51eeb64f4079143d55b69333599c2 2>&1 | head -20 && echo "=== is wt-vuln ancestor of fix? ===" && git merge-base --is-ancestor 87a6aa674a 29890721cda51eeb64f4079143d55b69333599c2 && echo "YES vuln is ancestor of fix" || echo "NO vuln is NOT ancestor of fix"; echo "=== is wt-fixed the fix or descendant? ===" && git merge-base --is-ancestor 29890721cda51eeb64f4079143d55b69333599c2 592219798f && echo "YES fix is ancestor of wt-fixed" || echo "NO fix is NOT ancestor of wt-fixed"
=== fixed commit msg ===
29890721cda51eeb64f4079143d55b69333599c2 crypto: reject small-order EdDSA points during verify
Return false for Ed25519 and Ed448 one-shot verification when the public
key or signature R component is a known low-order point. This keeps key
import behavior unchanged while making WebCrypto verification match WPT
expectations across OpenSSL variants.

Remove the stale WPT expected-failure entry and add focused regression
1:30
08 · How to Fix

How to Fix REPRO-2026-00225

Coming soon

Step-by-step mitigation and hardening guidance for REPRO-2026-00225 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: REPRO-2026-00225

What does REPRO-2026-00225 (the Node.js WebCrypto EdDSA issue) cover?

This reproduction covers a high-severity signature-verification bypass in Node.js's built-in WebCrypto EdDSA (Ed25519/Ed448) implementation, where crypto.subtle.verify() accepted forged signatures using small-order (low-order) points before the fix. Pruva reproduced it (reproduction REPRO-2026-00225).

How does the small-order point signature forgery work?

An attacker supplies a small-order (e.g. identity element) point as the public key or as the R component of a signature. Because these points satisfy the verification equation independent of the message, subtle.verify() returns true for a forged signature -- most severely, using the identity point lets any message pass with S=0, without ever knowing a real private key.

How severe is this issue?

High severity -- any application relying on crypto.subtle.verify() for Ed25519/Ed448 signatures across Node.js's supported release lines (22.x, 24.x, 26.x) could accept forged signatures, breaking authentication and integrity guarantees.

How can I reproduce this Node.js WebCrypto issue?

Download the verified script from this page and run it in an isolated environment against a Node.js build prior to commit 29890721cda51eeb64f4079143d55b69333599c2. It constructs a small-order (identity) public key and a matching forged signature, calls crypto.subtle.verify(), and shows it incorrectly returning true; against a patched build the same call returns false.
11 · References

References for REPRO-2026-00225

Authoritative sources for REPRO-2026-00225 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.