Files
sure/test/controllers/depositories_controller_test.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

42 lines
1.4 KiB
Ruby

require "test_helper"
class DepositoriesControllerTest < ActionDispatch::IntegrationTest
include AccountableResourceInterfaceTest
setup do
sign_in @user = users(:family_admin)
@account = accounts(:depository)
end
test "create falls back to the stored return_to when no form param is present" do
get new_account_path(return_to: transactions_path) # StoreLocation captures it into the session
assert_difference -> { Account.count } => 1 do
post depositories_path, params: {
account: { name: "Return To Checking", currency: "USD", balance: 100, accountable_type: "Depository" }
}
end
assert_redirected_to transactions_path
end
test "create prefers the form return_to over the session value" do
get new_account_path(return_to: transactions_path) # session return_to
post depositories_path, params: {
account: { name: "Form RT Checking", currency: "USD", balance: 100, accountable_type: "Depository", return_to: budgets_path }
}
assert_redirected_to budgets_path
end
test "create ignores an external return_to (open-redirect guard)" do
post depositories_path, params: {
account: { name: "Evil RT Checking", currency: "USD", balance: 100, accountable_type: "Depository", return_to: "https://evil.example/phish" }
}
created = Account.order(:created_at).last
assert_redirected_to account_path(created) # not the external URL
end
end