This document describes the security measures built into the LangCore API.
All user-supplied URLs (document_url, callback_url) pass through
app.core.security.validate_url() before the worker fetches or
POSTs to them. The check enforces:
| Guard | Detail |
|---|---|
| Scheme allowlist | Only http and https are accepted. |
| Hostname check | localhost is explicitly blocked. |
| URL length | URLs longer than 2 048 characters are rejected. |
| DNS resolution | Resolves the hostname and rejects private/reserved IPs (RFC 1918, link-local, loopback, IPv6 ::1). |
| DNS timeout | Resolution times out after 5 s to prevent slow DNS attacks. |
| Domain allowlist | When ALLOWED_URL_DOMAINS is set, only listed domains (and their sub-domains) are permitted. |
The document downloader follows redirects (up to 5 hops) but
re-validates every redirect target against the same SSRF
rules before following it. This prevents an attacker from
submitting a "safe" public URL that 302-redirects to a private
IP or cloud metadata endpoint (e.g. 169.254.169.254).
Additionally, the worker re-validates the document_url just
before downloading — defence-in-depth in case a task is
enqueued outside the API route.
The current implementation resolves the hostname at validation time. A malicious DNS server could return a public IP during validation and a private IP when the worker later connects ("DNS rebinding"). For high-security deployments, pin resolved IPs and pass them to the HTTP client directly (a future enhancement).
When WEBHOOK_SECRET is configured, every webhook POST includes two
extra headers:
| Header | Value |
|---|---|
X-Webhook-Timestamp |
Unix epoch seconds (integer as string) |
X-Webhook-Signature |
Hex-encoded HMAC-SHA256 of {timestamp}.{body} |
message = f"{timestamp}.".encode() + raw_body_bytes
signature = HMAC-SHA256(secret.encode(), message).hexdigest()
-
Read
X-Webhook-TimestampandX-Webhook-Signatureheaders. -
Reject if the timestamp is older than 5 minutes (replay protection).
-
Compute the expected signature:
expected = hmac.new( secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256, ).hexdigest()
-
Use
hmac.compare_digest(expected, received_signature)to compare (constant-time).
API keys (OPENAI_API_KEY, GEMINI_API_KEY, LANGCORE_API_KEY)
are loaded from environment variables or a .env file and never
logged or returned in API responses. Worker processes inherit the
same environment.