open-swe/tests/test_linear_webhook_replay.py
Adam Moussa 3e56062c43
fix: enforce a replay window on Linear webhooks (AUTHZ-001)
verify_linear_signature accepted any correctly-signed body with no freshness
check, so a captured request could be replayed indefinitely. Parse the
signed webhookTimestamp (Unix ms) and reject requests outside a 60s window,
failing closed when the field is missing or malformed — mirroring the Slack
verifier.
2026-06-29 11:37:03 -04:00

53 lines
1.8 KiB
Python

"""Replay-window enforcement for Linear webhook signature verification (AUTHZ-001)."""
from __future__ import annotations
import hashlib
import hmac
import json
from datetime import UTC, datetime
from agent import webapp
_SECRET = "linear-signing-secret"
def _sign(body: bytes) -> str:
return hmac.new(_SECRET.encode("utf-8"), body, hashlib.sha256).hexdigest()
def _now_ms() -> int:
return int(datetime.now(UTC).timestamp() * 1000)
def test_fresh_timestamp_accepted() -> None:
body = json.dumps({"type": "Comment", "webhookTimestamp": _now_ms()}).encode()
assert webapp.verify_linear_signature(body, _sign(body), _SECRET) is True
def test_stale_timestamp_rejected() -> None:
stale = _now_ms() - 10 * 60 * 1000 # 10 minutes old
body = json.dumps({"type": "Comment", "webhookTimestamp": stale}).encode()
# Signature is valid, but the timestamp is outside the freshness window.
assert webapp.verify_linear_signature(body, _sign(body), _SECRET) is False
def test_future_timestamp_rejected() -> None:
future = _now_ms() + 10 * 60 * 1000
body = json.dumps({"type": "Comment", "webhookTimestamp": future}).encode()
assert webapp.verify_linear_signature(body, _sign(body), _SECRET) is False
def test_missing_timestamp_rejected() -> None:
body = json.dumps({"type": "Comment"}).encode()
assert webapp.verify_linear_signature(body, _sign(body), _SECRET) is False
def test_non_numeric_timestamp_rejected() -> None:
body = json.dumps({"type": "Comment", "webhookTimestamp": "not-a-number"}).encode()
assert webapp.verify_linear_signature(body, _sign(body), _SECRET) is False
def test_bad_signature_rejected_even_when_fresh() -> None:
body = json.dumps({"type": "Comment", "webhookTimestamp": _now_ms()}).encode()
assert webapp.verify_linear_signature(body, "deadbeef", _SECRET) is False