fix(security+scheduler): IPv6 SSRF gap + scheduler unit tests [supersedes #111, #112] - #119
Conversation
…un, panic recovery Added scheduler_test.go with 8 test cases covering all previously untested security-critical code paths from PR #90: TestLastTickAt_zero — zero time before first tick TestHealthy_beforeStart — false on fresh scheduler (zero lastTickAt) TestHealthy_freshTick — true when lastTickAt == now TestHealthy_stale — false when lastTickAt is 3×pollInterval ago TestComputeNextRun_valid — "0 * * * *" / UTC returns top-of-hour future time TestComputeNextRun_invalid — unparseable expression returns non-nil error TestComputeNextRun_invalidTimezone — unrecognised IANA zone returns non-nil error TestPanicRecovery — panicProxy crashes ProxyA2ARequest; scheduler goroutine recovers and remains Healthy To support these tests, scheduler.go gained four changes (minimal surface): 1. Added mu sync.RWMutex, lastTickAt time.Time, and tickInterval time.Duration fields to Scheduler. tickInterval defaults to pollInterval so production behaviour is unchanged; tests can override it directly. 2. Added LastTickAt() and Healthy() methods with read-lock protection. 3. tick() now records lastTickAt after wg.Wait() — a single atomic write under the mutex, no hot-path cost. 4. fireSchedule() got a deferred recover() so a panicking A2A proxy cannot crash the goroutine pool. Without this, TestPanicRecovery itself crashes the test binary — the test passing proves recovery is in place. Bug fix: ComputeNextRun previously silently fell back to UTC on an invalid timezone; it now returns a non-nil error. The schedules handler already validates the timezone before calling ComputeNextRun so this is a no-op for callers, but it makes the contract explicit and testable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #94 blocked 169.254.0.0/16 but left IPv6 equivalents fully open. Go's (*IPNet).Contains() does not match pure IPv6 addresses against IPv4 CIDRs, so ::1, fe80::*, and fc00::/7 all bypassed the check. Add three explicit IPv6 entries to blockedRanges: - fe80::/10 (IPv6 link-local — cloud metadata analogue) - ::1/128 (IPv6 loopback) - fc00::/7 (IPv6 ULA — RFC-4193 private) IPv4-mapped IPv6 (::ffff:169.254.x.x) is already safe: Go normalises these to IPv4 via To4() before Contains() runs. Tests: four new cases in TestValidateAgentURL covering all three blocked IPv6 ranges plus the IPv4-mapped IPv6 auto-normalisation path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verified — correct fix, holding for user approvalReviewed both parts of the diff: IPv6 SSRF gap (the security side):
Scheduler unit tests (the reliability side):
Why I'm not merging autonomously: touches both SSRF validation (security-critical) and the scheduler core (large blast radius). Per overnight triage rules, both categories need explicit CEO approval. CI status: same billing block as #110 — 6× FAILURE is a GitHub Actions org spending cap, not a real failure. Verified locally:
Ready for CEO merge once billing unblocks or with --admin. (Supersedes #111 and #112 per the PR title — I haven't verified those branches myself since they're already closed.) |
🚨 CONFIRMED LIVE (audit cycle-4, 2026-04-15)Security Auditor ran DAST against IPv6 link-local URL bypassed Filed as issue #145. This PR has the correct fix. The |
…port Cherry-picks the one genuinely new fix from #169 after confirming the rest of that PR is already covered on main (C1/C3/C5 by wsAuth group, C6 by #94+#119 SSRF blocklist, C4 ownership by existing WHERE filter). Pre-existing middleware (WorkspaceAuth on /workspaces/:id/* sub-routes) proves the caller owns the :id path param. But the body field source_id was never validated — a workspace authenticated for its own /activity endpoint could still attribute logs to a different workspace by setting source_id=<foreign UUID>. Rejected with 403 now. No schema change, no new middleware. 4-line handler delta. Closes the only real gap in #169; #169 itself will be closed as superseded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ht sweep Captures ~27 PRs merged across both repos this session: security hardening cluster (#94/#99/#106/#110/#119/#162/#155/#167/#185/#200/#203/ #209/#233), data-integrity fixes (#212/#224/#236), CI runner migration (#186), platform/scheduler reliability (#95/#149/#207/#206), workspace runtime features (#205/#208/#198/#216/#225/#235/#231), code-review follow-ups (#228/#232). Updated counts: 816 Go (+70), 1180 Python (+40), 453 vitest (unchanged — UI/a11y patches), 97 jest (unchanged). CLAUDE.md additions: - Idle Loop section (#205) under Architectural Patterns - Admin auth middleware variants section linking docs/runbooks/admin-auth.md - Migration runner section explaining the .down.sql filter (#212) - Per-route auth notes in the API table (PATCH field-whitelist, CanvasOrBearer on PUT /canvas/viewport, AdminAuth on bundles/events/templates-import/ approvals-pending/admin-liveness) - Database section updated with workspace_auth_tokens auto-revoke (#110), scheduler.error_detail surfacing (#206), workspace_schedules.last_status 'skipped' state (#207) PLAN.md additions: - New Recently launched (overnight sweep) section with full PR/issue index - Phase status updated (B–G now complete, H partial) - Live infrastructure deltas (migration fix, token rotation, legal pages) - Outstanding items consolidated Edit-history file expanded from the tick-9 stub to a full session record covering malware cleanup, CI runner migration, security cluster, data integrity, infra/feature/code-review batches, and outstanding user actions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…port Cherry-picks the one genuinely new fix from #169 after confirming the rest of that PR is already covered on main (C1/C3/C5 by wsAuth group, C6 by #94+#119 SSRF blocklist, C4 ownership by existing WHERE filter). Pre-existing middleware (WorkspaceAuth on /workspaces/:id/* sub-routes) proves the caller owns the :id path param. But the body field source_id was never validated — a workspace authenticated for its own /activity endpoint could still attribute logs to a different workspace by setting source_id=<foreign UUID>. Rejected with 403 now. No schema change, no new middleware. 4-line handler delta. Closes the only real gap in #169; #169 itself will be closed as superseded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ht sweep Captures ~27 PRs merged across both repos this session: security hardening cluster (#94/#99/#106/#110/#119/#162/#155/#167/#185/#200/#203/ #209/#233), data-integrity fixes (#212/#224/#236), CI runner migration (#186), platform/scheduler reliability (#95/#149/#207/#206), workspace runtime features (#205/#208/#198/#216/#225/#235/#231), code-review follow-ups (#228/#232). Updated counts: 816 Go (+70), 1180 Python (+40), 453 vitest (unchanged — UI/a11y patches), 97 jest (unchanged). CLAUDE.md additions: - Idle Loop section (#205) under Architectural Patterns - Admin auth middleware variants section linking docs/runbooks/admin-auth.md - Migration runner section explaining the .down.sql filter (#212) - Per-route auth notes in the API table (PATCH field-whitelist, CanvasOrBearer on PUT /canvas/viewport, AdminAuth on bundles/events/templates-import/ approvals-pending/admin-liveness) - Database section updated with workspace_auth_tokens auto-revoke (#110), scheduler.error_detail surfacing (#206), workspace_schedules.last_status 'skipped' state (#207) PLAN.md additions: - New Recently launched (overnight sweep) section with full PR/issue index - Phase status updated (B–G now complete, H partial) - Live infrastructure deltas (migration fix, token rotation, legal pages) - Outstanding items consolidated Edit-history file expanded from the tick-9 stub to a full session record covering malware cleanup, CI runner migration, security cluster, data integrity, infra/feature/code-review batches, and outstanding user actions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
e4e6634): addsfe80::/10,::1/128, andfc00::/7tovalidateAgentURL's blocked ranges, closing the C6 gap where Go'snet.ParseCIDR/Contains()doesn't match pure IPv6 addresses against IPv4-only blocklist entries. IPv4-mapped IPv6 (e.g.::ffff:169.254.169.254) is handled automatically by Go'sTo4()normalisation. All existing IPv4 blocks (loopback, RFC-1918, link-local) are preserved — PR fix(security): close IPv6 SSRF gap in validateAgentURL (C6) #112's branch inadvertently dropped them; this cherry-pick brings only the net-new IPv6 entries.9a66a4c): addsscheduler_test.gocoveringHealthy(),LastTickAt(),ComputeNextRun(), and panic-recovery inStart(). Also addstickInterval time.Durationfield toSchedulerstruct so tests can inject a sub-millisecond tick interval without waiting 30 s.Why a new branch
PRs #111 and #112 were based on a stale branch (pre-#95, #99, #106) — their diffs vs. main showed -965 deletions of files added by those PRs. Both original commits were clean; this branch cherry-picks them onto a fresh base from main. Original PRs are closed.
Conflict resolutions
scheduler.go: HEAD (PR #95) added
LastTickAt()andHealthy()methods; PR #111 independently added the same methods plustickInterval. Kept HEAD's method bodies (direct mutex access, slightly leaner) and addedtickIntervalfrom PR #111. Removed the duplicated method definitions.registry.go / registry_test.go: PR #112's branch had removed the RFC-1918 and loopback IPv4 blocks (a regression). Cherry-pick resolution: kept all HEAD IPv4 blocked ranges, added only the three new IPv6 entries. Test table updated to match: all existing
wantErr: truecases preserved, four new IPv6 cases added.Test plan
go test -race ./internal/scheduler/...— all scheduler tests pass including new onesgo test -race ./internal/handlers/...—TestValidateAgentURLcovers all 4 new IPv6 casesgo vet ./...— clean🤖 Generated with Claude Code