fix: normalize and validate signature_header in webhook schemas

Strip whitespace (blank → None) and reject non-ASCII-token characters
to prevent impossible header lookups that would fail all signed requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hugh Brown
2026-03-04 10:32:13 -07:00
committed by Abhimanyu Saharan
parent fe310b50dc
commit ac69c6b7b8

View File

@@ -6,6 +6,8 @@ from datetime import datetime
from typing import Annotated from typing import Annotated
from uuid import UUID from uuid import UUID
import re
from pydantic import BeforeValidator from pydantic import BeforeValidator
from sqlmodel import SQLModel from sqlmodel import SQLModel
@@ -13,6 +15,9 @@ from app.schemas.common import NonEmptyStr
RUNTIME_ANNOTATION_TYPES = (datetime, UUID, NonEmptyStr) RUNTIME_ANNOTATION_TYPES = (datetime, UUID, NonEmptyStr)
# RFC 7230 token characters: visible ASCII except delimiters.
_HTTP_TOKEN_RE = re.compile(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$")
def _normalize_secret(v: str | None) -> str | None: def _normalize_secret(v: str | None) -> str | None:
"""Normalize blank/whitespace-only secrets to None.""" """Normalize blank/whitespace-only secrets to None."""
@@ -22,7 +27,21 @@ def _normalize_secret(v: str | None) -> str | None:
return stripped or None return stripped or None
def _normalize_signature_header(v: str | None) -> str | None:
"""Normalize and validate signature_header as a valid HTTP header name."""
if v is None:
return None
stripped = v.strip()
if not stripped:
return None
if not _HTTP_TOKEN_RE.match(stripped):
msg = "signature_header must be a valid HTTP header name (ASCII token characters only)"
raise ValueError(msg)
return stripped
NormalizedSecret = Annotated[str | None, BeforeValidator(_normalize_secret)] NormalizedSecret = Annotated[str | None, BeforeValidator(_normalize_secret)]
NormalizedSignatureHeader = Annotated[str | None, BeforeValidator(_normalize_signature_header)]
class BoardWebhookCreate(SQLModel): class BoardWebhookCreate(SQLModel):
@@ -32,7 +51,7 @@ class BoardWebhookCreate(SQLModel):
enabled: bool = True enabled: bool = True
agent_id: UUID | None = None agent_id: UUID | None = None
secret: NormalizedSecret = None secret: NormalizedSecret = None
signature_header: str | None = None signature_header: NormalizedSignatureHeader = None
class BoardWebhookUpdate(SQLModel): class BoardWebhookUpdate(SQLModel):
@@ -42,7 +61,7 @@ class BoardWebhookUpdate(SQLModel):
enabled: bool | None = None enabled: bool | None = None
agent_id: UUID | None = None agent_id: UUID | None = None
secret: NormalizedSecret = None secret: NormalizedSecret = None
signature_header: str | None = None signature_header: NormalizedSignatureHeader = None
class BoardWebhookRead(SQLModel): class BoardWebhookRead(SQLModel):