mirror of
https://github.com/we-promise/sure.git
synced 2026-06-05 18:59:04 +00:00
* 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).
53 lines
1.5 KiB
Ruby
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
|