Files
sure/app/controllers/concerns/store_location.rb
Guillem Arias Fauste 74f811c334 fix(accounts): honor stored return_to after subtype account creation (#2109)
* fix(accounts): honor stored return_to after subtype account creation

Closes #1766.

The savings-goals empty-state "Add an account" CTA passes ?return_to, which
StoreLocation captures into session[:return_to], but account-creation flows
didn't always consume it:

- AccountableResource#create honored a form-carried return_to but not the
  session value, so if the param wasn't threaded through the multi-step
  new-account flow the user still landed on the account page. Added a
  session[:return_to] fallback (the form param still wins).
- PropertiesController is a 3-step wizard (create → balances → address) that
  never threaded return_to as a form param, and its final redirect went
  straight to account_path. It now honors session[:return_to] on completion.

Rails blocks external-host redirects, so return_to can't open-redirect.
valuations#create uses redirect_back_or_to (referer-based) — different flow,
left as-is.

Tests: depository create prefers the form return_to and falls back to the
session value; property wizard completion honors the stored return_to.

* fix(accounts): block open-redirect via return_to; consume session value

Two AI-review findings on #2109:

- Open-redirect (codex): the property wizard's turbo_stream completion uses
  stream_redirect_to, which the client resolves with Turbo.visit — that
  full-navigates cross-origin, bypassing Rails' redirect host-guard. A crafted
  ?return_to=https://evil could walk the user off-site. Filter return_to at the
  StoreLocation choke point (store time) to internal absolute paths only, and
  sanitize the separate form-param channel, so an unsafe value can't reach
  redirect_to / stream_redirect_to.
- Stale session (coderabbit): session[:return_to] was read but never consumed.
  Consume it with delete at redirect time so it can't leak into a later flow.

Adds guard tests (external return_to falls back to the account page).

* fix(security): guard safe_return_to against non-String return_to

A crafted `?return_to[]=foo` makes params[:return_to] an Array, and
Array#match? doesn't exist, so safe_return_to raised NoMethodError
before the open-redirect hardening could reject it. Add an
is_a?(String) check as the first gate. Other CodeRabbit/Codex
return_to findings on this PR were already addressed (consume-side
re-validation + session.delete).
2026-06-03 15:15:49 +02:00

53 lines
1.5 KiB
Ruby

module StoreLocation
extend ActiveSupport::Concern
included do
helper_method :previous_path
before_action :store_return_to
after_action :clear_previous_path
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
end
def previous_path
session[:return_to] || fallback_path
end
private
def handle_not_found
if request.fullpath == session[:return_to]
session.delete(:return_to)
redirect_to fallback_path
else
head :not_found
end
end
def store_return_to
safe = safe_return_to(params[:return_to])
session[:return_to] = safe if safe
end
# Only allow internal absolute paths (a single leading "/"). Blocks absolute
# URLs, protocol-relative ("//evil"), and backslash tricks ("/\\evil") so a
# crafted ?return_to= can't open-redirect — including via a custom
# turbo_stream redirect, which Rails' redirect host-guard does NOT cover
# (the client `Turbo.visit`es the target and full-navigates cross-origin).
def safe_return_to(value)
# is_a?(String) first: a crafted `?return_to[]=foo` makes params[:return_to]
# an Array, and Array#match? doesn't exist — without this guard the helper
# raises NoMethodError before the redirect hardening can reject it.
value if value.is_a?(String) && value.present? && value.match?(%r{\A/(?![/\\])})
end
def clear_previous_path
if request.fullpath == session[:return_to]
session.delete(:return_to)
end
end
def fallback_path
root_path
end
end