Skip to content

Security hardening, robustness fixes, and Windows support - #6

Open
abinashstack wants to merge 8 commits into
mainfrom
claude/funny-volta-nmnpdm
Open

abinashstack wants to merge 8 commits into
mainfrom
claude/funny-volta-nmnpdm

Conversation

@abinashstack

@abinashstack abinashstack commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Three parts: fixes from a security and correctness audit of the repo, tests for those fixes, and Windows support.

1. Security

  • SwiftBar command injection: the menu bar printed asset_type from INDmoney unescaped, before the | color=… attributes. An upstream value containing | or a newline could add a clickable bash= action. It's escaped now, and sbEscape also strips newlines and control characters.
  • Tokens in logs: OAuth and registration errors included the full response body. A malformed 200 token response would write access_token and refresh_token to agent.log. Errors now report only the OAuth error code, and response reads are capped in size.
  • Login abort: any web page could end an in-progress indw login by sending the browser to 127.0.0.1:47823/callback?error=…. Callbacks without the correct state are now ignored. state is compared in constant time, and the callback server has a ReadHeaderTimeout.
  • Plugin installer: menubar.refresh_interval is used in the plugin filename, so it's now validated (blocks path traversal). The binary path is shell-quoted in the generated script.
  • System tools: system binaries are run by absolute path instead of being looked up in $PATH.
  • Terminal output: the CLI strips control and ANSI characters from server-supplied names.
  • Go version: go.mod is raised from 1.26.4 to 1.26.6, which fixes six standard-library vulnerabilities that govulncheck reports as reachable (GO-2026-6218, -6090, -6089, -5972, -5856, -5026).
  • CI: permissions: contents: read, actions pinned to commit SHAs, persist-credentials: false, govulncheck pinned, and go test added.
  • SECURITY.md: documents that the Keychain item (and the Windows DPAPI file) can be read by any process running as the same user.

2. Robustness

  • Concurrent token refresh: the poller, menu bar and CLI could redeem the same refresh token at once and lose a rotated token. Refreshes now take a cross-process lock and re-read stored tokens after acquiring it. Refreshed tokens are kept in memory even if saving fails.
  • Keychain save: uses -U only, so there's no delete-then-add window.
  • 401 handling: the MCP client refreshes the token and retries once.
  • Due-date alerts: the old code was off by one day in IST ("due tomorrow" showed as 0d, and "due today" never alerted). Dates are now compared as local calendar days.
  • Scheduling: both the weekday and the time are converted to the local zone, using the current DST offset. The cadence matches the documented 10 minutes; the code was scheduling every 5.
  • Other: watchlist de-duplication no longer collapses ticker-only entries, stale alert timers are pruned from state.json, and truncation and padding count characters rather than bytes.

3. Windows support

Each macOS-only piece gets a Windows counterpart, selected by build tags:

macOS Windows
Tokens Keychain DPAPI-encrypted file in %AppData%\indmoney-watch (Credential Manager's 2.5 KB limit is too small for JWTs)
Alerts osascript Toast via Windows PowerShell; text passed through env vars and XML-escaped, script passed with -EncodedCommand
indw start / stop launchd Task Scheduler task from an XML definition: runs hidden via conhost --headless, on battery, never overlapping, 5 min limit
Refresh lock flock LockFileEx
Login browser open rundll32 url.dll
Menu bar SwiftBar Not available; menubar install explains why

Cross-platform changes:

  • indw logs [-f] is implemented in Go instead of calling tail.
  • run-once --log writes to agent.log with 5 MB rotation. Task Scheduler can't redirect output the way launchd does, so the Windows task uses this flag.
  • notify.MacBanner is renamed to notify.Banner.

Testing

  • CI is green on macos-latest and windows-latest: build, vet, tests, and govulncheck.
  • Locally: go vet and staticcheck are clean for windows, darwin and linux, and go test -race ./... passes.
  • New tests catch the old bugs: I ran the escaping, due-date and watchlist tests against the pre-fix code, and they failed there.
  • Other new tests: interval validation, shell quoting (round-tripped through bash), launchd and Task Scheduler schedules across time zones, the 401 retry, OAuth error redaction, callback forgery handling, refresh-lock exclusivity, and log tail, follow and rotation.
  • Windows-only tests, passing on windows-latest:
    • DPAPI round-trip, including a check that the file contains no plaintext and can't be decrypted without the entropy value;
    • toast script parsing, and hostile-text escaping in the toast XML;
    • registering a real scheduled task, reading it back from Task Scheduler, and deleting it.
  • End to end with the real binary: in a sandboxed HOME, run-once --log writes to agent.log and indw logs reads it back. The installer rejects a traversal interval. A binary path containing ', $(…) and backticks doesn't inject anything.
  • Not run: a real toast appearing on a Windows desktop, and a scheduled run firing on its own on Windows.

After merging, re-run indw start (and indw menubar install on macOS) to pick up the new schedule and plugin script.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TT1VrXR9CkmWFfRQN1H87J

claude added 3 commits October 3, 2026 10:48
Security:
- menubar: escape asset_type (was printed raw before SwiftBar attributes,
  allowing an upstream value to inject a clickable bash= action); sbEscape
  now also strips newlines/control chars so upstream text can't add lines.
- oauth: never echo token/registration response bodies in errors (a
  malformed 200 leaked access/refresh tokens into agent.log); cap reads.
- oauth: callbacks without our state are ignored instead of aborting the
  login, so any web page can't cancel an in-progress login; constant-time
  state compare; ReadHeaderTimeout on the callback server.
- menubar install: validate refresh_interval (used in the plugin filename)
  and shell-quote the binary path in the generated script.
- invoke security/osascript/launchctl/open/defaults/tail by absolute path.
- CLI output strips control characters from server-supplied names.
- CI: least-privilege permissions, SHA-pinned actions, no persisted
  credentials, pinned govulncheck, and run go test.

Robustness:
- serialise token refreshes across processes with a lock file and re-read
  Keychain after acquiring it, so the poller and menu bar can't burn a
  rotated refresh token; keep refreshed tokens in memory if saving fails.
- keychain save uses -U only (no delete-then-add window).
- MCP client refreshes and retries once on HTTP 401.
- due-date alerts compare local calendar dates (were off by one: "due
  tomorrow" showed as 0d and due-today never alerted).
- launchd schedule converts weekday as well as time to local zone and uses
  the current DST offset; cadence now matches the documented 10 minutes.
- watchlist dedupe no longer collapses ticker-only entries; prune stale
  debounce keys; rune-safe truncation/padding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT1VrXR9CkmWFfRQN1H87J
Extract the OAuth redirect handler into callbackHandler so it can be
exercised directly, and cover: forged ?error=/?state= requests being
ignored without ending the login, HTML-escaping of the error param, and
exclusivity of the refresh lock file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT1VrXR9CkmWFfRQN1H87J
Each macOS-only piece gets a Windows counterpart behind build tags:

- Tokens: DPAPI-encrypted file (CryptProtectData, per-user, with app
  entropy) in %AppData%\indmoney-watch instead of the Keychain.
  Credential Manager's 2.5 KB blob limit is too small for JWTs.
- Notifications: Windows toast via PowerShell/WinRT, text passed through
  env vars and XML-escaped, script passed with -EncodedCommand.
- Background poller: `indw start`/`stop` register a Task Scheduler task
  from an XML definition (local-time weekdays like launchd, runs hidden
  under conhost --headless, on battery, no overlap, 5 min limit).
- Refresh lock: LockFileEx instead of flock.
- Browser: rundll32 url.dll instead of open(1).
- Config dir: %AppData%\indmoney-watch.

Cross-platform changes:
- `indw logs [-f]` is implemented in Go instead of shelling out to tail.
- `run-once --log` appends to agent.log with simple 5 MB rotation (Task
  Scheduler can't redirect output the way launchd does).
- `menubar install` errors clearly off macOS (SwiftBar is macOS-only).
- notify.MacBanner renamed to notify.Banner.

CI now runs build/vet/test on windows-latest as well as macos-latest; the
Windows tests cover the DPAPI round trip, toast script parsing and
escaping, and registering, reading back and deleting a real scheduled
task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT1VrXR9CkmWFfRQN1H87J
@abinashstack abinashstack changed the title Security hardening and robustness fixes from repo audit Security hardening, robustness fixes, and Windows support Oct 3, 2026
claude added 2 commits October 3, 2026 11:20
On a fresh Windows runner, Windows PowerShell writes "Preparing modules
for first use" progress records to stderr as CLIXML when output is
redirected. The toast tests compared combined output to "ok" and failed
even though the checks passed. Set $ProgressPreference in the toast
script (keeps real error messages clean too) and have the tests read
stdout only, reporting stderr on failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT1VrXR9CkmWFfRQN1H87J
govulncheck in CI flags six reachable std-lib issues in go1.26.4
(GO-2026-6218 net/url, GO-2026-6090 and GO-2026-5856 crypto/tls,
GO-2026-6089 and GO-2026-5026 net/http, GO-2026-5972 encoding/asn1), all
fixed in go1.26.6. Same remedy as 42a3508.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT1VrXR9CkmWFfRQN1H87J

Copy link
Copy Markdown
Owner Author

CI on efa3ec9 failed in two places:

  • build (windows-latest): this PR's bug, fixed in 2b85ac7. The toast tests compared stdout and stderr together against exactly ok. On a fresh runner, Windows PowerShell also writes "Preparing modules for first use" progress messages to stderr, so the tests failed even though every check inside them passed. The toast script now silences progress output, and the tests read stdout only. The DPAPI round-trip and the real Task Scheduler register, read-back and delete tests passed on that run.
  • build (macos-latest), govulncheck step: not caused by this PR. Six reachable Go standard-library vulnerabilities in Go 1.26.4 (GO-2026-6218, -6090, -6089, -5972, -5856, -5026) are fixed in 1.26.6, and main pins 1.26.4 too, so it would fail there as well. I applied the same remedy as 42a3508 in efee201: go.mod now says go 1.26.6.

Generated by Claude Code

claude added 2 commits October 3, 2026 11:32
govulncheck failures appear without any code change when new Go
vulnerabilities are published (as happened with go1.26.4), but CI only
ran on push/PR, so main could sit red unnoticed. A weekly schedule
surfaces that automatically; workflow_dispatch allows on-demand runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT1VrXR9CkmWFfRQN1H87J
Lets a scan be started on demand (Actions tab or API), e.g. after the
workflow is re-enabled following GitHub's inactivity auto-disable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT1VrXR9CkmWFfRQN1H87J
Comment thread internal/store/lock_unix.go Fixed
Comment thread internal/store/lock_unix.go Fixed
CodeQL (go/unhandled-writable-file-close) flagged the lock file handle as
writable and closed without error handling. Nothing is ever written to
it and flock/LockFileEx only need read access, so open it O_RDONLY.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TT1VrXR9CkmWFfRQN1H87J
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.

3 participants