Skip to content

security: sanitize exception messages in v1 API responses (FIX-11)#1521

Merged
jjmata merged 2 commits intowe-promise:mainfrom
dgilperez:security/pr6-authz-api-errors
Apr 19, 2026
Merged

security: sanitize exception messages in v1 API responses (FIX-11)#1521
jjmata merged 2 commits intowe-promise:mainfrom
dgilperez:security/pr6-authz-api-errors

Conversation

@dgilperez
Copy link
Copy Markdown

@dgilperez dgilperez commented Apr 19, 2026

Summary

Smallest PR of the series — a single finding addressed across several v1 controllers.

Part of the security hardening originally bundled in #1104 (closed), now split by functional area per @jjmata's feedback. Companion PRs: #1516 #1517 #1518 #1519 #1520.

Finding addressed

FIX-11 (MED, CWE-209) — Raw exception messages leaked in API error responses. Replaced e.message bodies with generic user-facing strings; the details are logged server-side for debugging.

Files touched:

  • app/controllers/api/v1/{accounts,auth,categories,holdings,sync,trades,transactions,valuations}_controller.rb
  • app/controllers/settings/providers_controller.rb (sanitized flash + preserved logger)
  • app/controllers/webhooks_controller.rb

Note on FIX-12 (skipped)

The original #1104 also addressed FIX-12 (invite codes missing admin authorization). Upstream main already covers this with a stricter ensure_super_admin before_action on invite_codes_controller.rb (tighter than the original patch). Skipped to preserve the current policy.

Tests

  • test/controllers/settings/providers_controller_test.rb updated for sanitized flash.
  • Full suite: 3225 runs, 0 failures.
  • bin/rubocop clean, bin/brakeman 0 warnings.

Out of scope

Related

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Standardized error messages across API endpoints for improved consistency.
    • Enhanced device registration validation to reject invalid device types.
    • Improved error handling in authentication flow to prevent partial data commits.
  • Improvements

    • Refined webhook error responses for better reliability.
    • Strengthened provider settings update error handling.

Replace raw e.message/error.message interpolations in response bodies
with generic error strings, and log class+message server-side. Prevents
leaking internal exception details (stack traces, SQL fragments, record
data) to API clients.

Covers:
- API v1 accounts, categories (index/show), holdings, sync, trades,
  transactions (index/show/create/update/destroy), valuations
  (show/create/update): replace "Error: #{e.message}" with
  "An unexpected error occurred".
- API v1 auth: device-registration rescue paths now log
  "[Auth] Device registration failed: ..." and respond with
  "Failed to register device".
- WebhooksController#plaid and #plaid_eu: log full error and respond
  with "Invalid webhook".
- Settings::ProvidersController: generic user-facing flash alert,
  detailed log line with error class + message.

Updates providers_controller_test assertion to match sanitized flash.
@brin-security-scanner brin-security-scanner Bot added the contributor:verified Contributor passed trust analysis. label Apr 19, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

This PR replaces detailed exception messages returned to users with generic messages across multiple controllers and tightens device-registration validation and error handling in the API auth flow, while preserving internal logging and status codes.

Changes

Cohort / File(s) Summary
API generic error responses
app/controllers/api/v1/accounts_controller.rb, app/controllers/api/v1/categories_controller.rb, app/controllers/api/v1/holdings_controller.rb, app/controllers/api/v1/sync_controller.rb, app/controllers/api/v1/trades_controller.rb, app/controllers/api/v1/transactions_controller.rb, app/controllers/api/v1/valuations_controller.rb
Replaced interpolated exception messages in rescue handlers (e.g., "Error: #{e.message}") with a fixed "An unexpected error occurred" message; logging and HTTP status codes remain unchanged.
Auth: device registration & validation
app/controllers/api/v1/auth_controller.rb
Wrapped signup device/token operations in a DB transaction; added upfront device validation (MobileDevice.new(...).valid?) and stricter blank-field checks; changed ActiveRecord::RecordInvalid handling to log a generic "Failed to register device" message and return a generic error payload (no exception text).
Settings & webhooks error messaging
app/controllers/settings/providers_controller.rb, app/controllers/webhooks_controller.rb
Now logs exception class with message and returns generic user-facing messages ("Failed to update provider settings. Please try again.", "Invalid webhook") instead of embedding exception text; control flow unchanged.
Tests: expectations updated / new auth test
test/controllers/settings/providers_controller_test.rb, test/controllers/api/v1/auth_controller_test.rb
Updated provider-settings failure test to expect generic flash and tightened logger expectation; added auth signup test asserting invalid device_type (e.g., "windows") returns 400 and makes no DB/token changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client as Client
  participant AuthCtrl as Api::V1::AuthController
  participant DB as Database
  participant Invite as InviteCode
  participant Device as MobileDeviceService
  participant Token as TokenIssuer

  rect rgba(200,200,255,0.5)
  Client->>AuthCtrl: POST /api/v1/auth/signup (user + device params)
  AuthCtrl->>DB: ActiveRecord transaction begin
  AuthCtrl->>DB: create user (user.save)
  DB-->>AuthCtrl: user saved
  AuthCtrl->>Invite: InviteCode.claim!(code)
  Invite-->>AuthCtrl: claimed
  AuthCtrl->>Device: MobileDevice.upsert_device!(params)
  Device-->>AuthCtrl: device record
  AuthCtrl->>Token: device.issue_token!
  Token-->>AuthCtrl: token_response
  AuthCtrl->>DB: ActiveRecord transaction commit
  AuthCtrl-->>Client: 201 Created + token_response
  end

  rect rgba(255,200,200,0.5)
  Note over Device,Token: On Device/Token failure (ActiveRecord::RecordInvalid)
  Device-->>AuthCtrl: raises RecordInvalid
  AuthCtrl->>AuthCtrl: rescue RecordInvalid
  AuthCtrl->>Rails.logger: log "[Auth] Device registration failed: <class> - <message>"
  AuthCtrl-->>Client: 400 Bad Request { error: "Failed to register device" }
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • jjmata
  • dwvwdv

Poem

🐰
I nibble bits of error lore,
Hide the messages behind the door.
Logs keep secrets safe and sound,
Users see a calm safe ground.
Hoppity-hop — the change is done!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main objective: sanitizing exception messages in v1 API responses for security (FIX-11). It clearly identifies the primary change across multiple API controllers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@brin-security-scanner brin-security-scanner Bot added the pr:verified PR passed security analysis. label Apr 19, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/controllers/api/v1/auth_controller.rb (1)

48-60: ⚠️ Potential issue | 🟠 Major

Make device registration failures rollback prior auth side effects.

Line 56 and Line 377 can return 422 after callers have already committed signup/SSO mutations. In signup, the user/family and invite claim happen before device registration; in SSO account creation, the user, invitation, and OIDC identity are committed before issue_mobile_tokens. Since valid_device_info? only checks presence and MobileDevice can still raise ActiveRecord::RecordInvalid for invalid attributes like device_type, clients can receive "Failed to register device" while the account/link was actually created. Wrap those auth mutations plus device/token issuance in one transaction, or validate device params against model constraints before any mutation.

Also applies to: 213-225, 371-379

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/api/v1/auth_controller.rb` around lines 48 - 60, The device
registration can fail after user/family/invite/identity changes have been
committed (see signup and issue_mobile_tokens paths), causing partial account
creation; fix by wrapping the related mutations and device/token creation in a
single transaction (e.g., wrap the user creation/invite claim/identity creation
plus MobileDevice.upsert_device! and device.issue_token! in one ActiveRecord
transaction) so any ActiveRecord::RecordInvalid from MobileDevice.upsert_device!
or device.issue_token! will rollback prior changes; alternatively (or in
addition) validate device params up-front using MobileDevice validations (call a
validator or MobileDevice.new(device_params).valid?) before committing
invites/user/identity to avoid raising after commit. Ensure changes reference
InviteCode.claim!, MobileDevice.upsert_device!, device.issue_token!, signup and
issue_mobile_tokens flows mentioned in the review.
🧹 Nitpick comments (1)
test/controllers/settings/providers_controller_test.rb (1)

273-274: Tighten the log expectation to cover the exception class.

The comment says the log includes class and message, but the regex would still pass if StandardError - were removed.

Proposed test tightening
-      Rails.logger.expects(:error).with(regexp_matches(/Failed to update provider settings.*Database error/)).once
+      Rails.logger.expects(:error).with(regexp_matches(/Failed to update provider settings: StandardError - Database error/)).once
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/controllers/settings/providers_controller_test.rb` around lines 273 -
274, The test currently expects Rails.logger.error to be called with /Failed to
update provider settings.*Database error/ which would pass if the exception
class is missing; update the Rails.logger.expects in
providers_controller_test.rb to require the exception class name as well (e.g.
tighten the regex to include " - ActiveRecord::RecordInvalid" or the specific
exception your controller raises) so the expectation verifies both the error
message and the exception class are logged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/controllers/api/v1/auth_controller.rb`:
- Around line 48-60: The device registration can fail after
user/family/invite/identity changes have been committed (see signup and
issue_mobile_tokens paths), causing partial account creation; fix by wrapping
the related mutations and device/token creation in a single transaction (e.g.,
wrap the user creation/invite claim/identity creation plus
MobileDevice.upsert_device! and device.issue_token! in one ActiveRecord
transaction) so any ActiveRecord::RecordInvalid from MobileDevice.upsert_device!
or device.issue_token! will rollback prior changes; alternatively (or in
addition) validate device params up-front using MobileDevice validations (call a
validator or MobileDevice.new(device_params).valid?) before committing
invites/user/identity to avoid raising after commit. Ensure changes reference
InviteCode.claim!, MobileDevice.upsert_device!, device.issue_token!, signup and
issue_mobile_tokens flows mentioned in the review.

---

Nitpick comments:
In `@test/controllers/settings/providers_controller_test.rb`:
- Around line 273-274: The test currently expects Rails.logger.error to be
called with /Failed to update provider settings.*Database error/ which would
pass if the exception class is missing; update the Rails.logger.expects in
providers_controller_test.rb to require the exception class name as well (e.g.
tighten the regex to include " - ActiveRecord::RecordInvalid" or the specific
exception your controller raises) so the expectation verifies both the error
message and the exception class are logged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3450265-34da-4df9-b530-eadbc567da2d

📥 Commits

Reviewing files that changed from the base of the PR and between b32e9db and 90ae605.

📒 Files selected for processing (11)
  • app/controllers/api/v1/accounts_controller.rb
  • app/controllers/api/v1/auth_controller.rb
  • app/controllers/api/v1/categories_controller.rb
  • app/controllers/api/v1/holdings_controller.rb
  • app/controllers/api/v1/sync_controller.rb
  • app/controllers/api/v1/trades_controller.rb
  • app/controllers/api/v1/transactions_controller.rb
  • app/controllers/api/v1/valuations_controller.rb
  • app/controllers/settings/providers_controller.rb
  • app/controllers/webhooks_controller.rb
  • test/controllers/settings/providers_controller_test.rb

Major — partial-commit on device registration failure:
- Strengthened valid_device_info? to also run MobileDevice's model
  validations up-front (device_type inclusion, attribute presence), not
  just a flat "are the keys present?" check. A client that sends a bad
  device_type ("windows", etc.) is now rejected at the API boundary
  BEFORE signup commits any user/family/invite state.
- Wrapped the signup path (user.save + InviteCode.claim + MobileDevice
  upsert + token issuance) in ActiveRecord::Base.transaction. A
  post-save RecordInvalid from device registration (e.g., racing
  uniqueness on device_id) now rolls back the user/invite/family so
  clients don't see a partial-account state.
- Rescue branch logs the exception class + message ("#{e.class} - #{e.message}")
  for better postmortem debugging, matching the providers controller
  pattern.

Nit:
- Tightened providers_controller_test log expectation regex to assert on
  both the exception class name AND the message ("StandardError - Database
  error"), so a regression that drops either still fails the test.

Tests:
- New: "should reject signup with invalid device_type before committing
  any state" — POST /api/v1/auth/signup with device_type="windows"
  returns 400 AND asserts no User, MobileDevice, or Doorkeeper::AccessToken
  row was created.

Note on SSO path (sso_exchange → issue_mobile_tokens, lines 173/225): the
device_info in those flows comes from Rails.cache (populated by an earlier
request that already passed valid_device_info?), so the pre-validation
covers it indirectly. Wrapping the full SSO account creation (user +
invitation + OidcIdentity + issue_mobile_tokens) in one transaction would
be a meaningful architectural cleanup but is out of scope for this
error-hygiene PR — filed it as a mental note for a follow-up.
@dgilperez
Copy link
Copy Markdown
Author

Thanks — addressed both points, with one caveat on scope.

Major — partial-commit on device registration failure (auth_controller.rb:48-60, 213-225, 371-379): Addressed materially. Two changes:

  1. Pre-validation — strengthened valid_device_info? to call MobileDevice.new(device_params).valid? and surface attribute-level errors (e.g., device_type must be ios/android/web). A misconfigured client that was previously only caught after MobileDevice.upsert_device! raised RecordInvalid (post-commit) is now rejected at the API boundary with 400 BEFORE any user.save. Skips the :user belongs_to error and uniqueness (both are unavoidable without a user context; upsert_device! treats device_id collisions as updates anyway).

  2. Transaction wrap in signupuser.save + InviteCode.claim! + MobileDevice.upsert_device! + device.issue_token! now run inside ActiveRecord::Base.transaction. Any remaining post-save RecordInvalid (e.g., genuine racing uniqueness) rolls back the user/family/invite; clients get a clean 422 with no dangling state.

New test: "should reject signup with invalid device_type before committing any state" posts with device_type: "windows" and asserts 400 AND zero change in User.count + MobileDevice.count + Doorkeeper::AccessToken.count.

Scope caveat: the SSO path (sso_exchangeissue_mobile_tokens at lines 173/225) has the same architectural concern — user.saveinvitation.update!OidcIdentity.create_from_omniauthSsoAuditLogissue_mobile_tokens all run sequentially without a wrapping transaction. The pre-validation change covers the main practical failure mode (bad device_type) because issue_mobile_tokens reads device_info from Rails.cache, which was populated by an earlier request that already passed valid_device_info?. But a true fix would refactor issue_mobile_tokens to stop rendering inline and wrap the whole SSO account-creation flow in one transaction. That's a meaningful refactor and I'd rather land it as a targeted follow-up than expand this PR's scope beyond API error hygiene.

Nit — test regex tightened (providers_controller_test.rb:273): Changed /Failed to update provider settings.*Database error//Failed to update provider settings: StandardError - Database error/ so a regression that drops either the class name or the message still fails the test.

All changes in commit 028f24cd. Full suite: 3231 runs, 0 failures. Rubocop + brakeman clean.

— wolfgang 🤖

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
app/controllers/api/v1/auth_controller.rb (3)

98-98: Minor: log format inconsistent with the signup branch.

Line 64 logs "#{e.class} - #{e.message}", while Line 98 and Line 395 only log e.message. Since the PR objective specifically calls out "logs exceptions including class + message for server-side debugging", aligning these two call sites with the signup format would make server-side triage consistent across all three device-registration paths.

♻️ Proposed tweak
-            Rails.logger.error("[Auth] Device registration failed: #{e.message}")
+            Rails.logger.error("[Auth] Device registration failed: #{e.class} - #{e.message}")

Apply at both Line 98 (login) and Line 395 (issue_mobile_tokens).

Also applies to: 395-395

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/api/v1/auth_controller.rb` at line 98, Update the
Rails.logger.error calls in the login and issue_mobile_tokens flows to match the
signup branch format by including exception class and message; locate the
Rails.logger.error("[Auth] Device registration failed: #{e.message}") call
inside the login method and the analogous logger in issue_mobile_tokens and
change the log payload to include "#{e.class} - #{e.message}" so all three
device-registration paths (signup, login, issue_mobile_tokens) consistently log
exception class and message.

93-101: Consider the same transaction wrap for login (and issue_mobile_tokens) to avoid partial-state token revocation.

MobileDevice#issue_token! (see app/models/mobile_device.rb:69-88) calls revoke_all_tokens! before Doorkeeper::AccessToken.create!. If create! raises ActiveRecord::RecordInvalid — which is exactly what this rescue block catches — the user's existing tokens on this device have already been revoked with no replacement issued, and the client only sees a generic 422. On login (and SSO link/create flows routed through issue_mobile_tokens on Lines 388–397), this can effectively lock the user out of active mobile sessions on a transient failure.

This is the same class of partial-commit bug the signup change addresses; wrapping device upsert + token issuance in ActiveRecord::Base.transaction would roll back the revoke_all_tokens! side effect on failure and keep the three code paths consistent.

♻️ Example for the `login` path
-          # Create device and OAuth token
-          begin
-            device = MobileDevice.upsert_device!(user, device_params)
-            token_response = device.issue_token!
-          rescue ActiveRecord::RecordInvalid => e
-            Rails.logger.error("[Auth] Device registration failed: #{e.message}")
-            render json: { error: "Failed to register device" }, status: :unprocessable_entity
-            return
-          end
+          # Create device and OAuth token atomically so a token-create failure
+          # does not leave the device with all prior tokens revoked.
+          token_response = nil
+          begin
+            ActiveRecord::Base.transaction do
+              device = MobileDevice.upsert_device!(user, device_params)
+              token_response = device.issue_token!
+            end
+          rescue ActiveRecord::RecordInvalid => e
+            Rails.logger.error("[Auth] Device registration failed: #{e.class} - #{e.message}")
+            render json: { error: "Failed to register device" }, status: :unprocessable_entity
+            return
+          end

The same pattern applies to issue_mobile_tokens on Lines 388–397.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/api/v1/auth_controller.rb` around lines 93 - 101, The login
(and issue_mobile_tokens) flow can leave device tokens revoked if
MobileDevice#issue_token! calls revoke_all_tokens! before creating a new token
and the subsequent Doorkeeper::AccessToken.create! raises
ActiveRecord::RecordInvalid; wrap the device upsert and token issuance in a
single DB transaction to guarantee rollback on failure. Concretely, in the login
action (and in the method that calls issue_mobile_tokens) surround the calls to
MobileDevice.upsert_device!(...) and device.issue_token! with
ActiveRecord::Base.transaction do ... end so that revoke_all_tokens! is rolled
back if token creation fails; ensure you reference MobileDevice.upsert_device!,
MobileDevice#issue_token!, revoke_all_tokens!, and
ActiveRecord::Base.transaction when making the change.

329-334: Nit: prefer the public errors enumerable over the internal errors.errors accessor.

ActiveModel::Errors is itself enumerable and yields ActiveModel::Error objects, so you can skip the .errors.errors indirection. Purely cosmetic — behavior is unchanged.

♻️ Proposed tweak
-          preview = MobileDevice.new(device_params)
-          preview.valid?
-          relevant_errors = preview.errors.errors.reject do |err|
-            err.type == :taken || err.attribute == :user
-          end
-          relevant_errors.empty?
+          preview = MobileDevice.new(device_params)
+          preview.valid?
+          preview.errors.none? { |err| err.type != :taken && err.attribute != :user }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/api/v1/auth_controller.rb` around lines 329 - 334, Replace
the internal errors.errors access with the public enumerable on
ActiveModel::Errors: when building the preview (MobileDevice.new(device_params))
iterate over preview.errors (not preview.errors.errors) to filter out entries
with type == :taken or attribute == :user and then check emptiness (update the
use of relevant_errors to call preview.errors.reject { |err| ... } so behavior
stays identical but uses the public API).
test/controllers/api/v1/auth_controller_test.rb (1)

97-114: Consider also asserting the generic error body.

The test covers the no-state-change contract well. A small enhancement: the neighboring test at Lines 80–95 asserts response_data["error"], and this PR is explicitly about keeping error messages generic. Adding a body assertion here would guard against a regression where valid_device_info? starts leaking e.message (e.g. "Device type is not included in the list") to the client.

♻️ Proposed addition
     assert_response :bad_request
+    response_data = JSON.parse(response.body)
+    assert_equal "Device information is required", response_data["error"]
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/controllers/api/v1/auth_controller_test.rb` around lines 97 - 114, Add
an assertion that inspects the JSON response body (e.g. parse response into
response_data) and verifies the error field is the generic client-facing message
rather than the raw exception: after the post to "/api/v1/auth/signup" in the
test "should reject signup with invalid device_type before committing any
state", assert response_data["error"] is present and assert_not_match(/is not
included/, response_data["error"]) (or match the same generic error string used
by the neighboring test that checks response_data["error"]) to guard against
leaking e.message from valid_device_info?.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/controllers/api/v1/auth_controller.rb`:
- Line 98: Update the Rails.logger.error calls in the login and
issue_mobile_tokens flows to match the signup branch format by including
exception class and message; locate the Rails.logger.error("[Auth] Device
registration failed: #{e.message}") call inside the login method and the
analogous logger in issue_mobile_tokens and change the log payload to include
"#{e.class} - #{e.message}" so all three device-registration paths (signup,
login, issue_mobile_tokens) consistently log exception class and message.
- Around line 93-101: The login (and issue_mobile_tokens) flow can leave device
tokens revoked if MobileDevice#issue_token! calls revoke_all_tokens! before
creating a new token and the subsequent Doorkeeper::AccessToken.create! raises
ActiveRecord::RecordInvalid; wrap the device upsert and token issuance in a
single DB transaction to guarantee rollback on failure. Concretely, in the login
action (and in the method that calls issue_mobile_tokens) surround the calls to
MobileDevice.upsert_device!(...) and device.issue_token! with
ActiveRecord::Base.transaction do ... end so that revoke_all_tokens! is rolled
back if token creation fails; ensure you reference MobileDevice.upsert_device!,
MobileDevice#issue_token!, revoke_all_tokens!, and
ActiveRecord::Base.transaction when making the change.
- Around line 329-334: Replace the internal errors.errors access with the public
enumerable on ActiveModel::Errors: when building the preview
(MobileDevice.new(device_params)) iterate over preview.errors (not
preview.errors.errors) to filter out entries with type == :taken or attribute ==
:user and then check emptiness (update the use of relevant_errors to call
preview.errors.reject { |err| ... } so behavior stays identical but uses the
public API).

In `@test/controllers/api/v1/auth_controller_test.rb`:
- Around line 97-114: Add an assertion that inspects the JSON response body
(e.g. parse response into response_data) and verifies the error field is the
generic client-facing message rather than the raw exception: after the post to
"/api/v1/auth/signup" in the test "should reject signup with invalid device_type
before committing any state", assert response_data["error"] is present and
assert_not_match(/is not included/, response_data["error"]) (or match the same
generic error string used by the neighboring test that checks
response_data["error"]) to guard against leaking e.message from
valid_device_info?.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad841bab-9a8b-4e4b-86b7-91c6b34720d7

📥 Commits

Reviewing files that changed from the base of the PR and between 90ae605 and 028f24c.

📒 Files selected for processing (3)
  • app/controllers/api/v1/auth_controller.rb
  • test/controllers/api/v1/auth_controller_test.rb
  • test/controllers/settings/providers_controller_test.rb

@jjmata jjmata added this to the v0.7.0 milestone Apr 19, 2026
@jjmata jjmata merged commit 7f17fbf into we-promise:main Apr 19, 2026
9 checks passed
@dgilperez dgilperez deleted the security/pr6-authz-api-errors branch April 19, 2026 16:41
@dgilperez
Copy link
Copy Markdown
Author

🍻

binarygituser pushed a commit to binarygituser/sure-dev that referenced this pull request Apr 26, 2026
…e-promise#1521)

* fix(security): sanitize exception messages in API responses (FIX-11)

Replace raw e.message/error.message interpolations in response bodies
with generic error strings, and log class+message server-side. Prevents
leaking internal exception details (stack traces, SQL fragments, record
data) to API clients.

Covers:
- API v1 accounts, categories (index/show), holdings, sync, trades,
  transactions (index/show/create/update/destroy), valuations
  (show/create/update): replace "Error: #{e.message}" with
  "An unexpected error occurred".
- API v1 auth: device-registration rescue paths now log
  "[Auth] Device registration failed: ..." and respond with
  "Failed to register device".
- WebhooksController#plaid and #plaid_eu: log full error and respond
  with "Invalid webhook".
- Settings::ProvidersController: generic user-facing flash alert,
  detailed log line with error class + message.

Updates providers_controller_test assertion to match sanitized flash.

* fix(security): address CodeRabbit review

Major — partial-commit on device registration failure:
- Strengthened valid_device_info? to also run MobileDevice's model
  validations up-front (device_type inclusion, attribute presence), not
  just a flat "are the keys present?" check. A client that sends a bad
  device_type ("windows", etc.) is now rejected at the API boundary
  BEFORE signup commits any user/family/invite state.
- Wrapped the signup path (user.save + InviteCode.claim + MobileDevice
  upsert + token issuance) in ActiveRecord::Base.transaction. A
  post-save RecordInvalid from device registration (e.g., racing
  uniqueness on device_id) now rolls back the user/invite/family so
  clients don't see a partial-account state.
- Rescue branch logs the exception class + message ("#{e.class} - #{e.message}")
  for better postmortem debugging, matching the providers controller
  pattern.

Nit:
- Tightened providers_controller_test log expectation regex to assert on
  both the exception class name AND the message ("StandardError - Database
  error"), so a regression that drops either still fails the test.

Tests:
- New: "should reject signup with invalid device_type before committing
  any state" — POST /api/v1/auth/signup with device_type="windows"
  returns 400 AND asserts no User, MobileDevice, or Doorkeeper::AccessToken
  row was created.

Note on SSO path (sso_exchange → issue_mobile_tokens, lines 173/225): the
device_info in those flows comes from Rails.cache (populated by an earlier
request that already passed valid_device_info?), so the pre-validation
covers it indirectly. Wrapping the full SSO account creation (user +
invitation + OidcIdentity + issue_mobile_tokens) in one transaction would
be a meaningful architectural cleanup but is out of scope for this
error-hygiene PR — filed it as a mental note for a follow-up.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Development

Successfully merging this pull request may close these issues.

2 participants