Commit Graph
13 Commits
Author SHA1 Message Date
78228e68dd security: throttle every credential-guessing endpoint, fix duplicate Rack::Attack middleware (#3263)
* security: throttle every credential-guessing endpoint, fix duplicate Rack::Attack middleware

Follow-up on #1087 (Findings H4, M7). PR 4 of the 6-PR series.

Enumerated every endpoint that checks a password, TOTP code, or backup
code (grepped for User.authenticate_by/#authenticate/#verify_otp? across
app/controllers, not just the ones named in the issue) — six in total,
none previously throttled:

- POST /sessions (SessionsController#create) — web login
- POST /mfa/verify (MfaController#verify_code) — TOTP + backup codes
  (#verify_otp? handles both internally, so no separate endpoint to add)
- POST /password_reset (PasswordResetsController#create) — also M7
- POST /api/v1/auth/login (Api::V1::AuthController#login) — mobile/API
- POST /oidc_account/create_link (OidcAccountsController#create_link) —
  password check gating SSO-identity linking, not sign-in; easy to miss
  grepping routes.rb for "session"/"login"
- POST /api/v1/auth/sso_link (Api::V1::AuthController#sso_link) — same
  as above for the mobile app

Each gets two throttles (ip AND normalized email, or ip AND the MFA
step-up's session-bound user id where there's no email param) so an
attacker can't bypass by rotating IPs against one target, nor by
spraying many emails from one IP — Rack::Attack requires every matching
throttle to pass. limit: 10/minute, matching the existing oauth/token
and admin/ip throttles already in this file.

Also fixed a latent, unrelated-but-adjacent bug found while confirming
these throttles would actually enforce the limits documented in their
own comments: config/application.rb had an explicit `config.middleware.use
Rack::Attack` alongside the gem's own Railtie doing the same thing (`bin/rails
middleware` listed it twice) — every throttle's counter was incrementing
twice per request, so all of them, old and new, were silently firing at
half their documented limit. Removed the redundant explicit registration.

Race-condition check (per standing instruction): Rack::Attack's counter
increments are atomic within its cache store, so concurrent requests at
the threshold don't undercount. No new race introduced.

New tests in test/integration/rack_attack_test.rb:
- Registration checks for all 6 new throttle keys (existing convention
  in this file).
- Direct block-level tests for the discriminator logic (right path
  matched, right value extracted, blank/missing input produces nil
  rather than a bogus key) — Rack::Attack's cache backs onto Rails.cache,
  which is :null_store in the test environment, so no amount of request
  volume in a normal integration test can ever actually trip a throttle
  here; calling the registered block directly against a constructed
  Rack::Attack::Request is what makes the assertions meaningful instead
  of just checking string keys exist.
- Regression test asserting Rack::Attack appears exactly once in the
  middleware stack.

Verified against the NAS sure_test_web container: full restart, bin/rails
test (8/8 rack_attack tests green; ran the full test/integration suite
plus sessions/mfa/password_resets/api-auth/oidc_accounts controller tests
too — 6 pre-existing failures, confirmed identical on the unmodified
baseline before concluding they're the known WebAuthn-RP-ID-mismatch and
AI-disabled environmental categories, not a regression), bin/rubocop,
bin/brakeman. Also did a live demonstration against the running container
(which runs RAILS_ENV=production, where Rack::Attack is actually enabled):
12 rapid POSTs to /sessions with bad credentials — requests 1-10 got 422,
11 and 12 got 429, exactly matching limit: 10. Container restored to its
original state and restarted afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* security: extract email from JSON bodies for credential-guess throttles

Rack::Attack runs before Rails' JSON parameter parsing, so request.params
only exposed query/form fields. The documented api/v1/auth/login and
.../sso_link JSON format bypassed the per-email throttle entirely,
letting an attacker rotate IPs against one target's account.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* security: guard JSON email peek against non-rewindable input and non-object payloads

Rack 3 no longer requires rack.input to be rewindable, and a bare
JSON.parse(body)["email"] raises NoMethodError on valid non-Hash JSON
(null, arrays, scalars) — either would 500 the request instead of just
skipping the email throttle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* security: assert non-rewindable JSON bodies stay readable by the controller

Only checking that the throttle discriminator returned nil left a gap: an
implementation that read the body and then discarded the result on error
would pass the same assertion while leaving the controller with an
exhausted stream.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* security: close credential-guessing throttle bypass via format-suffixed paths

request.path == "/sessions" (etc.) never matched "/sessions.json", which
Rails still routes to the same controller action since none of these
routes are declared format: false. Match the optional format suffix
explicitly instead, per jjmata's review on PR #3263.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* security: match Rails' actual format-segment charset in credential_guess_path

\w excludes hyphens, but Rails' default (.:format) segment matches
[^./?]+, which does include them — e.g. "/api/v1/auth/login.rate-limit"
still routed and bypassed the throttle. Match the real charset instead,
per CodeRabbit's follow-up on PR #3263.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 07:48:58 +02:00
Brandon bf5ceff269 Fix flaky sign_out teardown in six test suites (#3208)
Six suites (passkey, MFA, SnapTrade, categorize, onboarding and the Active
Storage authorization integration tests) share a sign_out helper that deletes
the user's sessions through the controller, one HTTP request per session,
iterating in unspecified order. The moment the loop deletes the session the
test itself is signed in with, every later request in the loop is
unauthenticated and silently deletes nothing, so whichever sessions happen to
sort after it survive. The sessions fixture belongs to the same user these
suites use, so a surviving fixture row then fails every assertion that expects
the user to have no sessions.

Row order usually favors the fixture, which is why the suites usually pass.
Under parallel CI they fail a few times a week, always in this file family,
always with the fixture session as the leftover. Forcing newest-first order
reproduces it deterministically on current main: ten of the fifteen passkey
tests fail.

Teardown hygiene is not the behavior under test, so the helpers now destroy
the sessions directly, which no order can break. All six suites run green
three times in a row.
2026-08-27 07:30:35 +02:00
Guillem Arias Fauste e699f5272b feat(sidekiq): mount Web UI in production behind super-admin sessions (#2683)
/sidekiq was mounted `unless Rails.env.production?`, and the Docker
image bakes RAILS_ENV=production — so no self-hosted or managed
deployment has ever had queue tooling, while the production basic-auth
block (with default "sure"/"sure" credentials) was dead code. Worse,
any custom non-production env (e.g. staging) got the dashboard with no
authentication at all.

Now:

- Development keeps the open mount for convenience.
- Everywhere else the route only exists for a signed-in super admin,
  via a routing constraint that resolves the signed session cookie the
  same way Authentication#find_session_by_cookie does. The session's
  user is always the true user (impersonation is resolved at the
  Current level and impersonating a super admin is forbidden), and
  sessions are only created after MFA verification, so neither can
  bypass it. Fails closed — errors mean 404.
- The "sure"/"sure" default credentials are deleted. Basic auth now
  activates only when BOTH SIDEKIQ_WEB_USERNAME and
  SIDEKIQ_WEB_PASSWORD are explicitly set, as an optional second
  layer on top of the constraint.
- Documented in .env.example and docs/hosting/docker.md, including
  warnings that the dashboard is break-glass tooling: never manually
  retry SimplefinConnectionUpdateJob (single-use token), and deleting
  jobs does not update the corresponding Sure records.

The super-admin bar's Jobs link is feature-detected via
sidekiq_web_available? and lights up automatically now that the route
exists in production.
2026-07-17 07:00:27 +02:00
0xτensor 0ad1e59165 fix(a11y): add skip-link and aria-current="page" to application layout (#1781)
* fix(a11y): add skip-link and aria-current="page" to application layout

* test(a11y): cover application layout skip-link and #main anchor

* fix(a11y): extend skip-link and #main anchor to settings layout
2026-05-14 21:53:31 +02:00
ghostandJuan José Mata e59235fdc5 feat(statements): add account statement vault (#1753)
* feat(statements): add account statement vault

Add web-only statement uploads, account linking, duplicate detection, and per-account coverage/reconciliation checks without mutating transactions. Extend ActiveStorage authorization and targeted tests for family/account scoping.

* fix(statements): return deleted account statements to inbox

Preserve linked statement records when an account is deleted by moving them back to the unmatched inbox, then expand coverage for upload validation, sanitized parser metadata, unavailable reconciliation, and missing-month coverage.

* fix(statements): harden vault upload review flows

Address review and security findings in the statement vault by preserving sanitized parser metadata, failing closed on orphaned statement blobs, avoiding account_id mass assignment permits, and adding regression coverage for link/delete edge cases.

* fix(statements): harden vault upload and access controls

* fix(statements): address vault hardening review

* fix(statements): address vault review feedback

Prioritize SHA-256 duplicate detection while preserving MD5 fallback for legacy rows.

Remove free-form account notes from statement matching, document direct account-destroy unlinking, and add year-selectable historical coverage with muted out-of-range months.

* fix(statements): harden vault review follow-ups

Clarify legacy MD5 checksum use, whitelist statement balance helper dispatch, and preserve sanitized parser metadata.

Hide statement management controls from read-only viewers while keeping server-side authorization unchanged.

* fix(statements): repair settings system coverage

Allow the changelog provider lookup in the self-hosting settings system test, include Statement Vault in settings navigation coverage, and align the feature title casing. Update the devcontainer so ActiveStorage and parallel system tests can run in the documented environment.

* fix(statements): move vault beside accounts

Place Statement Vault with account settings instead of between Imports and Exports. Keep settings footer ordering and system navigation coverage aligned, including the non-admin visibility guard.

* fix(statements): address vault review cleanup

Resolve CodeRabbit review feedback for statement upload validation, duplicate race handling, account statement matching semantics, metadata detection, ActiveStorage authorization tests, and small UI/style cleanups.

* fix(statements): address vault cleanup review

* fix(statements): deduplicate vault style helpers

* fix(statements): close vault review follow-ups

* fix(statements): refresh schema after upstream rebase

* fix(statements): process vault uploads sequentially

* fix(statements): close vault review follow-ups

* fix(statements): scope vault index to accessible accounts

* fix(statements): harden statement vault readiness

Squash the statement vault migration hardening into the feature migration, tighten Active Storage authorization edge cases, bound CSV metadata detection, and add real PDF fixture coverage for stored statements.

Validation: targeted statement/auth/controller/provider tests, full Rails suite, system tests, RuboCop, Biome, Brakeman, Zeitwerk, importmap audit, npm audit, ERB lint, CodeRabbit, and Codex Security all passed locally.

* fix(statements): close vault review follow-ups

Move statement unlinking to after account destroy commit, keep Kraken account creation on the shared crypto helper, and add statement metadata length limits with DB checks.

Validation: fresh devcontainer with fresh DB via db:prepare, focused account/statement/Kraken/Binance tests, RuboCop, Brakeman, Zeitwerk, git diff --check, CodeRabbit, and Codex Security passed before commit.

* fix(statements): address vault scan follow-ups

Move statement tab data setup out of the ERB partial, harden reconciliation labels and coverage initialization, and tighten statement schema constraints.

Validation: CodeRabbit and Codex Security reviewed the current PR diff; Rails focused tests, full Rails tests, system tests, RuboCop, Brakeman, Zeitwerk, ERB lint, npm lint, importmap audit, npm audit, and git diff --check passed.

* fix(statements): defer vault tab loading

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-05-13 21:05:11 +02:00
Ellion BlessanandJuan José Mata 98ae6782dc feat(transaction): add support for file attachments using Active Storage (#713)
* feat(transaction): add support for file attachments using Active Storage

* feat(attachments): implement transaction attachments with upload, show, and delete functionality

* feat(attachments): enhance attachment upload functionality to support multiple files and improved error handling

* feat(attachments): add attachment upload form and display functionality in transaction views

* feat(attachments): implement attachment validation for count, size, and content type; enhance upload form with validation hints

* fix(attachments): use correct UI components

* feat(attachments): Implement Turbo Stream responses for creating and deleting transaction attachments.

* fix(attachments): include auth in activestorage controller

* test(attachments): add test coverage for turbostream and auth

* feat(attachments): extract strings to i18n

* fix(attachments): ensure only newly added attachments are purged when transaction validation fails.

* fix(attachments): validate attachment params

* refactor(attachments): use stimulus declarative actions

* fix(attachments): add auth for other representations

* refactor(attachments): use Browse component for attachment uploads

* fix(attachments): reject empty values on attachment upload

* fix(attachments): hide the upload form if reached max uploads

* fix(attachments): correctly purge only newly added attachments on upload failure

* fix(attachments): ensure attachment count limit is respected within a transaction lock

* fix(attachments): update attachment parameter handling to avoid `ParameterMissing` errors.

* fix(components): adjust icon_only logic for buttonish

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-03-14 23:56:27 +01:00
Juan José MataandClaude ef4f5f7b8b feat: CORS support (#813)
* feat: Add CORS support for Flutter mobile client

Add rack-cors gem and configure CORS for API and OAuth endpoints
to enable cross-origin requests from mobile clients and other
external applications.

https://claude.ai/code/session_01RJ6MKLkjBv7x5AQLEUn8AF

* feat: Add /sessions/* to CORS for webview authentication

Enable CORS for session endpoints to support webview-based
authentication flows in the Flutter mobile client.

https://claude.ai/code/session_01RJ6MKLkjBv7x5AQLEUn8AF

* test: Add integration tests for CORS configuration

Test that CORS middleware is configured and returns proper headers
for API, OAuth, and session endpoints including preflight requests.

https://claude.ai/code/session_01RJ6MKLkjBv7x5AQLEUn8AF

* Gemfile.lock

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-28 17:25:02 +01:00
Juan José Mata 5706280dd7 More rebranding changes (#159)
* Replace Maybe for Sure in select code areas

* Make sure passwords are consistent

* Remove (admin|member) from demo data first name

* Database and schema names finally to `sure`

* Fix broken test

* Another (benchmarking) database name to `sure_*`

* More rebranding to Sure

* Missed this Maybe mention in the same page

* Random nitpicks and more Maybes

* Demo data accounts and more Maybes

* Test data account updates

* Impersonation test accounts

* Consistency with `compose.example.yml`
2025-09-24 00:19:51 +02:00
Juan José Mata 099425d240 First quick Sure rebrand (#74)
* First cut of smallest rebrand, pending icons

* Leave SQL schema tokens/user names the same for now

* First batch of logos

* Release notes/what's new

* /releases missing

* redirect_uri for sureapp://

* Padded logo

* Test the correct /releases URL

* Missed a few mobile URIs

* Some icons/asssets from /website/ repo

* Seed/sample data user @sure.local now

* New screenshot

* Want to keep their legal "boilerplate" from the upstream repo
2025-08-05 23:35:01 +02:00
Josh PigfordandClaude cba0bdf0e2 Fix OAuth mobile app support with custom URL schemes
- Configure Doorkeeper to allow custom URL schemes (maybeapp://)
- Disable force_ssl_in_redirect_uri to support non-HTTPS schemes
- Add custom Doorkeeper views with mobile OAuth detection
- Disable Turbo for mobile OAuth flows to prevent redirect interference
- Add display parameter preservation through OAuth flow
- Create custom Doorkeeper layouts with proper styling
- Add comprehensive integration tests for mobile OAuth flows
- Ensure all OAuth pages use proper doorkeeper/application layout

This allows the mobile app to complete OAuth authorization flows
without the web app interfering with custom URL scheme redirects.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-18 05:38:23 -05:00
Josh PigfordandClaude b803ddac96 Add comprehensive API v1 with OAuth and API key authentication (#2389)
* OAuth

* Add API test routes and update Doorkeeper token handling for test environment

- Introduced API namespace with test routes for controller testing in the test environment.
- Updated Doorkeeper configuration to allow fallback to plain tokens in the test environment for easier testing.
- Modified schema to change resource_owner_id type from bigint to string.

* Implement API key authentication and enhance access control

- Replaced Doorkeeper OAuth authentication with a custom method supporting both OAuth and API keys in the BaseController.
- Added methods for API key authentication, including validation and logging.
- Introduced scope-based authorization for API keys in the TestController.
- Updated routes to include API key management endpoints.
- Enhanced logging for API access to include authentication method details.
- Added tests for API key functionality, including validation, scope checks, and access control enforcement.

* Add API key rate limiting and usage tracking

- Implemented rate limiting for API key authentication in BaseController.
- Added methods to check rate limits, render appropriate responses, and include rate limit headers in responses.
- Updated routes to include a new usage resource for tracking API usage.
- Enhanced tests to verify rate limit functionality, including exceeding limits and per-key tracking.
- Cleaned up Redis data in tests to ensure isolation between test cases.

* Add Jbuilder for JSON rendering and refactor AccountsController

- Added Jbuilder gem for improved JSON response handling.
- Refactored index action in AccountsController to utilize Jbuilder for rendering JSON.
- Removed manual serialization of accounts and streamlined response structure.
- Implemented a before_action in BaseController to enforce JSON format for all API requests.

* Add transactions resource to API routes

- Added routes for transactions, allowing index, show, create, update, and destroy actions.
- This enhancement supports comprehensive transaction management within the API.

* Enhance API authentication and onboarding handling

- Updated BaseController to skip onboarding requirements for API endpoints and added manual token verification for OAuth authentication.
- Improved error handling and logging for invalid access tokens.
- Introduced a method to set up the current context for API requests, ensuring compatibility with session-like behavior.
- Excluded API paths from onboarding redirects in the Onboardable concern.
- Updated database schema to change resource_owner_id type from bigint to string for OAuth access grants.

* Fix rubocop offenses

- Fix indentation and spacing issues
- Convert single quotes to double quotes
- Add spaces inside array brackets
- Fix comment alignment
- Add missing trailing newlines
- Correct else/end alignment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix API test failures and improve test reliability

- Fix ApiRateLimiterTest by removing mock users method and using fixtures
- Fix UsageControllerTest by removing mock users method and using fixtures
- Fix BaseControllerTest by using different users for multiple API keys
- Use unique display_key values with SecureRandom to avoid conflicts
- Fix double render issue in UsageController by returning after authorize_scope\!
- Specify controller name in routes for usage resource
- Remove trailing whitespace and empty lines per Rubocop

All tests now pass and linting is clean.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add API transactions controller warning to brakeman ignore

The account_id parameter in the API transactions controller is properly
validated on line 79: family.accounts.find(transaction_params[:account_id])
This ensures users can only create transactions in accounts belonging to
their family, making this a false positive.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Signed-off-by: Josh Pigford <josh@joshpigford.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-06-17 15:57:05 -05:00
Zach Gollwitzer 45ae4a9737 CSV Transaction Imports (#708)
Introduces a basic CSV import module for bulk-importing account transactions.

Changes include:

- User can load a CSV
- User can configure the column mappings for a CSV
- Imported CSV shows invalid cells
- User can clean up their data directly in the UI
- User can see a preview of the import rows and confirm import
- Layout refactor + Import nav stepper
- System test stability improvements
2024-05-17 09:09:32 -04:00
Josh Pigford 99de24ac70 Initial commit 2024-02-02 09:05:04 -06:00