Skip to content

harden(serve): close webhook-secret fail-open + tighten config parsing (OBS-1/2/3)#77

Merged
abpai merged 2 commits into
mainfrom
serve-config-hardening
Jun 23, 2026
Merged

harden(serve): close webhook-secret fail-open + tighten config parsing (OBS-1/2/3)#77
abpai merged 2 commits into
mainfrom
serve-config-hardening

Conversation

@abpai

@abpai abpai commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to the merged serve+webhooks QA work (#76), addressing the three out-of-scope observations its xhigh /code-review recorded. No behavior change for valid inputs; tightens two validation gaps and removes duplication. Full suite 491 pass / 27 skip / 0 fail (518 ran); bun run check clean; independent codex review APPROVE.

Changes

OBS-2 — close a webhook-secret fail-open (security). assertTunnelSecurity hardcoded a jira/linear→secret-env ternary that fell through to null for any other provider. The day a new webhook-capable provider is added, the tunnel guard would silently start a public tunnel with no signing secret enforced. Now there's a single source of truth — WEBHOOK_SECRET_ENV: Record<TrackerProvider, string | null> plus a trackerProviderFromEnv() normalizer in tracker-config.ts — and the guard looks both up:

  • The Record<TrackerProvider, …> type makes adding a provider a compile error until its secret env is declared.
  • The normalizer recognizes providers from the map's own keys (so inherited names like constructor can't leak through), keeping it in lockstep with the map.

OBS-1 — tighten the env interval parse (strictness). resolvePollingSyncIntervalMs parsed KANBAN_SYNC_INTERVAL_MS with Number(), accepting hex/scientific notation (0x3e8, 1e3). Now digits-only + Number.isSafeInteger, matching the strict --sync-interval-ms CLI flag. (Env path keeps INVALID_CONFIG; CLI path keeps INVALID_ARGUMENT.)

OBS-3 — share one integer parser (dedup). New parseBoundedInt in transport-input.ts is the single digits-only /^\d+$/ + isSafeInteger parser; parsePositiveInt, parsePort, and parseSyncIntervalMs all delegate to it, so a future hardening fix lands in one place.

Verification

  • Full suite 491 pass / 27 skip / 0 fail (518 ran); bun run check exit 0.
  • +16 test cases: parseBoundedInt bounds/rejection matrix, env hex/scientific/overflow rejection, the WEBHOOK_SECRET_ENV map contract + normalizer (incl. prototype-pollution guard).
  • All pre-existing parsePositiveInt, sync-config, and assertTunnelSecurity tests pass unchanged (messages/contracts preserved).
  • Independent codex review: APPROVE, no required changes (the one optional note — making the normalizer derive from the map — is applied here).

https://claude.ai/code/session_01LQjR5poSV2M3nxt8h5hNad

abpai added 2 commits June 23, 2026 01:28
…share int parser

Follow-up to the serve+webhooks QA work (PR #76), addressing the three
out-of-scope observations the xhigh code-review recorded (OBS-1/2/3).

OBS-2 (security): assertTunnelSecurity hardcoded a jira/linear -> secret-env
ternary that fell through to null for any other provider, so a future
webhook-capable provider could start a public tunnel with no signing secret
enforced. Add a single source of truth `WEBHOOK_SECRET_ENV: Record<
TrackerProvider, string | null>` plus a `trackerProviderFromEnv()` normalizer in
tracker-config.ts; the guard now looks both up. The Record type makes adding a
provider a compile error until its secret is declared, and the normalizer
derives recognized providers from the map's own keys so it stays in lockstep.

OBS-1 (strictness): resolvePollingSyncIntervalMs parsed KANBAN_SYNC_INTERVAL_MS
with Number(), accepting hex/scientific notation. Tighten to digits-only +
Number.isSafeInteger, matching the strict --sync-interval-ms CLI flag (env path
keeps INVALID_CONFIG; CLI path keeps INVALID_ARGUMENT).

OBS-3 (dedup): extract `parseBoundedInt` as the single digits-only integer
parser; parsePositiveInt, parsePort, and parseSyncIntervalMs all delegate to it,
so a future hardening fix lands in one place.

Independent codex review: APPROVE. +16 test cases; full suite 491 pass /
27 skip / 0 fail; check clean.

Claude-Session: https://claude.ai/code/session_01LQjR5poSV2M3nxt8h5hNad
…ate/enforcement drift)

OBS-2 made WEBHOOK_SECRET_ENV the single source of truth for the tunnel
gate (assertTunnelSecurity), but the runtime signature enforcement in
jira-core/linear-core still hardcoded process.env['JIRA_WEBHOOK_SECRET'] /
['LINEAR_WEBHOOK_SECRET']. The map only guaranteed the gate was exhaustive,
not that the gate and enforcement read the same env name — so a future
provider whose verification env diverged from its map entry could pass the
gate while verification read an unset var, accepting unsigned writes over a
public tunnel (fail-open).

Route both handlers through a new tracker-config webhookSecretFromEnv() that
resolves the secret via WEBHOOK_SECRET_ENV, so gate and enforcement share one
source of truth and cannot drift. Also collapses the per-handler double
process.env read into a single resolution. Rendered messages are unchanged.

+4 tests pinning that the helper reads exactly the env name the map declares
and never a sibling provider's env. Suite 495 pass / 0 fail, check clean.

Claude-Session: https://claude.ai/code/session_01LQjR5poSV2M3nxt8h5hNad
@abpai abpai merged commit 4fc4fdc into main Jun 23, 2026
1 check passed
abpai added a commit that referenced this pull request Jun 23, 2026
…mitive (#79)

Follow-up to #77 (review findings F3 + F2).

F3 — JIRA_BOARD_ID used Number.parseInt, which silently coerced trailing
garbage and signs into a plausible-but-wrong board id ('12abc'→12, '-5'→-5,
'1e3'→1) that was then pinned via Number.isFinite. Now resolved through a
strict loader: unset/blank → undefined (no board), set-but-invalid → throw
INVALID_CONFIG. This matches how resolvePollingSyncIntervalMs already treats a
malformed optional numeric env var (the codebase's own precedent), instead of
silently dropping or misinterpreting it.

BEHAVIOR CHANGE: a non-empty malformed JIRA_BOARD_ID now fails config loading
with INVALID_CONFIG rather than being silently ignored. Updated the wiring test
that previously asserted silent fallback.

F2 — extracted parseDecimalDigits (transport-input.ts) as the single pure
digits-only/safe-integer primitive; parseBoundedInt, resolvePollingSyncIntervalMs,
and the new JIRA_BOARD_ID loader all build on it, so "what counts as an integer"
has one definition and can't drift (no third hand-rolled copy). Error
codes/messages unchanged (INVALID_ARGUMENT vs INVALID_CONFIG preserved per path).

+4 tests (parseDecimalDigits unit coverage; JIRA_BOARD_ID accept/omit/reject).
Suite 499 pass / 0 fail, bun run check clean.

Claude-Session: https://claude.ai/code/session_01LQjR5poSV2M3nxt8h5hNad
@abpai abpai deleted the serve-config-hardening branch June 23, 2026 17:11
abpai added a commit that referenced this pull request Jun 24, 2026
The serve-hardening and CLI work since v0.7.0 (PRs #76, #77, #78, #79) merged
without changesets, so the Release workflow had nothing to consume and the
package is still published at 0.7.0. Add the missing changesets so the next
release captures this work.

- serve-api-webhook-hardening (patch): #76 - envelope webhook-route errors plus
  tunnel / Postgres-receipt / broadcast / base-path fixes (10 defects).
- webhook-secret-fail-open (patch): #77 - single WEBHOOK_SECRET_ENV source of
  truth for the tunnel gate + stricter KANBAN_SYNC_INTERVAL_MS parsing.
- honor-default-task-column (patch): #78 - SQLite createTask honors the
  configured default task column, with first-column fallback parity with Postgres.
- strict-jira-board-id (minor): #79 - a malformed JIRA_BOARD_ID now throws
  INVALID_CONFIG instead of being silently misparsed (behavior change).

Net bump: minor -> 0.8.0. #80 (pure internal refactor) and the README link fix
are intentionally omitted as they have no user-facing change.

Claude-Session: https://claude.ai/code/session_01X9j5Rs6kXK8BuHguhy3x33
@github-actions github-actions Bot mentioned this pull request Jun 24, 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