Skip to content

fix: split rate-limit buckets, guard cross-tab network change, enforce error discriminant uniqueness - #1178

Merged
Ejirowebfi merged 1 commit into
Favourorg:mainfrom
manuelusman73-png:fix/issues-1162-1163-1164
Aug 28, 2026
Merged

fix: split rate-limit buckets, guard cross-tab network change, enforce error discriminant uniqueness#1178
Ejirowebfi merged 1 commit into
Favourorg:mainfrom
manuelusman73-png:fix/issues-1162-1163-1164

Conversation

@manuelusman73-png

Copy link
Copy Markdown
Contributor

Summary

Fixes three issues from the August 2026 critical-issues audit in a single PR.

Closes #1162
Closes #1163
Closes #1164


#1162 — Rate-limit bucket shared between challenge issuance and authenticated actions (High)

Root cause: isRateLimited(address) keyed one shared bucket per Stellar address across every call site — including the unauthenticated GET /api/auth/challenge, where a Stellar address is a public value. An attacker could exhaust a victim's daily quota at zero cost, locking them out of login and all authenticated actions for 24 hours.

Changes:

  • api/_lib/rateLimit.ts: split into two exported functions:
    • isChallengeRateLimited(ip) — keyed on requester IP, limits GET /api/auth/challenge; defaults to 20/window, 200/day (env: CHALLENGE_RATE_LIMIT_WINDOW, CHALLENGE_RATE_LIMIT_DAY)
    • isActionRateLimited(address) — keyed on wallet address, limits uploads/unpin after proof-of-possession; keeps existing defaults of 10/window, 100/day
    • isRateLimited retained as a deprecated shim delegating to isActionRateLimited
  • api/auth/challenge.ts: GET now calls isChallengeRateLimited(clientIp(req)); POST verify calls isActionRateLimited(address)
  • api/ipfs/upload-file.ts, upload-json.ts, unpin.ts: switched to isActionRateLimited
  • api/_lib/rateLimit.test.ts: added tests asserting full bucket isolation including the griefing scenario (attacker floods challenge endpoint with victim address; victim action quota untouched)

#1163 — Cross-tab localStorage network sync can retarget an in-progress form (High)

Root cause: useLocalStorage's storage event listener silently updates NetworkContext when another tab writes the persisted network key. StellarContext rebuilds stellarService pointing at a different factory contract, while MintForm/BurnForm/SetMetadataForm/AdminPanel retain their stale token address and amount. useNetworkGuard only checks Freighter-vs-app agreement — it had no way to know the app's own target changed mid-fill.

Changes:

  • frontend/src/hooks/useNetworkGuard.ts: captures the network value at hook-mount time; sets networkChangedSinceMount = true whenever NetworkContext drifts away (cross-tab or manual switch); returns new acknowledgeNetworkChange() that rebases the snapshot and clears the flag. Blocks submission with a descriptive reason when drift is detected.
  • MintForm, BurnForm, SetMetadataForm, AdminPanel: consume networkChangedSinceMount and acknowledgeNetworkChange; render an explicit "Network changed to X since you opened this form — review and confirm" banner with a confirmation button.

#1164 — Three contract errors sharing discriminant 24 (Medium-High)

Root cause: The contract already carries the correct unique codes (24 TreasuryTransferFailed, 25 BatchSizeExceeded, 26 NoPendingProposal, 27 ProposalExpired), but there was no automated guard preventing a future collision, and the frontend's CONTRACT_ERROR_MESSAGES map was missing all four codes.

Changes:

  • contracts/token-factory/src/test.rs: added test_error_discriminants_are_unique — exhaustively lists all 27 Error variants, casts each to u32, asserts uniqueness via HashSet. Also asserts the total variant count, so adding a new variant without registering it here fails CI.
  • frontend/src/utils/contractErrors.ts: added entries for codes 24–27 in both CONTRACT_ERROR_MESSAGES and CONTRACT_ERROR_REQUIREMENTS.

Documentation

docs/security-triage.md: added a section explaining why challenge issuance and authenticated actions are rate-limited differently (#1162), why the network-changed guard is separate from the Freighter mismatch check (#1163), and where the error-discriminant uniqueness check lives (#1164).


Testing

  • The rolldown native binding is missing in this codespace (@rolldown/binding-linux-x64-gnu not installed), which prevents npm run test:api and npm run test from running locally. This is a pre-existing environment issue unrelated to these changes — CI on the upstream repo will execute the full suite.
  • TypeScript compiles cleanly (tsc --noEmit on both root and frontend tsconfigs) with no new errors introduced by these changes.

…e error discriminant uniqueness

Closes Favourorg#1162
Closes Favourorg#1163
Closes Favourorg#1164

## Favourorg#1162 — Rate-limit bucket shared between challenge issuance and authenticated actions

Challenge issuance (GET /api/auth/challenge) is now limited by **requester
IP** via a new isChallengeRateLimited(ip) function, not by the target Stellar
address. Authenticated actions (upload-file, upload-json, unpin) continue to
use an address-keyed bucket via the new isActionRateLimited(address) function.
The two bucket namespaces (ratelimit:challenge:<ip> vs ratelimit:action:<addr>)
are fully independent in both the Vercel KV and in-memory paths, so exhausting
one cannot affect the other.

- api/_lib/rateLimit.ts: split into isChallengeRateLimited / isActionRateLimited;
  kept isRateLimited as a deprecated shim for any external callers
- api/auth/challenge.ts: GET uses isChallengeRateLimited(clientIp(req));
  POST verify uses isActionRateLimited(address)
- api/ipfs/upload-file.ts, upload-json.ts, unpin.ts: switched to isActionRateLimited
- api/_lib/rateLimit.test.ts: added tests asserting bucket isolation including
  the griefing scenario (attacker floods challenge endpoint with victim address;
  victim action quota is unaffected)

## Favourorg#1163 — Cross-tab localStorage network sync can retarget an in-progress form

useNetworkGuard now captures the network value active at hook-mount time and
sets networkChangedSinceMount=true whenever NetworkContext drifts away from
that baseline (e.g. via a cross-tab storage event). Submission is blocked and
an explicit 'Network changed to X since you opened this form' banner with an
acknowledgement button is shown. Clicking the button calls
acknowledgeNetworkChange(), which rebases the snapshot and re-enables the form.

- frontend/src/hooks/useNetworkGuard.ts: added networkChangedSinceMount and
  acknowledgeNetworkChange() to the return value
- MintForm, BurnForm, SetMetadataForm, AdminPanel: consume the new values and
  render the confirmation banner

## Favourorg#1164 — Contract error enum discriminant uniqueness

Added test_error_discriminants_are_unique in contracts/token-factory/src/test.rs
that exhaustively lists all 27 Error variants, casts each to u32, and asserts
uniqueness via a HashSet. A future collision will fail CI before shipping.
Added entries for codes 24-27 (TreasuryTransferFailed, BatchSizeExceeded,
NoPendingProposal, ProposalExpired) to CONTRACT_ERROR_MESSAGES and
CONTRACT_ERROR_REQUIREMENTS in frontend/src/utils/contractErrors.ts.

Documented all three fixes in docs/security-triage.md.
@Ejirowebfi
Ejirowebfi merged commit 7ddf2c4 into Favourorg:main Aug 28, 2026
18 of 20 checks passed
github-actions Bot pushed a commit that referenced this pull request Aug 28, 2026
## [1.8.7](v1.8.6...v1.8.7) (2026-08-28)

### Bug Fixes

* **auth:** replace timing-unsafe JWT signature comparison with timingSafeEqual ([#1179](#1179)) ([8925440](8925440)), closes [#4](#4)
* **contract:** replace single-step upgrade with two-step timelock (issue [#6](#6)) ([#1180](#1180)) ([cb9f98a](cb9f98a))
* split rate-limit buckets, guard cross-tab network change, enforce error discriminant uniqueness ([#1178](#1178)) ([7ddf2c4](7ddf2c4)), closes [#1162](#1162) [#1163](#1163) [#1164](#1164) [#1162](#1162) [#1163](#1163) [#1164](#1164)
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.8.7 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants