CVE-2026-23967 (GHSA-qv7w-v773-3xqm) states its impact as "An attacker can derive a new valid signature for a previously signed message from an existing signature." That is still true on 0.5.4. The fix that shipped added a range check on the decoded integers, but the encoding is what needs checking.
decodeDer in src/sm2/asn1.js reads R and S by their own length bytes and nothing else:
const vIndexR = getStartOfV(input, start)
const lR = getL(input, start)
const vR = input.substr(vIndexR, lR * 2)
...
const r = new BigInteger(vR, 16)
There is no minimal-encoding requirement, no check on the 02 tags, and the outer SEQUENCE length is never compared against the actual input length. doVerifySignature in src/sm2/index.js then range-checks that r and s lie in [1, n-1], but a 00-padded R decodes to the identical in-range integer, so that check passes.
Prepending 00 bytes to the R INTEGER and fixing up the two length bytes therefore yields distinct signature strings that all verify against the same message and public key. Against the published npm package 0.5.4 on Node v26.5.0:
orig 3046022100ace9e76a5279c85c56f3514d929ecab6... true
pad=1 distinct=true verifies=true
pad=2 distinct=true verifies=true
pad=3 distinct=true verifies=true
pad=8 distinct=true verifies=true
Because the SEQUENCE length is not validated against the input, the padding is effectively unbounded, so a single signature has many valid encodings. The fixed-width r||s path (der: false) is not affected: prepending changes r, and appending pushes s out of range.
Impact is limited to anything a caller builds on signature-byte identity, such as replay protection keyed on a signature, deduplication, or idempotency tokens. It does not let an attacker sign a new message, which is why I have scored this lower than the original advisory.
A fix would be to require canonical DER on the verify path: reject non-minimal INTEGERs (a leading 00 is only legal when the next byte has its high bit set), check the 02 tags, and require the outer SEQUENCE length to equal the remaining input.
I used AI assistance while reading the code and preparing this report. The reproduction above I ran myself against the published npm package.
CVE-2026-23967 (GHSA-qv7w-v773-3xqm) states its impact as "An attacker can derive a new valid signature for a previously signed message from an existing signature." That is still true on 0.5.4. The fix that shipped added a range check on the decoded integers, but the encoding is what needs checking.
decodeDerinsrc/sm2/asn1.jsreads R and S by their own length bytes and nothing else:There is no minimal-encoding requirement, no check on the
02tags, and the outer SEQUENCE length is never compared against the actual input length.doVerifySignatureinsrc/sm2/index.jsthen range-checks that r and s lie in [1, n-1], but a00-padded R decodes to the identical in-range integer, so that check passes.Prepending
00bytes to the R INTEGER and fixing up the two length bytes therefore yields distinct signature strings that all verify against the same message and public key. Against the published npm package 0.5.4 on Node v26.5.0:Because the SEQUENCE length is not validated against the input, the padding is effectively unbounded, so a single signature has many valid encodings. The fixed-width
r||spath (der: false) is not affected: prepending changes r, and appending pushes s out of range.Impact is limited to anything a caller builds on signature-byte identity, such as replay protection keyed on a signature, deduplication, or idempotency tokens. It does not let an attacker sign a new message, which is why I have scored this lower than the original advisory.
A fix would be to require canonical DER on the verify path: reject non-minimal INTEGERs (a leading
00is only legal when the next byte has its high bit set), check the02tags, and require the outer SEQUENCE length to equal the remaining input.I used AI assistance while reading the code and preparing this report. The reproduction above I ran myself against the published npm package.