CVE-2021-25970: Insufficient Session Expiration (CWE-613)
value: Camaleon CMS 0.1.7 to 2.6.0 doesn’t terminate the active session of the users, even after the admin changes the user’s password. A user that was already logged in, will still have access to the application even after the password was changed.
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: 8.8
Published: 2021-10-20T12:15Z
Repository: https://github.com/owen2345/camaleon-cms
Language: Ruby
Files changed: app/controllers/camaleon_cms/admin/users_controller.rb, app/helpers/camaleon_cms/session_helper.rb, app/models/concerns/camaleon_cms/user_methods.rb
Commit date: 2021-10-11 23:53:35 -0500
Commit message: Logout user on password change, except in the session making the change

Diff: diff --git a/app/controllers/camaleon_cms/admin/users_controller.rb b/app/controllers/camaleon_cms/admin/users_controller.rb
index 69ec6f2f..d9c61038 100644
--- a/app/controllers/camaleon_cms/admin/users_controller.rb
+++ b/app/controllers/camaleon_cms/admin/users_controller.rb
@@ -44,11 +44,26 @@ class CamaleonCms::Admin::UsersController < CamaleonCms::AdminController
   end
 
   # update som ajax requests from profile or user form
   def updated_ajax
     @user = current_site.users.find(params[:user_id])
-    render inline: @user.update(params.require(:password).permit!) ? "" : @user.errors.full_messages.join(', ')
+    update_session = current_user_is?(@user)
+    @user.update(params.require(:password).permit!)
+    render inline: @user.errors.full_messages.join(', ')
+    # keep user logged in when changing their own password
+    update_auth_token_in_cookie @user.auth_token if update_session && @user.saved_change_to_password_digest?
+  end
+
+  def update_auth_token_in_cookie(token)
+    return unless cookie_auth_token_complete?
+    current_token = cookie_split_auth_token
+    updated_token = [token, *current_token[1..-1]]
+    cookies[:auth_token] = updated_token.join("&")
+  end
+
+  def current_user_is?(user)
+    user_auth_token_from_cookie == user.auth_token rescue false
   end
 
   def edit
     add_breadcrumb I18n.t("camaleon_cms.admin.button.edit")
     r = {user: @user, render: 'form' }
diff --git a/app/helpers/camaleon_cms/session_helper.rb b/app/helpers/camaleon_cms/session_helper.rb
index 248c227b..91ea0d0a 100644
--- a/app/helpers/camaleon_cms/session_helper.rb
+++ b/app/helpers/camaleon_cms/session_helper.rb
@@ -118,15 +118,25 @@ module CamaleonCms::SessionHelper
     return @cama_current_user if defined?(@cama_current_user)
     # api current user...
     @cama_current_user = cama_calc_api_current_user
     return @cama_current_user if @cama_current_user
 
-    return nil unless cookies[:auth_token].present?
-    c = cookies[:auth_token].split("&")
-    return nil unless c.size == 3
+    return nil unless cookie_auth_token_complete?
 
-    @cama_current_user = current_site.users_include_admins.find_by_auth_token(c[0]).try(:decorate)
+    @cama_current_user = current_site.users_include_admins.find_by_auth_token(user_auth_token_from_cookie).try(:decorate)
+  end
+
+  def cookie_auth_token_complete?
+    cookie_split_auth_token&.size == 3
+  end
+
+  def cookie_split_auth_token
+    cookies[:auth_token]&.split("&")
+  end
+
+  def user_auth_token_from_cookie
+    cookie_split_auth_token.first
   end
 
   # check if a visitor was logged in
   # if the user was not logged in, then redirect to login url
   def cama_authenticate(redirect_uri = nil)
diff --git a/app/models/concerns/camaleon_cms/user_methods.rb b/app/models/concerns/camaleon_cms/user_methods.rb
index aebe1822..9988670f 100644
--- a/app/models/concerns/camaleon_cms/user_methods.rb
+++ b/app/models/concerns/camaleon_cms/user_methods.rb
@@ -9,10 +9,12 @@ module CamaleonCms::UserMethods extend ActiveSupport::Concern
     # callbacks
     before_validation :cama_before_validation
     before_destroy :reassign_posts
     after_destroy :reassign_comments
     before_create { generate_token(:auth_token) }
+    # invaliidate sessions when changing password
+    before_update { generate_token :auth_token if will_save_change_to_password_digest? }
 
     # relations
     cama_define_common_relationships('User')
     has_many :all_posts, class_name: "CamaleonCms::Post", foreign_key: :user_id
     has_many :all_comments, class_name: 'CamaleonCms::PostComment'

Vulnerable code:     render inline: @user.update(params.require(:password).permit!) ? "" : @user.errors.full_messages.join(', ')
    return nil unless cookies[:auth_token].present?
    c = cookies[:auth_token].split("&")
    return nil unless c.size == 3
    @cama_current_user = current_site.users_include_admins.find_by_auth_token(c[0]).try(:decorate)
Fixed code:     update_session = current_user_is?(@user)
    @user.update(params.require(:password).permit!)
    render inline: @user.errors.full_messages.join(', ')
    # keep user logged in when changing their own password
    update_auth_token_in_cookie @user.auth_token if update_session && @user.saved_change_to_password_digest?
  end

  def update_auth_token_in_cookie(token)
    return unless cookie_auth_token_complete?
    current_token = cookie_split_auth_token
    updated_token = [token, *current_token[1..-1]]
    cookies[:auth_token] = updated_token.join("&")
  end

  def current_user_is?(user)
    user_auth_token_from_cookie == user.auth_token rescue false
    return nil unless cookie_auth_token_complete?
    @cama_current_user = current_site.users_include_admins.find_by_auth_token(user_auth_token_from_cookie).try(:decorate)
  end

  def cookie_auth_token_complete?
    cookie_split_auth_token&.size == 3
  end

  def cookie_split_auth_token
    cookies[:auth_token]&.split("&")
  end

  def user_auth_token_from_cookie
    cookie_split_auth_token.first
    # invaliidate sessions when changing password
    before_update { generate_token :auth_token if will_save_change_to_password_digest? }