CVE-2020-5607 (Ruby, published 2020-07-10T02:15Z)
Description: value: 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 v3 base score: 6.1)
Weakness: CWE-601 \u2014 URL Redirection to Untrusted Site ('Open Redirect')
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.
Repository: https://github.com/shirasagi/shirasagi
Files changed:
app/controllers/concerns/member/login_filter.rb
app/controllers/member/agents/nodes/login_controller.rb
app/models/member/node.rb
spec/features/members/agents/nodes/login_spec.rb
spec/models/member/node/login_spec.rb
Commit message: [fix][cms] open redirect vulnerability on member login (#3646)

* [fix] open redirect vulnerability

* [add] reproducible spec

Vulnerable code:
  def redirect_url
    return "/" unless member_login_node
    member_login_node.redirect_url || "/"
  end

    ref = URI::decode(params[:ref] || flash[:ref] || "")
    ref = redirect_url if ref.blank?
describe 'members/agents/nodes/login', type: :feature, dbscope: :example do

Fixed code:
    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?
  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


    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
describe 'members/agents/nodes/login', type: :feature, dbscope: :example, js: true do

  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
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