Skip to content

fix: add future timestamp rejection to webhook replay protection#83

Open
jonwiggins wants to merge 8 commits intomainfrom
optio/task-6e9f1699-267e-4db3-ab27-4abad8639b14
Open

fix: add future timestamp rejection to webhook replay protection#83
jonwiggins wants to merge 8 commits intomainfrom
optio/task-6e9f1699-267e-4db3-ab27-4abad8639b14

Conversation

@jonwiggins
Copy link
Owner

@jonwiggins jonwiggins commented Mar 25, 2026

Summary

  • Hardens the isReplayedEvent() webhook replay protection to also reject events with timestamps far in the future
  • Prevents clock-skew replay attacks where an attacker sends a replayed event with a manipulated future timestamp
  • Adds 2 new tests covering future timestamp rejection behavior

Context

The webhook signature validation (merged in #76) included replay protection that rejects events older than 5 minutes. However, it did not reject events with timestamps in the future, leaving a potential attack vector for clock-skew replay attacks.

Test plan

  • Existing isReplayedEvent tests still pass
  • New test: rejects timestamp 10 minutes in the future with 5-minute tolerance
  • New test: allows timestamp 1 minute in the future within 5-minute tolerance
  • Full typecheck passes
  • All 186 tests pass

🤖 Generated with Claude Code

Optio Agent and others added 8 commits March 25, 2026 03:45
Add signature verification to the /api/webhooks/github endpoint to prevent
forged webhook payloads. When GITHUB_WEBHOOK_SECRET is configured, all
incoming webhooks must include a valid X-Hub-Signature-256 header. Also adds
replay protection to reject events with stale timestamps.

- Validate HMAC-SHA256 signature against raw request body using timing-safe comparison
- Return 401 for missing or invalid signatures
- Add replay protection (reject events older than 5 minutes)
- Use Fastify preParsing hook to capture raw body for signature verification
- Add /api/webhooks/ to public routes (webhooks use signature auth, not session cookies)
- Unit tests for signature verification and replay detection
- Updated .env.example and Helm chart with GITHUB_WEBHOOK_SECRET config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add indexes on frequently queried columns to prevent full table scans
as task volume grows:

- tasks(repo_url, state) — filtering tasks by repo and state
- tasks(state) — state-based filtering on task list
- tasks(parent_task_id) — finding subtasks
- tasks(created_at DESC) — sorting by creation time
- task_logs(task_id, timestamp) — fetching logs for a task
- repo_pods(repo_url) — finding pods by repo
- task_events(task_id) — fetching event history

Includes both Drizzle schema index definitions and a SQL migration.

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
)

Add a webhook notification system that delivers events to external
endpoints when task state changes occur. Supports Slack incoming
webhooks out of the box with Block Kit formatted messages.

- CRUD endpoints: POST/GET/DELETE /api/webhooks
- Delivery log: GET /api/webhooks/:id/deliveries
- Events: task.completed, task.failed, task.needs_attention, task.pr_opened, review.completed
- HMAC-SHA256 signature in X-Optio-Signature header
- BullMQ worker with exponential backoff retry (3 attempts)
- Auto-detect Slack webhook URLs and format payloads with blocks
- Webhook secrets masked in API responses

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the Notion ticket provider stub that threw "not yet implemented"
errors on all methods. This prevents crashes if a user configures Notion
as a ticket source. The TicketSource enum, provider factory, and docs
are updated to reflect only the implemented providers (GitHub, Linear).

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add production-ready features to the Helm chart:

- Ingress TLS with cert-manager integration: auto-generates TLS blocks and
  cluster-issuer annotations when certManager.enabled is set
- Namespace ResourceQuota: configurable CPU, memory, pod, and PVC limits to
  prevent resource exhaustion by rogue agents
- Pod anti-affinity for API and web: soft/hard modes to spread replicas across
  nodes for high availability
- Agent PVC template: ConfigMap-based template for repo pod home directory
  PVCs with configurable storage class and size
- Image pull secrets: global imagePullSecrets support for private registries
- Secure defaults: empty postgres password (required when auth enabled),
  chart fails fast with a clear error message
- RBAC: added PVC and ConfigMap permissions for API server
- Worker scaling documentation: explains BullMQ's Redis-backed job delivery
  ensures safe multi-replica API scaling

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add test coverage reporting infrastructure

- Install @vitest/coverage-v8 in all test packages (api, shared, agent-adapters)
- Add vitest.config.ts with v8 coverage provider and json-summary reporter
- Add test:coverage scripts to package.json in each test package
- Add test:coverage turbo task with coverage output caching
- Add coverage report generation script (scripts/coverage-report.mjs)
  - Generates markdown table with per-package coverage metrics
  - Writes to coverage-report.md for PR comments
  - Writes to GITHUB_STEP_SUMMARY when available
- Add coverage/ and coverage-report.md to .gitignore
- Add eslint as explicit root devDependency (needed by lint-staged)

Note: CI workflow update (.github/workflows/ci.yml) requires a GitHub token
with `workflow` scope. The required change is to replace `pnpm turbo test`
with `pnpm turbo test:coverage` and add coverage report + artifact upload
steps. See PR description for the exact diff.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: add coverage thresholds and CI workflow integration

- Add minimum coverage thresholds to vitest configs:
  - api: 25% lines/statements/functions, 50% branches
  - shared: 70% lines/statements/functions, 80% branches
  - agent-adapters: 50% lines/statements, 80% branches/functions
- Update CI test job to run test:coverage with PR comment reporting
- Add coverage artifact upload with 14-day retention
- Coverage report uses marocchino/sticky-pull-request-comment for PR comments

Note: CI workflow changes (.github/workflows/ci.yml) require a PAT with
the `workflow` scope. The workflow changes are included in this commit and
will need to be pushed by a token with appropriate permissions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: auto-enable coverage in CI via vitest config

Set coverage.enabled = !!process.env.CI in all vitest configs so that
`pnpm turbo test` automatically collects coverage data and enforces
thresholds when running in GitHub Actions (CI=true), while keeping
local test runs fast by default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add task search and filtering with saved views

Add server-side search API (GET /api/tasks/search) with full-text search
on title/prompt, multi-field filtering (state, repo, agent type, cost
range, date range, author), and cursor-based pagination. Update the web
UI task list with an advanced filters panel, cost range inputs, saved
views persisted in localStorage, URL-synced filters for shareable links,
and a "Load more" button for cursor pagination.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add task search and filtering with cursor-based pagination

Add server-side search API (GET /api/tasks/search) with full-text search
on title/prompt, multi-field filtering (state, repo, agent type, cost
range, date range, author), and cursor-based pagination. Update the web
UI with advanced filter panel (repo dropdown, agent type, cost range),
URL-synced filters for shareable links, saved views in localStorage,
and a load-more button for paginated results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Optio Agent <optio-agent@noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The existing isReplayedEvent() only rejects events with timestamps in
the past. This adds rejection of events with timestamps far in the
future, which mitigates clock-skew replay attacks where an attacker
sends a replayed event with a manipulated future timestamp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jonwiggins jonwiggins changed the title fix: validate GitHub webhook signatures with HMAC-SHA256 fix: add future timestamp rejection to webhook replay protection Mar 25, 2026
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