Skip to content

security: harden dictation, telemetry, tools, file perms, and supply chain#1335

Open
r3dbars wants to merge 2 commits into
mainfrom
claude/app-security-audit-a8ce08
Open

security: harden dictation, telemetry, tools, file perms, and supply chain#1335
r3dbars wants to merge 2 commits into
mainfrom
claude/app-security-audit-a8ce08

Conversation

@r3dbars

@r3dbars r3dbars commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Why

A deep security audit of the app surfaced a set of defensive gaps — none critical, but worth closing. This PR fixes the actionable findings from that audit. The app's security posture was already strong (local-first, owner-only files, Sparkle EdDSA, HTTPS-only telemetry with allowlists, mature nightly checks); these are hardening changes on top of that.

Product Impact

  • Affects: dictation / meetings / agent artifacts / docs only (all four, defensively)
  • Lane: dictation reliability + release ops + agent workflow
  • Why this matters: prevents dictation Enter landing in the wrong window, tightens what can ride off-device to Sentry, removes an unbounded-read DoS in the companion tools, and closes a file-permission TOCTOU window for custom save locations on shared volumes.

What changed

Dictation / paste

  • HIGH — Auto Enter re-verifies the paste target is still frontmost immediately before posting Return, so a stray Enter is never submitted into a window that stole focus during the auto-enter delay (DictationSessionController.performAutoEnterIfNeeded). Skips and logs counts/bundle-id only.
  • Clipboard restore now best-effort clears resident dictation text when the restore guard fails, instead of leaving it on the pasteboard (only when the current pasteboard still equals the dictated text, so it never clobbers content copied afterward).
  • Synthetic Cmd+V / Return now use a private CGEventSource so they don't inherit physically-held modifiers.
  • Meeting-title filename sanitizer hardened (strips leading dots / all-dots).

Telemetry (Sentry)

  • Support-diagnostic extras now route through a positive key allowlist owned by SupportDiagnosticsBundle, dropping the free-text reliability blob and interpolated route_*/runtime_*/storage_* keys the sensitive-key fragment list didn't catch — matching the captureObservabilityEvent contract instead of relying on key-drop + regex alone.
  • Hard length cap (80) on the free-text-capable reason diagnostic tag before redaction.
  • Unused, allowlist-bypassing capture(error:) / capture(message:) made inert no-ops (no callers in the tree).

Companion tools

  • Cap full-file transcript reads at a shared 16 MB CaptureFileLimits.maxTranscriptBytes across MCP, CLI, and CaptureKit, removing an unbounded String(contentsOf:) local-DoS. Each call site honors its existing not-found/empty/throw contract on over-cap.

File permissions

  • umask(0o077) at app init so every created file is owner-only by default, closing the write-then-chmod TOCTOU window (covers Data.write(.atomic) temp files too); added posixPermissions: 0o600 to explicit createFile sites as belt-and-suspenders. Existing restrictToOwnerOnly/chmod calls retained.

Supply chain / docs

  • build-deps.sh now asserts the resolved mlx-swift-lm revision matches the pin (mirrors the WhisperKit revision check), failing loudly on drift.
  • THIRD_PARTY_LICENSES.md: explicit eSpeak NG GPL-3.0-or-later corresponding-source offer.
  • SECURITY.md: documented the MCP trust model (launch == full read), appcast hosting/trust roots, screen-capture audio-only intent, and the disable-library-validation rationale + removal tracking.

How I checked it

  • python3 scripts/ops/privacy-leak-sweep.pyPASS (all leak classes, 0 findings)
  • python3 scripts/ops/nightly-security-check.py98/100 (secret_scan 25/25, observability_privacy 25/25, release_integrity 20/20, entitlements_and_signing 15/15; the only finding is a pre-existing, unrelated missing local automation.toml)
  • bash -n scripts/entrypoints/build-deps.sh → OK
  • Confirmed no disallowed observability payload keys were introduced into the policy sources (scanned against config/security/nightly-security-manifest.json)
  • Verified the headline fix against real APIs (EventReporter.capture, DictationPasteTarget.matchesCurrentFrontmostApp, dictationContext(extra:)) and that the Tools Package.swift test-target product references resolve
  • bash build.sh --no-open / bash run-tests.sh / bash run-integration-smoke.sh / swift testnot runnable here: this environment is Linux with no Swift toolchain and the targets are macOS-only (platforms: [.macOS(.v14)]). These need to run in macOS CI before merge.

Risk Review

  • Privacy / local-first behavior reviewed — changes reduce off-device exposure; no new network paths
  • Storage path or migration impact reviewed — umask + perms are additive; no path/layout changes
  • Release/update impact reviewed — build-deps.sh assertion is additive; no version/appcast changes
  • Agent PR is draft pending human review
  • No private transcripts, audio, tokens, personal paths, or customer data included

Notes

Tests were added/extended in existing registered files (ClipboardRestoringTextPasterTests, SentryEventPolicyTests, SupportDiagnosticsBundleTests) and the Tools package test targets — no Tests/FastTests.manifest changes were required.

Out of scope / deliberately not changed (flagged in the audit, not code-fixable here):

  • disable-library-validation + sandbox-off remain (required by the MLX/WhisperKit/CoreML stack; mitigated by Developer-ID + notarization) — now documented and tracked.
  • Legacy bundle id com.justinbetker.draft left as-is (changing it has signing/TCC/Sparkle-identity consequences).
  • Committed Sentry DSN / PostHog key are client-embeddable ingest keys by design.
  • Appcast-on-GitHub-branch and MCP-no-auth are trust-model trade-offs — documented rather than changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QXiZDoQn4mmK2mKuz7R4w7


Generated by Claude Code

claude added 2 commits June 29, 2026 12:16
…chain

Audit-driven security hardening across six areas. No behavior change for
normal use; all changes are defensive.

Dictation / paste (Sources/UI/Overlay, Sources/Support):
- HIGH: re-verify the paste target is still frontmost immediately before
  Auto Enter posts Return, so a stray Enter is never submitted into a window
  that stole focus during the auto-enter delay. Skips + logs (counts only).
- Clipboard restore now best-effort clears resident dictation text when the
  restore guard fails, instead of leaving it on the pasteboard.
- Synthetic Cmd+V / Return use a private CGEventSource so they don't inherit
  physically-held modifiers.
- Harden meeting-title filename sanitizer (strip leading dots / all-dots).

Telemetry (Sources/Observability, Sources/UI/Shared):
- Route support-diagnostic Sentry extras through a positive key allowlist
  (drops the free-text reliability blob and uncaught interpolated keys),
  matching the captureObservabilityEvent contract.
- Hard length cap on the free-text-capable `reason` Sentry tag.
- Make unused allowlist-bypassing capture(error:)/capture(message:) inert.

Companion tools (Tools/*):
- Cap full-file transcript reads (16 MB shared CaptureFileLimits) across MCP,
  CLI, and CaptureKit to remove an unbounded-read local-DoS.

File permissions (Sources/*):
- Set umask(0o077) at app init so created files are owner-only by default,
  closing the write-then-chmod TOCTOU window; add posixPermissions to
  explicit createFile sites as belt-and-suspenders.

Supply chain / docs:
- Assert mlx-swift-lm resolved revision matches the pin in build-deps.sh.
- Document eSpeak NG GPL-3.0 corresponding-source offer.
- Document MCP trust model, appcast hosting, screen-capture audio-only intent,
  and disable-library-validation rationale in SECURITY.md.

Verification: privacy-leak-sweep PASS; nightly-security-check 98/100 (only a
pre-existing unrelated automation.toml finding). Swift build/tests require
macOS and must run in CI — not runnable in this Linux environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXiZDoQn4mmK2mKuz7R4w7
…yContext

The support-diagnostic hardening added the allowlist + test but left the
`latest_reliability_packet` free-text blob being built in `sentryContext`
itself, so its new raw-context assertion (SupportDiagnosticsBundleTests:183)
failed in CI. Remove the emission at the source — `reliability_packet_count`
already carries the coarse signal, and the human-readable diagnostics text
(built separately and redacted) still summarizes recent packets. The
downstream allowlist remains as defense-in-depth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXiZDoQn4mmK2mKuz7R4w7
@r3dbars r3dbars marked this pull request as ready for review July 3, 2026 01:34
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