Skip to content

Production readiness: crash logging, startup resilience, proxy hardening, release gates - #8

Merged
robertsolves merged 3 commits into
mainfrom
claude/project-thread-ulzmi8
Nov 3, 2026
Merged

robertsolves merged 3 commits into
mainfrom
claude/project-thread-ulzmi8

Conversation

@robertsolves

@robertsolves robertsolves commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Robert · project thread

What & why

This PR targets main and includes #5 and #6, which are already merged. It doesn't conflict with #7 (welcome dialog) or #9 (orange theme): the crash-log menu item goes under File through a menu contribution, so it can't collide with #7's new Help menu.

I audited the app for things that would hurt real users: security, crash paths, validation, error handling, packaging and CI. Areas the earlier bug sweeps already covered, like the SQL fencing and the AI write-keyword scan, were checked and left alone.

Findings, all fixed here

# Finding Fix
1 Uncaught errors vanished. There was no FlutterError.onError or PlatformDispatcher.onError, so a release build left nothing behind, and a widget that threw while building painted a grey box. installErrorHandlers (lib/util/error_reporting.dart) runs in every isolate. Errors go to the console and to ~/Library/Logs/dbscope/dbscope.log, which rotates at 1 MB. Release builds show a short placeholder that names the log. File ▸ Show crash log opens the log's folder.
2 One bad saved tab could stop every launch. restorePersistedTabs runs before runApp, and a hard as String? cast on a malformed entry threw. Each entry is wrapped in its own try. A new test fails on the old code.
3 One bad saved profile hid all of them. ProfileStore.load threw, the picker came up empty, and the next save would have dropped the entry. Bad entries are skipped and written back unchanged on save.
4 Quit could hang. The quit hooks awaited disconnectAll with no bound, so a dead socket meant the app never quit. Each quit step is capped at 3 s, and a failure is logged.
5 pg proxy crash (DoS). A message with a field of the wrong type ({"type":"open","port":"x"}) threw inside an unawaited session, which killed the proxy for every client. A failed upgrade did the same. Each message and each upgrade is guarded, and the accept loop can't die. Checked live: malformed messages get error replies and new connections still work.
6 pg proxy cross-site WebSocket hijacking. With no --allow-origin (the default), any web page the user opened could reach ws://127.0.0.1:8765 and from there any Postgres on their network. By default the proxy accepts only the hosted app's Origins and localhost pages. Clients that send no Origin are still allowed. --allow-origin is now repeatable and replaces the defaults. --allow-any-origin is an explicit opt-out. Checked live: a foreign Origin gets a 403.
7 pg proxy could reach any host and had no limit on rows. The client picks the database host, so an exposed proxy can be used as a pivot, and one SELECT * could exhaust memory. --allow-db-host host[:port] (repeatable) pins which databases a client may open. --max-rows (default 100000) caps each result, and a capped reply carries "truncated": true. Checked live.
8 pg proxy secret and liveness. The shared secret was compared with !=, dead peers were never dropped, and a bad --port crashed with a raw stack trace. The secret is compared in constant time, sockets are pinged every 30 s, and --port is validated.
9 Read-only mode wasn't enforced on PG Proxy connections. setReadOnly was a no-op, so a profile marked read-only still accepted writes. It now runs SET default_transaction_read_only, the same as a direct connection. Checked live through the proxy: CREATE TABLE fails with 25006.
10 Cleartext by default. New connections defaulted to SSL disable even for remote hosts. suggestSslMode: disable for localhost and require for anything else. The default follows the host field until the user picks a mode. Tested.
11 Weak link passphrases. A .dbscopeconn file contains the database password but could be exported with a 1-character passphrase. Export now needs at least 10 characters. Import still accepts any passphrase, so older files keep working.
12 Self-update could install a tampered release. The checksum and the zip come from the same release, so they only catch a corrupted download. The downloads also had no timeout. When the running app is signed, the update must pass codesign --verify --deep --strict and be signed by the same Developer ID team. Downloads time out after 30 s for metadata and 10 min for the zip.
13 No web security headers. Firebase Hosting now sends X-Frame-Options/frame-ancestors 'none', nosniff, HSTS, Referrer-Policy and Permissions-Policy.
14 Ungated releases. A v* tag and every web deploy built and shipped without running analyze or tests. release.yml and web.yml now run flutter analyze and flutter test on the exact commit they ship.
15 Workflow script injection. ${{ inputs.version }} was spliced straight into a shell script in release.yml. It's passed through an env var instead.

Decided against

  • Moving the PG Proxy shared secret into the keychain. Subwindows have no secure-storage plugin and get the password from the host over RPC, so a keychain-only secret would break proxy connections opened from a subwindow. On web, where this plugin is mostly used, secure storage is browser storage anyway. The reason is noted in a comment in profile_dialog.dart.

Still open (in TODO.md)

  • [P2] Check whether disable-library-validation in Release.entitlements is still needed. That needs a signed build and a Mac.
  • [P2] A Cloud SQL public IP stays assigned if dbscope crashes before it releases it.

How it was tested

  • Linux container with Flutter 3.47.7 and Postgres 16.
  • fvm flutter analyze is clean, and fvm flutter test passes 309 tests. The new test/production_readiness_test.dart covers the SSL default and tab restore; I checked that the restore test fails against the old code.
  • I ran the proxy live: malformed messages, a foreign Origin, a disallowed database host, the row cap and the read-only fence all behaved as described above.
  • Not run: the macOS-only paths (the signature check, the crash log location, Show crash log, quitting), because they need a Mac. CI's macOS job builds the app and checks that it launches. actionlint wasn't run either, because it couldn't be installed here.

Checklist

  • fvm flutter analyze is clean
  • fvm flutter test passes
  • SQL safety invariants in CLAUDE.md still hold (quoted identifiers, parameterised values)
  • No credentials, API keys or .dbscopeconn files in the diff

🤖 Generated with Claude Code

https://claude.ai/code/session_01EqzDdXBJbvRKgCoHNEN8vG

- Log uncaught errors to ~/Library/Logs/dbscope/dbscope.log and show a
  placeholder instead of a grey box for a widget that throws in release.
- Skip unreadable saved tabs and profiles instead of failing startup;
  write unreadable profiles back unchanged.
- Cap each quit step at 3 s so a dead socket can't hang Quit.
- pg proxy: survive malformed messages and failed upgrades, reject
  WebSocket upgrades from foreign Origins by default, compare the shared
  secret in constant time, ping idle sockets, validate --port.
- PG Proxy connections enforce read-only mode in the database.
- New remote connections default to SSL mode require.
- Connection link export needs a 10+ character passphrase.
- Self-update verifies the Developer ID team and times out stalled
  downloads.
- Security headers on Firebase Hosting.
- Release and web deploy run analyze + tests; release input passed via env.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqzDdXBJbvRKgCoHNEN8vG
@robertsolves
robertsolves changed the base branch from claude/project-thread-sroiel to main November 3, 2026 09:25
- dbscope_pg_proxy: --allow-db-host limits which databases a client may
  open; --max-rows (default 100000) caps each result and flags it
  truncated.
- File > Show crash log opens the folder holding dbscope.log (desktop).
- Note why the proxy shared secret stays in the profile, not the keychain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqzDdXBJbvRKgCoHNEN8vG
@robertsolves
robertsolves merged commit 26196fe into main Nov 3, 2026
4 checks passed
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.

2 participants