CVE-2020-15269: Insufficient Session Expiration (CWE-613)
value: In Spree before versions 3.7.11, 4.0.4, or 4.1.11, expired user tokens could be used to access Storefront API v2 endpoints. The issue is patched in versions 3.7.11, 4.0.4 and 4.1.11. A workaround without upgrading is described in the linked advisory.
CWE description: According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."
CVSS v3 base score: 9.1
Published: 2020-10-20T21:15Z
Repository: https://github.com/spree/spree
Language: Ruby
Files changed: api/app/controllers/spree/api/v2/base_controller.rb, api/lib/spree/api/testing_support/v2/base.rb, api/spec/requests/spree/api/v2/errors_spec.rb
Commit date: 2020-10-14 12:13:37 +0200
Commit message: ensure doorkeeper_token is valid when authenticating requests in v2

Diff: diff --git a/api/app/controllers/spree/api/v2/base_controller.rb b/api/app/controllers/spree/api/v2/base_controller.rb
index 767b70f52..170c9a314 100644
--- a/api/app/controllers/spree/api/v2/base_controller.rb
+++ b/api/app/controllers/spree/api/v2/base_controller.rb
@@ -51,11 +51,16 @@ module Spree
             render json: { error: error }, status: status, content_type: content_type
           end
         end
 
         def spree_current_user
-          @spree_current_user ||= Spree.user_class.find_by(id: doorkeeper_token.resource_owner_id) if doorkeeper_token
+          return nil unless doorkeeper_token
+          return @spree_current_user if @spree_current_user
+
+          doorkeeper_authorize!
+
+          @spree_current_user ||= Spree.user_class.find_by(id: doorkeeper_token.resource_owner_id)
         end
 
         def spree_authorize!(action, subject, *args)
           authorize!(action, subject, *args)
         end
diff --git a/api/lib/spree/api/testing_support/v2/base.rb b/api/lib/spree/api/testing_support/v2/base.rb
index b07b8ce7f..f7417b107 100644
--- a/api/lib/spree/api/testing_support/v2/base.rb
+++ b/api/lib/spree/api/testing_support/v2/base.rb
@@ -2,11 +2,11 @@ shared_context 'API v2 tokens' do
   let(:token) { Doorkeeper::AccessToken.create!(resource_owner_id: user.id, expires_in: nil) }
   let(:headers_bearer) { { 'Authorization' => "Bearer #{token.token}" } }
   let(:headers_order_token) { { 'X-Spree-Order-Token' => order.token } }
 end
 
-[200, 201, 400, 404, 403, 422].each do |status_code|
+[200, 201, 400, 401, 404, 403, 422].each do |status_code|
   shared_examples "returns #{status_code} HTTP status" do
     it "returns #{status_code}" do
       expect(response.status).to eq(status_code)
     end
   end
diff --git a/api/spec/requests/spree/api/v2/errors_spec.rb b/api/spec/requests/spree/api/v2/errors_spec.rb
index 6421c8cde..22bf101e8 100644
--- a/api/spec/requests/spree/api/v2/errors_spec.rb
+++ b/api/spec/requests/spree/api/v2/errors_spec.rb
@@ -27,6 +27,21 @@ describe 'API v2 Errors spec', type: :request do
 
     it 'returns proper error message' do
       expect(json_response['error']).to eq('You are not authorized to access this page.')
     end
   end
+
+  context 'expired token failure' do
+    let(:user) { create(:user) }
+    let(:headers) { headers_bearer }
+
+    include_context 'API v2 tokens'
+
+    before do
+      token.expires_in = -1
+      token.save
+      get '/api/v2/storefront/account', headers: headers
+    end
+
+    it_behaves_like 'returns 401 HTTP status'
+  end
 end

Vulnerable code:           @spree_current_user ||= Spree.user_class.find_by(id: doorkeeper_token.resource_owner_id) if doorkeeper_token
[200, 201, 400, 404, 403, 422].each do |status_code|
Fixed code:           return nil unless doorkeeper_token
          return @spree_current_user if @spree_current_user

          doorkeeper_authorize!

          @spree_current_user ||= Spree.user_class.find_by(id: doorkeeper_token.resource_owner_id)
[200, 201, 400, 401, 404, 403, 422].each do |status_code|

  context 'expired token failure' do
    let(:user) { create(:user) }
    let(:headers) { headers_bearer }

    include_context 'API v2 tokens'

    before do
      token.expires_in = -1
      token.save
      get '/api/v2/storefront/account', headers: headers
    end

    it_behaves_like 'returns 401 HTTP status'
  end