Skip to content

Commit 9820020

Browse files
royzwambagRoy Zwambag
andauthored
Backport to 2.x: Reject nil and empty HMAC keys (CVE-2026-45363) (#725)
Backport Reject nil and empty HMAC keys (CVE-2026-45363) Backport of upstream commit db560b7 to the 2.10 branch. Co-authored-by: Roy Zwambag <rzwambag@gitlab.com>
1 parent 658275c commit 9820020

5 files changed

Lines changed: 69 additions & 87 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
**Fixes and enhancements:**
4+
5+
- Backport: Reject `nil` and empty HMAC keys when signing and verifying ([CVE-2026-45363](https://www.cve.org/CVERecord?id=CVE-2026-45363) / [GHSA-c32j-vqhx-rx3x](https://github.com/jwt/ruby-jwt/security/advisories/GHSA-c32j-vqhx-rx3x)) [#725](https://github.com/jwt/ruby-jwt/pull/725) ([@royzwambag](https://github.com/royzwambag))
6+
37
## [v2.10.2](https://github.com/jwt/ruby-jwt/tree/v2.10.2) (2025-06-29)
48

59
[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.10.1...v2.10.2)

lib/jwt/jwa/hmac.rb

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,15 @@ def initialize(alg, digest)
1616
end
1717

1818
def sign(data:, signing_key:)
19-
signing_key ||= ''
20-
raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String)
19+
ensure_valid_key!(signing_key)
2120

2221
OpenSSL::HMAC.digest(digest.new, signing_key, data)
23-
rescue OpenSSL::HMACError => e
24-
raise_verify_error!('OpenSSL 3.0 does not support nil or empty hmac_secret') if signing_key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
25-
26-
raise e
2722
end
2823

2924
def verify(data:, signature:, verification_key:)
30-
SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key))
25+
ensure_valid_key!(verification_key)
26+
27+
SecurityUtils.secure_compare(signature, OpenSSL::HMAC.digest(digest.new, verification_key, data))
3128
end
3229

3330
register_algorithm(new('HS256', OpenSSL::Digest::SHA256))
@@ -38,6 +35,11 @@ def verify(data:, signature:, verification_key:)
3835

3936
attr_reader :digest
4037

38+
def ensure_valid_key!(key)
39+
raise_verify_error!('HMAC key expected to be a String') unless key.is_a?(String)
40+
raise_verify_error!('HMAC key cannot be empty') if key.empty?
41+
end
42+
4143
# Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
4244
# rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
4345
module SecurityUtils

spec/integration/readme_examples_spec.rb

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,6 @@
2828
]
2929
end
3030

31-
it 'decodes with HMAC algorithm without secret key' do
32-
pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression?
33-
token = JWT.encode payload, nil, 'HS256'
34-
decoded_token = JWT.decode token, nil, false
35-
36-
expect(token).to eq 'eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pVzcY2dX8JNM3LzIYeP2B1e1Wcpt1K3TWVvIYSF4x-o'
37-
expect(decoded_token).to eq [
38-
{ 'data' => 'test' },
39-
{ 'alg' => 'HS256' }
40-
]
41-
end
42-
4331
it 'RSA' do
4432
rsa_private = OpenSSL::PKey::RSA.generate 2048
4533
rsa_public = rsa_private.public_key

spec/jwt/jwa/hmac_spec.rb

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -12,68 +12,39 @@
1212
it { is_expected.to eq(valid_signature) }
1313
end
1414

15-
# Address OpenSSL 3.0 errors with empty hmac_secret - https://github.com/jwt/ruby-jwt/issues/526
15+
# GHSA-c32j-vqhx-rx3x: empty/nil keys must be rejected before reaching OpenSSL,
16+
# so a forged token signed with "" cannot verify.
1617
context 'when nil hmac_secret is passed' do
1718
let(:hmac_secret) { nil }
18-
context 'when OpenSSL 3.0 raises a malloc failure' do
19-
before do
20-
allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure'))
21-
end
2219

23-
it 'raises JWT::DecodeError' do
24-
expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret')
25-
end
20+
it 'raises JWT::DecodeError' do
21+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
2622
end
2723

28-
context 'when OpenSSL raises any other error' do
29-
before do
30-
allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error'))
31-
end
32-
33-
it 'raises the original error' do
34-
expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error')
35-
end
36-
end
37-
38-
context 'when other versions of openssl do not raise an exception' do
39-
let(:response) { Base64.decode64("Q7DO+ZJl+eNMEOqdNQGSbSezn1fG1nRWHYuiNueoGfs=\n") }
40-
before do
41-
allow(OpenSSL::HMAC).to receive(:digest).and_return(response)
42-
end
43-
44-
it { is_expected.to eql(response) }
24+
it 'does not call OpenSSL::HMAC.digest' do
25+
expect(OpenSSL::HMAC).not_to receive(:digest)
26+
expect { subject }.to raise_error(JWT::DecodeError)
4527
end
4628
end
4729

4830
context 'when blank hmac_secret is passed' do
4931
let(:hmac_secret) { '' }
50-
context 'when OpenSSL 3.0 raises a malloc failure' do
51-
before do
52-
allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure'))
53-
end
5432

55-
it 'raises JWT::DecodeError' do
56-
expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret')
57-
end
33+
it 'raises JWT::DecodeError' do
34+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key cannot be empty')
5835
end
5936

60-
context 'when OpenSSL raises any other error' do
61-
before do
62-
allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error'))
63-
end
64-
65-
it 'raises the original error' do
66-
expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error')
67-
end
37+
it 'does not call OpenSSL::HMAC.digest' do
38+
expect(OpenSSL::HMAC).not_to receive(:digest)
39+
expect { subject }.to raise_error(JWT::DecodeError)
6840
end
41+
end
6942

70-
context 'when other versions of openssl do not raise an exception' do
71-
let(:response) { Base64.decode64("Q7DO+ZJl+eNMEOqdNQGSbSezn1fG1nRWHYuiNueoGfs=\n") }
72-
before do
73-
allow(OpenSSL::HMAC).to receive(:digest).and_return(response)
74-
end
43+
context 'when non-String hmac_secret is passed' do
44+
let(:hmac_secret) { 123 }
7545

76-
it { is_expected.to eql(response) }
46+
it 'raises JWT::DecodeError' do
47+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
7748
end
7849
end
7950

@@ -124,6 +95,45 @@
12495

12596
it { is_expected.to be(false) }
12697
end
98+
99+
# GHSA-c32j-vqhx-rx3x: empty/nil keys must be rejected before reaching OpenSSL,
100+
# so a forged token signed with "" cannot verify.
101+
context 'when verification_key is nil' do
102+
let(:signature) { valid_signature }
103+
let(:hmac_secret) { nil }
104+
105+
it 'raises JWT::DecodeError' do
106+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
107+
end
108+
109+
it 'does not call OpenSSL::HMAC.digest' do
110+
expect(OpenSSL::HMAC).not_to receive(:digest)
111+
expect { subject }.to raise_error(JWT::DecodeError)
112+
end
113+
end
114+
115+
context 'when verification_key is an empty string' do
116+
let(:signature) { valid_signature }
117+
let(:hmac_secret) { '' }
118+
119+
it 'raises JWT::DecodeError' do
120+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key cannot be empty')
121+
end
122+
123+
it 'does not call OpenSSL::HMAC.digest' do
124+
expect(OpenSSL::HMAC).not_to receive(:digest)
125+
expect { subject }.to raise_error(JWT::DecodeError)
126+
end
127+
end
128+
129+
context 'when verification_key is not a String' do
130+
let(:signature) { valid_signature }
131+
let(:hmac_secret) { 123 }
132+
133+
it 'raises JWT::DecodeError' do
134+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
135+
end
136+
end
127137
end
128138

129139
context 'backwards compatibility' do

spec/jwt/jwt_spec.rb

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -650,20 +650,6 @@
650650
end
651651
end
652652

653-
context 'when hmac algorithm is used without secret key' do
654-
it 'encodes payload' do
655-
pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression?
656-
payload = { a: 1, b: 'b' }
657-
658-
token = JWT.encode(payload, '', 'HS256')
659-
660-
expect do
661-
token_without_secret = JWT.encode(payload, nil, 'HS256')
662-
expect(token).to eq(token_without_secret)
663-
end.not_to raise_error
664-
end
665-
end
666-
667653
context 'algorithm case insensitivity' do
668654
let(:payload) { { 'a' => 1, 'b' => 'b' } }
669655

@@ -780,14 +766,6 @@
780766
end
781767
end
782768

783-
describe 'when token signed with nil and decoded with nil' do
784-
let(:no_key_token) { JWT.encode(payload, nil, 'HS512') }
785-
it 'raises JWT::DecodeError' do
786-
pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression?
787-
expect { JWT.decode(no_key_token, nil, true, algorithms: 'HS512') }.to raise_error(JWT::DecodeError, 'No verification key available')
788-
end
789-
end
790-
791769
context 'when token ends with a newline char' do
792770
let(:token) { "#{JWT.encode(payload, 'secret', 'HS256')}\n" }
793771
it 'ignores the newline and decodes the token' do

0 commit comments

Comments
 (0)