CVE-2020-5607 (published 2020-07-10T02:15Z)

Open redirect vulnerability in SHIRASAGI v1.13.1 and earlier allows remote attackers to redirect users to arbitrary web sites and conduct phishing attacks via unspecified vectors.

Severity: MEDIUM
CVSS v2 base score: 5.8 (AV:N/AC:M/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-601
cwe_name: URL Redirection to Untrusted Site ('Open Redirect')
description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
extended_description: An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance. Whether this issue poses a vulnerability will be subject to the intended behavior of the application. For example, a search engine might intentionally provide redirects to arbitrary URLs.
         

Fix information:
repo_url: https://github.com/shirasagi/shirasagi
commit_details: repo_url: https://github.com/shirasagi/shirasagi
author: NAKANO Hideo
author_date: 2020-07-01 13:49:01+09:00
msg: [fix][cms] open redirect vulnerability on member login (#3646)

* [fix] open redirect vulnerability

* [add] reproducible spec
repository: repo_url: https://github.com/shirasagi/shirasagi
repo_name: shirasagi/shirasagi
description: SHIRASAGI
repo_language: JavaScript
owner: shirasagi
file_changes: filename: login_filter.rb
change_type: ModificationType.MODIFY
diff: @@ -68,11 +68,6 @@ module Member::LoginFilter
     "#{member_login_node.url}login.html"
   end
 
-  def redirect_url
-    return "/" unless member_login_node
-    member_login_node.redirect_url || "/"
-  end
-
   def translate_redirect_option(opts)
     case opts[:redirect]
     when true

programming_language: Ruby
filename: login_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -21,13 +21,36 @@ class Member::Agents::Nodes::LoginController < ApplicationController
       remote_addr: remote_addr,
       user_agent: request.user_agent)
 
-    ref = URI::decode(params[:ref] || flash[:ref] || "")
-    ref = redirect_url if ref.blank?
+    ref = @cur_node.make_trusted_full_url(params[:ref] || flash[:ref])
+    ref = @cur_node.redirect_full_url if ref.blank?
+    ref = @cur_site.full_url if ref.blank?
     flash.discard(:ref)
 
     redirect_to ref
   end
 
+  def make_url_if_trusted(ref)
+    return if ref.blank?
+
+    ref = URI::decode(ref)
+
+    site_url = URI.parse(@cur_site.full_url)
+    full_url = URI.join(site_url, ref) rescue nil
+    return if full_url.blank?
+    return unless %w(http https).include?(full_url.scheme)
+
+    # normalize full url
+    full_url.fragment = nil
+    full_url.query = nil
+
+    # trusted?
+    return if full_url.scheme != site_url.scheme
+    return if full_url.host != site_url.host
+    return if full_url.port != site_url.port
+
+    full_url.to_s
+  end
+
   public
 
   def login

programming_language: Ruby
filename: node.rb
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,45 @@ module Member::Node
     include History::Addon::Backup
 
     default_scope ->{ where(route: "member/login") }
+
+    def redirect_full_url
+      return if redirect_url.blank?
+
+      ret = make_full_url(redirect_url)
+      return if ret.blank?
+      return unless trusted?(ret)
+
+      ret.to_s
+    end
+
+    def make_trusted_full_url(ref)
+      return if ref.blank?
+
+      full_url = make_full_url(URI::decode(ref))
+      return if full_url.blank?
+
+      # normalize full url
+      full_url.fragment = nil
+      full_url.query = nil
+
+      # trusted?
+      return unless trusted?(full_url)
+
+      full_url.to_s
+    end
+
+    private
+
+    def make_full_url(path)
+      site_root_url = URI.parse(site.full_root_url)
+      URI.join(site_root_url, path) rescue nil
+    end
+
+    def trusted?(full_url)
+      return false if full_url.blank?
+
+      %w(http https).include?(full_url.scheme) && full_url.to_s.start_with?(site.full_url)
+    end
   end
 
   class Mypage

programming_language: Ruby
filename: login_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe 'members/agents/nodes/login', type: :feature, dbscope: :example do
+describe 'members/agents/nodes/login', type: :feature, dbscope: :example, js: true do
   describe "ss-2724" do
     let(:root_site) { cms_site }
     let(:site1) { create(:cms_site_subdir, domains: root_site.domains) }
@@ -46,4 +46,29 @@ describe 'members/agents/nodes/login', type: :feature, dbscope: :example do
       expect(page).to have_css(".form-login")
     end
   end
+
+  describe "open redirect" do
+    let(:site) { cms_site }
+    let(:layout) { create_cms_layout(cur_site: site) }
+    let(:index_page) { create :cms_page, cur_site: site, layout: layout, filename: "index.html", html: "you've been logged in" }
+    let!(:login_node) do
+      create(
+        :member_node_login, cur_site: site, layout: layout, redirect_url: index_page.url, form_auth: "enabled"
+      )
+    end
+    let!(:member) { create :cms_member, cur_site: site }
+
+    it do
+      visit "#{login_node.full_url}login.html" + "?" + { ref: URI.encode("https://www.ss-proj.org/") }.to_query
+
+      within ".form-login" do
+        fill_in "item[email]", with: member.email
+        fill_in "item[password]", with: member.in_password
+
+        click_on I18n.t("ss.login")
+      end
+
+      expect(page).to have_css(".body", text: "you've been logged in")
+    end
+  end
 end

programming_language: Ruby
filename: login_spec.rb
change_type: ModificationType.ADD
diff: @@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Member::Node::Login, type: :model, dbscope: :example do
+  describe "#redirect_full_url" do
+    let(:site) { cms_site }
+
+    context "when redirect_url is missing" do
+      let(:node) { create :member_node_login, cur_site: site, redirect_url: nil }
+      it do
+        expect(node.redirect_full_url).to be_blank
+      end
+    end
+
+    context "when redirect_url is full path" do
+      let(:node) { create :member_node_login, cur_site: site, redirect_url: "/#{unique_id}" }
+      it do
+        expect(node.redirect_full_url).to eq URI.join(node.site.full_url, node.redirect_url).to_s
+      end
+    end
+
+    context "when redirect_url is full url" do
+      let(:node) { create :member_node_login, cur_site: site, redirect_url: "#{site.full_url}#{unique_id}" }
+      it do
+        expect(node.redirect_full_url).to eq URI.join(node.site.full_url, node.redirect_url).to_s
+      end
+    end
+
+    context "when redirect_url is other site url" do
+      let(:node) { create :member_node_login, cur_site: site, redirect_url: unique_url }
+      it do
+        expect(node.redirect_full_url).to be_blank
+      end
+    end
+  end
+
+  describe "#make_trusted_full_url" do
+    let(:site) { cms_site }
+    let(:node) { create :member_node_login, cur_site: site }
+
+    context "when nil is given" do
+      it do
+        expect(node.make_trusted_full_url(nil)).to be_blank
+      end
+    end
+
+    context "when empty string is given" do
+      it do
+        expect(node.make_trusted_full_url("")).to be_blank
+      end
+    end
+
+    context "when full path is given" do
+      let(:path) { "/#{unique_id}" }
+      it do
+        expect(node.make_trusted_full_url(path)).to eq URI.join(node.site.full_url, path[1..-1]).to_s
+      end
+    end
+
+    context "when full url is given" do
+      let(:url) { "#{site.full_url}#{unique_id}" }
+      it do
+        expect(node.make_trusted_full_url(url)).to eq url
+      end
+    end
+
+    context "when other site url is given" do
+      let(:url) { unique_url }
+      it do
+        expect(node.make_trusted_full_url(url)).to be_blank
+      end
+    end
+  end
+end

programming_language: Ruby