Skip to content

fix(bank-sync): treat provider data as untrusted and keep secrets out of logs - #474

Merged
otherworld-dev merged 1 commit into
devfrom
fix/bank-sync-hardening
Nov 3, 2026
Merged

otherworld-dev merged 1 commit into
devfrom
fix/bank-sync-hardening

Conversation

@otherworld-dev

Copy link
Copy Markdown
Owner

Hardening for the existing bank-sync providers (GoCardless, SimpleFIN), found while reviewing #454. None of it needs a hostile user; items 1-2 need a hostile or compromised upstream, so all low severity.

  1. Provider data in Account Mappings. renderMappings() put lastCurrency/lastBalance from the provider into innerHTML unescaped. Now escaped, and the server keeps a currency only if it's a 3-letter code (uppercased) and a balance only if numeric, through one applyProviderAccountDetails() used by connect, sync and refresh. On a throwaway instance a seeded <img onerror> currency created live elements before (only CSP stopped the handler) and renders as text after.
  2. Authorization link. Opened with noopener,noreferrer, fallback link gets rel="noopener noreferrer", and the server refuses any link that isn't https:// with a host, before a connection is stored or updated. With noopener a blocked popup can't be detected, so the "click here if it didn't open" line now always shows.
  3. Reset/purge revoke. Factory reset, sample-data clear, user deletion, purge-deleted-users and seed-demo all go through FactoryResetService::reset(), which deleted connections without telling the provider. New BankConnectionRevoker (shared with Disconnect) reads credentials before the delete and revokes best-effort after the commit, so a rolled-back reset keeps working connections and provider calls don't run inside the DB transaction. Failures are logged without credentials and never block the reset.
  4. Credentials in log traces on PHP 8.1. #[\SensitiveParameter] is ignored on 8.1, and logError() attached the exception, whose trace can hold credential arguments. BankSyncController now uses handleErrorWithoutTrace() (logs class + file:line, same response), and catches \Throwable so a provider TypeError doesn't reach Nextcloud's own full-trace logger (that turns such a 500 into the usual 400 with the generic message).

Tests: new Vitest (escaping, https check, noopener) and PHPUnit (cleaning, https refusal on connect/reauthorize, revoker, reset ordering/rollback/failure, log context has no secret); the new tests fail on the old code. Full PHPUnit 7570 OK, Vitest 2242, psalm and cs clean. No new strings.

… of logs

- Escape a mapping's last balance and currency in the Account Mappings
  list, and store a currency only when it is a three-letter code and a
  balance only when it is a number.
- Open the bank authorization link with noopener,noreferrer (the fallback
  link carries the same rel), and refuse a link that isn't https on the
  server before the connection is stored or the browser sees it.
- Factory reset, clearing sample data and purging a deleted user now
  revoke each bank connection at the provider once the delete commits,
  through the same path Disconnect uses. Best-effort: failures are logged
  and never stop the reset.
- Bank sync controller errors are logged with the exception's class and
  message only, never the exception object, whose trace carries the
  decrypted credentials on PHP 8.1. Throwable is caught so a TypeError
  doesn't reach Nextcloud's own handler with its trace.
@otherworld-dev
otherworld-dev merged commit 4844d4f into dev Nov 3, 2026
13 checks passed
@otherworld-dev
otherworld-dev deleted the fix/bank-sync-hardening branch November 3, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant