mirror of
https://github.com/we-promise/sure.git
synced 2026-06-06 03:09:02 +00:00
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).
This commit is contained in:
committed by
GitHub
parent
5abf9cb537
commit
74f811c334
@@ -188,4 +188,89 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert draft_account.active?
|
||||
assert_redirected_to account_path(draft_account)
|
||||
end
|
||||
|
||||
test "address update on draft account honors stored return_to over the account page" do
|
||||
draft_account = Account.create!(
|
||||
family: @user.family,
|
||||
name: "Draft Property RT",
|
||||
accountable: Property.new,
|
||||
status: "draft",
|
||||
balance: 500000,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
# The property wizard (create → balances → address) doesn't thread return_to
|
||||
# as a form param, so StoreLocation's session value is the only carrier.
|
||||
get new_account_path(return_to: transactions_path)
|
||||
|
||||
patch update_address_property_path(draft_account), params: {
|
||||
property: {
|
||||
address_attributes: {
|
||||
line1: "789 Activate St",
|
||||
locality: "New York",
|
||||
region: "NY",
|
||||
country: "US",
|
||||
postal_code: "10001"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draft_account.reload
|
||||
assert draft_account.active?
|
||||
assert_redirected_to transactions_path
|
||||
end
|
||||
|
||||
test "address update ignores an external stored return_to (open-redirect guard)" do
|
||||
draft_account = Account.create!(
|
||||
family: @user.family,
|
||||
name: "Draft Property Evil",
|
||||
accountable: Property.new,
|
||||
status: "draft",
|
||||
balance: 500000,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
# A hostile ?return_to is rejected at store time, so the wizard falls back
|
||||
# to the account page rather than stream-redirecting off-site.
|
||||
get new_account_path(return_to: "https://evil.example/phish")
|
||||
|
||||
patch update_address_property_path(draft_account), params: {
|
||||
property: {
|
||||
address_attributes: {
|
||||
line1: "1 Safe St", locality: "NYC", region: "NY", country: "US", postal_code: "10001"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draft_account.reload
|
||||
assert draft_account.active?
|
||||
assert_redirected_to account_path(draft_account)
|
||||
end
|
||||
|
||||
test "address update tolerates a non-String stored return_to without raising" do
|
||||
draft_account = Account.create!(
|
||||
family: @user.family,
|
||||
name: "Draft Property Array",
|
||||
accountable: Property.new,
|
||||
status: "draft",
|
||||
balance: 500000,
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
# `?return_to[]=foo` makes params[:return_to] an Array; safe_return_to must
|
||||
# reject it via the is_a?(String) guard instead of raising NoMethodError.
|
||||
get new_account_path("return_to" => [ "/transactions" ])
|
||||
|
||||
patch update_address_property_path(draft_account), params: {
|
||||
property: {
|
||||
address_attributes: {
|
||||
line1: "1 Safe St", locality: "NYC", region: "NY", country: "US", postal_code: "10001"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draft_account.reload
|
||||
assert draft_account.active?
|
||||
assert_redirected_to account_path(draft_account)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user