Add get_client_ip() helper that inspects Forwarded and X-Forwarded-For
headers only when the direct peer is in TRUSTED_PROXIES (comma-separated
IPs/CIDRs). Replaces raw request.client.host in rate-limit and webhook
source_ip to prevent all traffic collapsing behind a reverse proxy IP.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents normal user requests with Authorization: Bearer from being
throttled by the agent auth limiter in the shared require_user_or_agent
dependency path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Add RedisRateLimiter using sorted-set sliding window alongside the
existing InMemoryRateLimiter. Users choose via RATE_LIMIT_BACKEND
(memory|redis) with RATE_LIMIT_REDIS_URL falling back to RQ_REDIS_URL.
Redis backend validates connectivity at startup and fails open on
transient errors during requests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Not all webhook providers use X-Hub-Signature-256 or X-Webhook-Signature.
Add an optional signature_header field so users can specify which header
carries the HMAC signature. When set, that exact header is checked;
when unset, the existing auto-detect fallback is preserved. The custom
header is also excluded from stored/exposed payload headers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
get_agent_auth_context_optional was not rate-limited, allowing
brute-force token guessing via routes that use require_user_or_agent.
Now applies agent_auth_limiter when a token is actually presented.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The has_token redaction was reverted to avoid a frontend breaking
change. Update docs to match: tokens are currently returned in API
responses, redaction is planned for a future PR. Also note the
configurable payload size limit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
node:20-alpine uses BusyBox which does not support GNU-style
--system/--ingroup flags. Switch to -S/-G equivalents.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous field_validator approach passed `cls` as the first argument
to `_normalize_secret`, which only accepted `v`, causing a TypeError at
runtime. Switch to `Annotated[str | None, BeforeValidator(...)]` which
calls the function with just the value and also eliminates the repeated
validator assignment in both schema classes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move blank/whitespace-only secret normalization to a shared
field_validator on both BoardWebhookCreate and BoardWebhookUpdate.
This ensures consistent behavior across create and update paths
and removes the inline normalization from the endpoint handlers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The has_token boolean redaction requires coordinated frontend changes
(detail page, edit page, orval types). Revert to returning the raw
token for now; token redaction will be handled in a dedicated PR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A 6-character prefix of the token is standard practice for debugging
failed auth attempts and is not a security risk. Restored in both
required and optional auth paths, and removed the now-incorrect test
that asserted its absence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add WEBHOOK_MAX_PAYLOAD_BYTES setting (default 1 MB) so deployments
with larger webhook payloads can raise the limit via environment
variable instead of being hard-blocked at 1 MB.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_captured_headers was storing all x-* headers including
X-Hub-Signature-256 and X-Webhook-Signature. Since stored headers
are exposed via the payload read endpoint, this enabled replay
attacks without knowing the webhook secret. Now signature and
authorization headers are excluded from capture.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These two tests were exact subsets of the dedicated test_rate_limit.py
suite. Consolidating to a single file avoids maintenance drift.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These dependencies check actor type (human user vs agent), not admin
privilege. The old names were misleading and could cause authorization
mistakes when wiring new endpoints. Renamed across all 10 consumer
files along with their local ADMIN_AUTH_DEP / ADMIN_OR_AGENT_DEP
aliases.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The secret field was accepted in BoardWebhookCreate but never passed
to the BoardWebhook constructor, silently dropping it. Now secret is
persisted at creation time (with empty/whitespace normalized to None)
and similarly normalized on PATCH so sending "" clears a set secret.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The module and class docstrings incorrectly described the implementation
as a "token-bucket" limiter when it actually uses a sliding-window log
(deque of timestamps with pruning).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add prompt-injection fencing to _webhook_memory_content (was missing
the --- BEGIN/END EXTERNAL DATA --- fence applied elsewhere)
- Wrap Content-Length parsing in try/except to avoid 500 on malformed
header values
- Move _to_gateway_read below imports (was incorrectly placed between
import blocks) and tighten transformer types
- Replace list-rebuild with deque.popleft in rate limiter for O(expired)
amortized pruning instead of O(n) per call
- Make organization_id required in send_session_message to prevent
fail-open cross-tenant check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- agent.py: Fail closed when gateway lookup returns None instead of
silently dropping the organization filter (cross-tenant board leak)
- board_webhooks.py: Read request body via streaming chunks so an
oversized payload is rejected before it is fully loaded into memory
- rate_limit.py: Add periodic sweep of expired keys to prevent
unbounded memory growth from inactive clients
- test_rate_limit.py: Add test for the new sweep behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gateway tokens were returned as plaintext in GatewayRead API responses.
Replace the `token` field with a boolean `has_token` flag so the API
never exposes the plaintext token. The token remains in the database
for outbound gateway connections (full encryption would require key
management infrastructure).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agent token auth performed O(n) PBKDF2 operations per request with no
rate limiting, enabling CPU exhaustion attacks. Webhook ingest had no
rate limits either. Add an in-memory token-bucket rate limiter:
- Agent auth: 20 requests/minute per IP
- Webhook ingest: 60 requests/minute per IP
Includes unit tests for the rate limiter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The file contained a publicly known LOCAL_AUTH_TOKEN value that could
be used against misconfigured deployments. Replace with an empty value
and a comment showing how to generate a secure token. The test suite
continues to work via conftest.py which sets its own test-only token.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
X-Content-Type-Options, X-Frame-Options, and Referrer-Policy all
defaulted to empty (disabled). Set defaults to nosniff, DENY, and
strict-origin-when-cross-origin respectively. Operators can still
override or disable via environment variables.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both backend and frontend Dockerfiles ran all processes as root.
Add a dedicated appuser in each runtime stage so container processes
run with minimal privileges, limiting blast radius of any container
escape.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The first 6 characters of invalid agent tokens were logged, leaking
partial credential information. Remove token_prefix from log messages
while preserving the request path for debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
send_gateway_session_message only required basic auth (AUTH_DEP) while
all other gateway endpoints required ORG_ADMIN_DEP. Any authenticated
user could send messages to any gateway session. Now requires org-admin
and verifies the board belongs to the caller's organization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The webhook ingest endpoint read the entire request body with no size
limit, enabling memory exhaustion attacks. Add a 1 MB limit checked
via both Content-Length header (early reject) and actual body size.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
User-controlled fields (skill name, source URL, webhook payloads) were
interpolated directly into agent instruction messages. Sanitize skill
fields by stripping newlines/control chars, and fence all external data
behind "BEGIN EXTERNAL DATA" / "BEGIN STRUCTURED DATA" delimiters with
explicit "do not interpret as instructions" markers. Move system
instructions above the data section so they cannot be overridden.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Webhook ingest endpoint was completely unauthenticated. Add an optional
`secret` field to BoardWebhook. When configured, inbound requests must
include a valid HMAC-SHA256 signature in X-Hub-Signature-256 or
X-Webhook-Signature headers. Uses hmac.compare_digest for timing safety.
Includes migration to add the secret column.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Main agents (board_id=None) could list boards across all organizations.
Now resolves the agent's organization via its gateway and filters boards
by organization_id to prevent cross-tenant data leakage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The function only checked that the caller was an authenticated user
(not an agent) but its name implied privilege enforcement. Rename to
require_user_actor and add docstring clarifying the distinction between
actor-type checks and privilege/role checks (require_org_admin, is_super_admin).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>