Repository navigation
Commit 6026bb3
committed
sec(api): stop a CSRF rejection falling through to the token approve path
Review finding F1. approveRIExchange falls back to the token flow when the
session path returns 403, so email-link holders can still approve. That test
is isPermissionDenied, which compares the status code alone, so a CSRF
rejection was indistinguishable from a legitimate approve-own denial and the
request proceeded on the token.
Measured before fixing, session with no CSRF headers and a valid ?token=:
err = <nil>
TransitionRIExchangeStatus called = true
ExecuteExchange called = true money moved
transition actor was NIL = true audit attribution lost
StampRIExchangeApprovedBy called = false
Not a reopened CSRF hole: the caller still needs the unguessable ApprovalToken,
which is constant-time compared and is sufficient authorization by design. But
a legitimate session-authed approve carrying a token degraded to an
unattributed system approval on a money path, and the code comment claimed
parity with approvePurchaseViaSession, which returns its CSRF 403 directly
rather than swallowing it. Shipping a #1757 fix whose comment overstates what
the code does is the failure mode #1757 was about.
CSRF rejection is now the errCSRFRejected sentinel. Note precisely what
protects it: isPermissionDenied(errCSRFRejected) is still true, because the
sentinel is a 403. What keeps it out of the fallback is the short-circuit
ordering of || at the dispatch, with the errors.Is term first. "Reached first"
rather than "unreachable", and the two have different failure modes: reordering
that expression reopens F1. The new test drives that exact path, so a reorder
fails it.
Chose the sentinel over widening isPermissionDenied because that predicate has
three other call sites in handler_purchases.go, and narrowing its meaning
globally is a larger blast radius than this finding warrants. Verified the
record-level approve-own denial still falls through, which is the case the
fallback exists for.
The new test drives the dispatch with a valid token, which the existing
empty-token test cannot reach, and asserts the money-moving call is never made.
Mutation-verified: replacing the sentinel with a plain NewClientError(403, ...)
makes it fail on both the TransitionRIExchangeStatus assertion and "an error is
expected but got nil", by assertion rather than by panic. Restored afterwards
and confirmed byte-identical.
Refs #17571 parent b7d7c1c commit 6026bb3
3 files changed
Lines changed: 86 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2057 | 2057 | | |
2058 | 2058 | | |
2059 | 2059 | | |
2060 | | - | |
| 2060 | + | |
| 2061 | + | |
| 2062 | + | |
| 2063 | + | |
| 2064 | + | |
| 2065 | + | |
| 2066 | + | |
| 2067 | + | |
| 2068 | + | |
2061 | 2069 | | |
2062 | 2070 | | |
2063 | 2071 | | |
| |||
2110 | 2118 | | |
2111 | 2119 | | |
2112 | 2120 | | |
2113 | | - | |
| 2121 | + | |
2114 | 2122 | | |
2115 | 2123 | | |
2116 | 2124 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
21 | 29 | | |
22 | 30 | | |
23 | 31 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
500 | 500 | | |
501 | 501 | | |
502 | 502 | | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
503 | 571 | | |
504 | 572 | | |
505 | 573 | | |
| |||
0 commit comments