Skip to content

fix: GHSA-mvf2-f6gm-w987 JWT Algorithm Confusion via Whitespace-Prefix#586

Closed
antoatta85 wants to merge 3 commits intomasterfrom
fix/rsa-key-trim-whitespace
Closed

fix: GHSA-mvf2-f6gm-w987 JWT Algorithm Confusion via Whitespace-Prefix#586
antoatta85 wants to merge 3 commits intomasterfrom
fix/rsa-key-trim-whitespace

Conversation

@antoatta85
Copy link
Copy Markdown
Collaborator

@antoatta85 antoatta85 commented Apr 1, 2026

fix: trim whitespace from PEM keys before pattern matching (GHSA-mvf2-f6gm-w987)

The fix for CVE-2023-48223 introduced a ^-anchored regex (publicKeyPemMatcher) to
detect RSA public keys. Any leading whitespace in the key string — common when keys are
loaded from databases, YAML configs, or environment variables — caused the anchor to
fail, misclassifying the RSA public key as an HMAC secret and re-enabling the original
algorithm confusion attack.

The private key path already called .trim() before matching; the public key path did not.

Fix

key.trim() is now applied before all regex and string matches in both
performDetectPublicKeyAlgorithms and performDetectPrivateKeyAlgorithm (src/crypto.js).

Tests added

Regression tests in test/crypto.spec.js covering keys with leading \n, spaces, and tabs for:

  • RSA public key → correctly classified as RSA (not HMAC)
  • EC and Ed25519 public keys → correctly classified
  • Private key with leading whitespace → still rejected by the verifier
  • Public key / X.509 cert with leading whitespace → still rejected by the signer

FIXES #589

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an incomplete mitigation for GHSA-mvf2-f6gm-w987 / CVE-2023-48223 by ensuring leading whitespace on PEM inputs does not bypass ^-anchored key detection, which could otherwise misclassify an RSA/EC/Ed public key as an HMAC secret and re-enable JWT algorithm confusion.

Changes:

  • Trim key strings before regex / substring checks in performDetectPublicKeyAlgorithms and performDetectPrivateKeyAlgorithm.
  • Add regression tests covering leading whitespace (\n, spaces, tabs) for RSA/EC/Ed public key detection and signer/verifier rejection guards.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/crypto.js Applies trim() prior to PEM/cert detection logic to prevent whitespace-prefixed key misclassification.
test/crypto.spec.js Adds regression tests validating correct algorithm detection and guard behavior with whitespace-prefixed keys/certs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@FlowmasterHaggla
Copy link
Copy Markdown

FlowmasterHaggla commented Apr 7, 2026

@antoatta85 , we really need this fix, because pipelines are blocked. Can you push the requested changes ?

I could provide a change, but don't have the permissions 😬

@SociableSteve
Copy link
Copy Markdown
Contributor

@antoatta85 , we really need this fix, because pipelines are blocked. Can you push the requested changes ?

I could provide a change, but don't have the permissions 😬

I will be reviewing this PR during this morning, along with any others that are open on this repository. If you have any changes you think should occur with this PR please feel free to comment and we'll take them into account.

@FlowmasterHaggla
Copy link
Copy Markdown

FlowmasterHaggla commented Apr 7, 2026

1) src/crypto.js

Use trimmedKey consistently for private PEM decoding (EC + PKCS8 paths), not only for regex checks.

--- a/src/crypto.js
+++ b/src/crypto.js
@@ -91,14 +91,14 @@ function performDetectPrivateKeyAlgorithm(key) {
   switch (pemData[1]) {
     case 'RSA': // pkcs1 format - Can only be RSA key
       return 'RS256'
     case 'EC': // sec1 format - Can only be a EC key
-      keyData = ECPrivateKey.decode(key, 'pem', { label: 'EC PRIVATE KEY' })
+      keyData = ECPrivateKey.decode(trimmedKey, 'pem', { label: 'EC PRIVATE KEY' })
       curveId = keyData.parameters.value.join('.')
       break
     case 'ENCRYPTED': // Can be either RSA or EC key - we'll used the supplied algorithm
       return 'ENCRYPTED'
     default:
       // pkcs8
-      keyData = PrivateKey.decode(key, 'pem', { label: 'PRIVATE KEY' })
+      keyData = PrivateKey.decode(trimmedKey, 'pem', { label: 'PRIVATE KEY' })
       oid = keyData.algorithm.algorithm.join('.')

2) test/crypto.spec.js

Make “leading whitespace” tests actually cover multiple whitespace prefixes (\n, spaces, tabs) in all relevant cases.

--- a/test/crypto.spec.js
+++ b/test/crypto.spec.js
@@ -75,6 +75,8 @@ const invalidPublicCurve = `-----BEGIN PUBLIC KEY-----
 T7r5sAUIWaF0Q5uk5NYmLOnCFxoP8Ua16sraCbAozdvg0wfvT7Cq
 -----END PUBLIC KEY-----
 `
+
+const leadingWhitespacePrefixes = ['\n', '    ', ' \n', '\t\t']
@@ -234,24 +236,29 @@ test('detectPublicKeyAlgorithms - unrecognized EC curves should be rejected', t
 // GHSA-mvf2-f6gm-w987: leading whitespace must not defeat the ^ anchor and cause
 // an RSA/EC/Ed public key to be misclassified as an HMAC secret.
 test('detectPublicKeyAlgorithms - RSA public key with leading whitespace must be detected as RSA (not HMAC)', t => {
-  const keyWithLeadingNewline = '\n' + publicKeys.RS.toString('utf-8')
-  t.assert.deepStrictEqual(detectPublicKeyAlgorithms(keyWithLeadingNewline), rsaAlgorithms)
-})
-
-test('detectPublicKeyAlgorithms - RSA public key with leading spaces/tabs must be detected as RSA (not HMAC)', t => {
-  const keyWithLeadingSpaces = '  \t  ' + publicKeys.RS.toString('utf-8')
-  t.assert.deepStrictEqual(detectPublicKeyAlgorithms(keyWithLeadingSpaces), rsaAlgorithms)
+  for (const prefix of leadingWhitespacePrefixes) {
+    t.assert.deepStrictEqual(detectPublicKeyAlgorithms(prefix + publicKeys.RS.toString('utf-8')), rsaAlgorithms)
+  }
 })

 test('detectPublicKeyAlgorithms - EC public key with leading whitespace must be detected as EC (not HMAC)', t => {
-  const keyWithLeadingNewline = '\n' + publicKeys.ES256.toString('utf-8')
-  t.assert.deepStrictEqual(detectPublicKeyAlgorithms(keyWithLeadingNewline), ['ES256'])
+  for (const prefix of leadingWhitespacePrefixes) {
+    t.assert.deepStrictEqual(detectPublicKeyAlgorithms(prefix + publicKeys.ES256.toString('utf-8')), ['ES256'])
+  }
 })

 test('detectPublicKeyAlgorithms - Ed25519 public key with leading whitespace must be detected as EdDSA (not HMAC)', t => {
-  const keyWithLeadingNewline = '\n' + publicKeys.Ed25519.toString('utf-8')
-  t.assert.deepStrictEqual(detectPublicKeyAlgorithms(keyWithLeadingNewline), ['EdDSA'])
+  for (const prefix of leadingWhitespacePrefixes) {
+    t.assert.deepStrictEqual(detectPublicKeyAlgorithms(prefix + publicKeys.Ed25519.toString('utf-8')), ['EdDSA'])
+  }
 })

 test('detectPublicKeyAlgorithms - private key with leading whitespace must still be rejected', t => {
-  const keyWithLeadingNewline = '\n' + privateKeys.RS.toString('utf-8')
-  t.assert.throws(() => detectPublicKeyAlgorithms(keyWithLeadingNewline), {
-    message: 'Private keys are not supported for verifying.'
-  })
+  for (const prefix of leadingWhitespacePrefixes) {
+    t.assert.throws(() => detectPublicKeyAlgorithms(prefix + privateKeys.RS.toString('utf-8')), {
+      message: 'Private keys are not supported for verifying.'
+    })
+  }
 })
@@ -263,20 +270,24 @@ test('detectPublicKeyAlgorithms - private key with leading whitespace must still
 // GHSA-mvf2-f6gm-w987 (signer side): public key with leading whitespace must not
 // bypass the "Public keys are not supported for signing" guard.
 test('detectPrivateKeyAlgorithm - public key with leading whitespace must still be rejected', t => {
-  const keyWithLeadingNewline = '\n' + publicKeys.RS.toString('utf-8')
-  t.assert.throws(() => detectPrivateKeyAlgorithm(keyWithLeadingNewline), {
-    message: 'Public keys are not supported for signing.'
-  })
+  for (const prefix of leadingWhitespacePrefixes) {
+    t.assert.throws(() => detectPrivateKeyAlgorithm(prefix + publicKeys.RS.toString('utf-8')), {
+      message: 'Public keys are not supported for signing.'
+    })
+  }
 })

 test('detectPrivateKeyAlgorithm - X.509 cert with leading whitespace must still be rejected', t => {
   const cert = readFileSync(resolve(__dirname, '../benchmarks/keys/rs-x509-public.key'))
-  const keyWithLeadingNewline = '\n' + cert.toString('utf-8')
-  t.assert.throws(() => detectPrivateKeyAlgorithm(keyWithLeadingNewline), {
-    message: 'Public keys are not supported for signing.'
-  })
+  for (const prefix of leadingWhitespacePrefixes) {
+    t.assert.throws(() => detectPrivateKeyAlgorithm(prefix + cert.toString('utf-8')), {
+      message: 'Public keys are not supported for signing.'
+    })
+  }
 })

 test('detectPrivateKeyAlgorithm - EC private key with leading whitespace must still be detected', t => {
-  const keyWithLeadingNewline = '\n' + privateKeys.ES256.toString('utf-8')
-  t.assert.equal(detectPrivateKeyAlgorithm(keyWithLeadingNewline), 'ES256')
+  for (const prefix of leadingWhitespacePrefixes) {
+    t.assert.equal(detectPrivateKeyAlgorithm(prefix + privateKeys.ES256.toString('utf-8')), 'ES256')
+  }
 })

maybe we could also test '\n ',

@dcs-soni
Copy link
Copy Markdown
Contributor

dcs-soni commented Apr 7, 2026

CI pipeline of one my projects is blocked by this. Hoping to get it resolved soon. Thank you!

@SociableSteve
Copy link
Copy Markdown
Contributor

@antoatta85 Please can you review/incorporate the feedback from @FlowmasterHaggla above?

@dcs-soni
Copy link
Copy Markdown
Contributor

dcs-soni commented Apr 7, 2026

Hey @SociableSteve and @antoatta85, thanks for moving on this!

Since this CVE is currently blocking CI security audits for downstream projects, I went ahead and incorporated @FlowmasterHaggla's feedback (including the \n edge case) into a superseding PR here: #598

I'm more than happy to close mine if @antoatta85 gets a chance to update this branch, if more changes are required, I just wanted to provide a fix that can be merged to unblock the release.

@antoatta85
Copy link
Copy Markdown
Collaborator Author

@antoatta85 Please can you review/incorporate the feedback from @FlowmasterHaggla above?

@SociableSteve done!

SociableSteve pushed a commit that referenced this pull request Apr 7, 2026
* fix: GHSA-mvf2-f6gm-w987 JWT Algorithm Confusion via Whitespace-Prefixed RSA Public Key

* fix: resolve JWT algorithm confusion with leading whitespace and gaps in EC/PKCS8 decodin

* test: remove redundant whitespace test spotted in code review

---------

Co-authored-by: antoatta85 <antonio.attanasio@nearform.com>
@SociableSteve
Copy link
Copy Markdown
Contributor

Resolved in #598

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Address Sec Adv: Whitespace-Prefixed RSA Key

5 participants