From 94daf26eade9bf2a51b8fd3237cd16ad58a84b94 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 27 May 2026 14:43:23 +0530 Subject: [PATCH] chore: update jwt and faraday (#14577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates two dependencies — `faraday` (2.14.1 → 2.14.2) and `jwt` (2.10.1 → 2.10.3) — to pick up security patches flagged by `bundle-audit`. Both are bumped to the minimal patched release within their existing major lines to keep the blast radius small. ### Faraday `Faraday::Connection#build_exclusive_url` still allowed a protocol-relative host override when the request target was passed as a `URI` object (rather than a `String`), bypassing the earlier fix for the string-based variant (CVE-2026-25765 / GHSA-33mh-2634-fwr2). On a fixed-base connection this could redirect a request to an attacker-controlled host while still forwarding connection-scoped headers such as `Authorization` — i.e. off-host request forgery (CVE-2026-33637 / GHSA-5rv5-xj5j-3484). The fix is a clean patch bump to `2.14.2`, within Faraday's existing version range — no API changes and no other gems affected. ### JWT `jwt` 2.10.1 accepts an empty/`nil` HMAC key during verification: `JWT.decode(token, "", true, algorithm: 'HS256')` (and keyfinder paths returning `""`/`nil`) verify a forged token, because the empty-key HMAC digest is treated as valid and `enforce_hmac_key_length` defaults to `false` (CVE-2026-45363, High). The advisory offers two fixes — `~> 2.10.3` or `>= 3.2.0`. We chose **2.10.3** deliberately: jumping to 3.x cascaded into upgrading `oauth2`, `twilio-ruby`, `googleauth`, `web-push`, and `signet` (all pinned `jwt < 3.0`), and `jwt` is used directly in 8+ places here (token services, OAuth callbacks, integration helpers), so a major bump carries real breakage risk for no extra security benefit. The Gemfile is pinned `'~> 2.10', '>= 2.10.3'` to hold the 2.x line. **Spec changes.** 2.10.3 tightens key handling: HMAC sign/verify now raises on a `nil`, empty, or non-`String` key instead of silently coercing it. A few specs relied on the old lax behaviour and needed updating: - `microsoft` / `google` callback specs built unsigned ID tokens via `JWT.encode(payload, false)`. Replaced with the correct unsigned form, `JWT.encode(payload, nil, 'none')`. - `instagram` / `linear` / `shopify` helper specs have a "client secret not configured" context where `client_secret` is `nil`. Their shared `valid_token` `let` signed with that `nil` secret, which Ruby evaluates before the helper runs — now raising. Since the helper short-circuits on the blank secret and never decodes the token, those contexts now override `valid_token` with a throwaway string. **Production is unaffected.** Every production HMAC path uses a real, non-empty key — `Rails.application.secret_key_base` (`BaseTokenService`, `Widget::TokenService`) or a client secret guarded by `return if client_secret.blank?` (Instagram/TikTok/Shopify/Linear helpers). The one `nil`-key call, `JWT.decode(id_token, nil, false)` in `OauthCallbackController`, runs with verification disabled, so the key is never inspected. Twilio voice tokens use `Twilio::JWT::AccessToken` from `twilio-ruby`, not this gem. The specs failed precisely because they exercised the unsafe empty-key pattern the patch now blocks — production never did. --- Gemfile | 2 +- Gemfile.lock | 6 +++--- spec/controllers/google/callbacks_controller_spec.rb | 4 ++-- spec/controllers/microsoft/callbacks_controller_spec.rb | 4 ++-- spec/helpers/instagram/integration_helper_spec.rb | 1 + spec/helpers/linear/integration_helper_spec.rb | 1 + spec/helpers/shopify/integration_helper_spec.rb | 1 + 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index e10984f536b..680a0738b99 100644 --- a/Gemfile +++ b/Gemfile @@ -89,7 +89,7 @@ gem 'rails-i18n', '~> 7.0' # two-factor authentication gem 'devise-two-factor', '>= 5.0.0' # authorization -gem 'jwt' +gem 'jwt', '~> 2.10', '>= 2.10.3' gem 'pundit' # super admin diff --git a/Gemfile.lock b/Gemfile.lock index 4da0e584759..0479393b0ba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -301,7 +301,7 @@ GEM railties (>= 5.0.0) faker (3.2.0) i18n (>= 1.8.11, < 2) - faraday (2.14.1) + faraday (2.14.2) faraday-net_http (>= 2.0, < 3.5) json logger @@ -491,7 +491,7 @@ GEM judoscale-sidekiq (1.8.2) judoscale-ruby (= 1.8.2) sidekiq (>= 5.0) - jwt (2.10.1) + jwt (2.10.3) base64 kaminari (1.2.2) activesupport (>= 4.1.0) @@ -1102,7 +1102,7 @@ DEPENDENCIES json_schemer judoscale-rails judoscale-sidekiq - jwt + jwt (~> 2.10, >= 2.10.3) kaminari koala letter_opener diff --git a/spec/controllers/google/callbacks_controller_spec.rb b/spec/controllers/google/callbacks_controller_spec.rb index a898ab3950e..b38fcfb593e 100644 --- a/spec/controllers/google/callbacks_controller_spec.rb +++ b/spec/controllers/google/callbacks_controller_spec.rb @@ -8,12 +8,12 @@ RSpec.describe 'Google::CallbacksController', type: :request do describe 'GET /google/callback' do let(:response_body_success) do - { id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer', + { id_token: JWT.encode({ email: email, name: 'test' }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer', refresh_token: SecureRandom.hex(10) } end let(:response_body_success_without_name) do - { id_token: JWT.encode({ email: email }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer', + { id_token: JWT.encode({ email: email }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer', refresh_token: SecureRandom.hex(10) } end diff --git a/spec/controllers/microsoft/callbacks_controller_spec.rb b/spec/controllers/microsoft/callbacks_controller_spec.rb index 6bd9a058384..129cfa383d8 100644 --- a/spec/controllers/microsoft/callbacks_controller_spec.rb +++ b/spec/controllers/microsoft/callbacks_controller_spec.rb @@ -8,12 +8,12 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do describe 'GET /microsoft/callback' do let(:response_body_success) do - { id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer', + { id_token: JWT.encode({ email: email, name: 'test' }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer', refresh_token: SecureRandom.hex(10) } end let(:response_body_success_without_name) do - { id_token: JWT.encode({ email: email }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer', + { id_token: JWT.encode({ email: email }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer', refresh_token: SecureRandom.hex(10) } end diff --git a/spec/helpers/instagram/integration_helper_spec.rb b/spec/helpers/instagram/integration_helper_spec.rb index 7a8bb30a4ce..25ec46b58ed 100644 --- a/spec/helpers/instagram/integration_helper_spec.rb +++ b/spec/helpers/instagram/integration_helper_spec.rb @@ -82,6 +82,7 @@ RSpec.describe Instagram::IntegrationHelper do context 'when client secret is not configured' do let(:client_secret) { nil } + let(:valid_token) { 'any-token' } it 'returns nil' do expect(verify_instagram_token(valid_token)).to be_nil diff --git a/spec/helpers/linear/integration_helper_spec.rb b/spec/helpers/linear/integration_helper_spec.rb index 4f0f65c312e..958baa4bf56 100644 --- a/spec/helpers/linear/integration_helper_spec.rb +++ b/spec/helpers/linear/integration_helper_spec.rb @@ -65,6 +65,7 @@ RSpec.describe Linear::IntegrationHelper do context 'when client secret is not configured' do let(:client_secret) { nil } + let(:valid_token) { 'any-token' } it 'returns nil' do expect(verify_linear_token(valid_token)).to be_nil diff --git a/spec/helpers/shopify/integration_helper_spec.rb b/spec/helpers/shopify/integration_helper_spec.rb index 15b7120d4cb..bfad66d9ae6 100644 --- a/spec/helpers/shopify/integration_helper_spec.rb +++ b/spec/helpers/shopify/integration_helper_spec.rb @@ -65,6 +65,7 @@ RSpec.describe Shopify::IntegrationHelper do context 'when client secret is not configured' do let(:client_secret) { nil } + let(:valid_token) { 'any-token' } it 'returns nil' do expect(verify_shopify_token(valid_token)).to be_nil