Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/admin_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1407,7 +1407,7 @@ def referral_programs
end

def referral_program_create
@referral_program = Referral::Program.new(name: params[:name], creator: current_user)
@referral_program = Referral::Program.new(name: params[:name], redirect_to: params[:redirect_to], creator: current_user)

if @referral_program.save
flash[:success] = "Referral program created successfully."
Expand Down
6 changes: 4 additions & 2 deletions app/controllers/referral/links_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
Rails.error.handle do
Referral::Attribution.create!(user: current_user, program: @link.program, link: @link)
end

redirect_to @link.program.redirect_to.presence || root_path, allow_other_host: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is only settable by admins this should be fine - I would leave a quick comment here explaining that

else
skip_authorization
end

redirect_to params[:return_to] || root_path
redirect_to params[:return_to] || root_path

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 3 days ago

The best practice is to never trust user input for redirect destinations without validation. To fix this, we should only allow redirection to:

  1. A whitelist of known good (relative) paths.
  2. Or, only allow relative URLs (i.e., not full URLs with protocols/hosts).

Here, the preferable solution is (2): check if params[:return_to] is present, is a relative path, and does not include a host or protocol. If it fails this check, redirect to root_path instead.

To implement this, parse the parameter with URI.parse, and only redirect if the result is (a) a path (relative path; i.e., the URI’s host and scheme are nil), and (b) not an absolute URL. Otherwise, fallback to root_path.

You may need to add require 'uri' at the top if it's not present.

All changes are within app/controllers/referral/links_controller.rb:

  • Add a safe helper method, e.g., sanitize_return_to, that does this check.
  • Use this sanitized value in the redirect call at line 25.

Suggested changeset 1
app/controllers/referral/links_controller.rb

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/app/controllers/referral/links_controller.rb b/app/controllers/referral/links_controller.rb
--- a/app/controllers/referral/links_controller.rb
+++ b/app/controllers/referral/links_controller.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require 'uri'
+
 module Referral
   class LinksController < ApplicationController
     before_action :set_link, only: :show
@@ -22,7 +24,7 @@
       else
         skip_authorization
 
-        redirect_to params[:return_to] || root_path
+        redirect_to sanitize_return_to
       end
     end
 
@@ -32,5 +34,23 @@
       @link = Referral::Link.find_by(slug: params[:id]).presence || Referral::Link.find_by_hashid(params[:id])
     end
 
+    # Only allow redirection to a safe internal path.
+    def sanitize_return_to
+      return_to = params[:return_to]
+      return root_path unless return_to.present?
+
+      begin
+        uri = URI.parse(return_to)
+        # Allow only relative paths (no scheme or host)
+        if uri.host.nil? && uri.scheme.nil? && uri.path.present? && return_to.starts_with?('/')
+          return return_to
+        else
+          return root_path
+        end
+      rescue URI::InvalidURIError
+        root_path
+      end
+    end
+
   end
 end
EOF
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'uri'

module Referral
class LinksController < ApplicationController
before_action :set_link, only: :show
@@ -22,7 +24,7 @@
else
skip_authorization

redirect_to params[:return_to] || root_path
redirect_to sanitize_return_to
end
end

@@ -32,5 +34,23 @@
@link = Referral::Link.find_by(slug: params[:id]).presence || Referral::Link.find_by_hashid(params[:id])
end

# Only allow redirection to a safe internal path.
def sanitize_return_to
return_to = params[:return_to]
return root_path unless return_to.present?

begin
uri = URI.parse(return_to)
# Allow only relative paths (no scheme or host)
if uri.host.nil? && uri.scheme.nil? && uri.path.present? && return_to.starts_with?('/')
return return_to
else
return root_path
end
rescue URI::InvalidURIError
root_path
end
end

end
end
Copilot is powered by AI and may make mistakes. Always verify output.
end
end

private
Expand Down
1 change: 1 addition & 0 deletions app/models/referral/program.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# login_header_text :string
# login_text_color :string
# name :string not null
# redirect_to :string
# created_at :datetime not null
# updated_at :datetime not null
# creator_id :bigint
Expand Down
4 changes: 3 additions & 1 deletion app/views/admin/referral_programs.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<div class="flex flex-col">
<h5 class="m-0 p-0"><%= program.name %></h5>
<small class="text-muted"><%= program.created_at.strftime("%Y-%m-%d") %> • Created by <%= program.creator.name %></small>
<small class="text-muted">Redirects to: <%= link_to(program.redirect_to.presence || root_url) %></small>
</div>
<div class="flex flex-row gap-2 justify-between m-0">
<span class="m-0"><%= pluralize(program.users.size, "user") %> • <%= pluralize(program.new_users.size, "new user") %></span>
Expand Down Expand Up @@ -65,7 +66,8 @@
<legend>Create a referral program</legend>
<%= form.label :name, "Program name" %>
<%= form.text_field :name, placeholder: "Program Name", required: true, class: "w-100" %>
<br>
<%= form.label :redirect_to, "Redirect to (optional)" %>
<%= form.text_field :redirect_to, placeholder: "https://hcb.hackclub.com/", class: "w-100" %>
<%= form.submit "Create", class: "btn bg-success" %>
</fieldset>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddRedirectToToReferralPrograms < ActiveRecord::Migration[8.0]
def change
add_column :referral_programs, :redirect_to, :string
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.0].define(version: 2025_12_05_035167) do
ActiveRecord::Schema[8.0].define(version: 2025_12_07_022849) do
create_schema "google_sheets"

# These are extensions that must be enabled in order to support this database
Expand Down Expand Up @@ -2010,6 +2010,7 @@
t.string "login_header_text"
t.string "login_text_color"
t.string "name", null: false
t.string "redirect_to"
t.datetime "updated_at", null: false
t.index ["creator_id"], name: "index_referral_programs_on_creator_id"
end
Expand Down