unclecode/crawl4ai · critical · UntrustedConfigError
LLMConfig.api_token may not reference an environment variabl
Error message
LLMConfig.api_token may not reference an environment variable from an untrusted request
What it means
Security control in LLMConfig: when an instance is marked Provenance.UNTRUSTED (originating from a request body / external input) and api_token starts with "env:", crawl4ai refuses to resolve the environment variable. Resolving env references from untrusted input would let an attacker exfiltrate server secrets (e.g. api_token="env:OPENAI_API_KEY" then reading the resolved value back), so it raises UntrustedConfigError. This is defense in depth behind a type gate that normally prevents untrusted LLMConfig construction entirely.
Source
Thrown at crawl4ai/async_configs.py:2243
frequency_penalty: Optional[float] = None,
presence_penalty: Optional[float] = None,
stop: Optional[List[str]] = None,
n: Optional[int] = None,
backoff_base_delay: Optional[int] = None,
backoff_max_attempts: Optional[int] = None,
backoff_exponential_factor: Optional[int] = None,
provenance: "Provenance" = None,
):
"""Configuaration class for LLM provider and API token."""
if provenance is None:
provenance = Provenance.TRUSTED
# Defense in depth: untrusted callers can already not reach here (the
# type gate forbids constructing LLMConfig from a request body), but if
# they ever do, never resolve env vars or read provider keys from the
# environment - that is the credential-exfil gadget.
if provenance == Provenance.UNTRUSTED:
if api_token and api_token.startswith("env:"):
raise UntrustedConfigError(
"LLMConfig.api_token may not reference an environment variable "
"from an untrusted request"
)
self.provider = provider
self.api_token = api_token # never os.getenv
self.base_url = base_url
self.temperature = temperature
self.max_tokens = max_tokens
self.top_p = top_p
self.frequency_penalty = frequency_penalty
self.presence_penalty = presence_penalty
self.stop = stop
self.n = n
self.backoff_base_delay = backoff_base_delay if backoff_base_delay is not None else 2
self.backoff_max_attempts = backoff_max_attempts if backoff_max_attempts is not None else 3
self.backoff_exponential_factor = backoff_exponential_factor if backoff_exponential_factor is not None else 2
return
self.provider = providerView on GitHub (pinned to 7e80152142)
Solutions
- Send literal tokens in untrusted/request-sourced configs; reserve "env:VAR" references for server-side (TRUSTED provenance) configs
- In your API layer, reject or sanitize any client field starting with 'env:' before it reaches LLMConfig
- If you legitimately need an env var server-side, construct LLMConfig in trusted code and never pass client provenance
Example fix
// before (server handling untrusted request body)
llm_cfg = LLMConfig(api_token=body["api_token"], provenance=Provenance.UNTRUSTED)
// body["api_token"] == "env:OPENAI_API_KEY" -> raises
// after
if str(body.get("api_token", "")).startswith("env:"):
abort(400, "env-referenced tokens are not allowed")
llm_cfg = LLMConfig(api_token=body["api_token"], provenance=Provenance.UNTRUSTED) Defensive patterns
Strategy: validation
Validate before calling
def safe_untrusted_llm_config(body: dict):
tok = body.get("api_token")
if isinstance(tok, str) and tok.startswith("env:"):
raise HTTPException(400, "env-referenced tokens are not allowed in requests")
return LLMConfig(provider=body.get("provider"), api_token=tok,
provenance=Provenance.UNTRUSTED) Type guard
def is_literal_token(tok) -> bool:
return tok is None or (isinstance(tok, str) and not tok.startswith("env:")) Try / catch
from crawl4ai.exceptions import UntrustedConfigError
try:
llm_cfg = LLMConfig(api_token=client_token, provenance=Provenance.UNTRUSTED)
except UntrustedConfigError:
abort(400, "invalid api_token") # reject, never fall back to env resolution Prevention
- Reject any client-supplied 'env:' prefixed secret at the API boundary
- Construct env-var-backed LLMConfigs only in trusted server code
- Never echo resolved tokens in logs or responses
When it happens
Trigger: Constructing LLMConfig(api_token="env:MY_SECRET", provenance=Provenance.UNTRUSTED); an API server deserializing a client-supplied LLM config with an env: token reference instead of a literal token.
Common situations: Building a crawl-as-a-service layer where clients submit extraction configs; test code explicitly exercising the untrusted path; server operators who want env-var tokens server-side while clients send literal tokens.
Related errors
- Rejected config: {e}
- invalid header name {name!r}
- control characters in value for header {name!r}
- URL must start with {schemes}
- type must be 'CrawlerRunConfig' or 'BrowserConfig'
AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14).
Data as JSON: /api/errors/e701f804c18f747f.
Report an issue: GitHub.