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 = provider

View on GitHub (pinned to 7e80152142)

Solutions

  1. Send literal tokens in untrusted/request-sourced configs; reserve "env:VAR" references for server-side (TRUSTED provenance) configs
  2. In your API layer, reject or sanitize any client field starting with 'env:' before it reaches LLMConfig
  3. 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

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


AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14). Data as JSON: /api/errors/e701f804c18f747f. Report an issue: GitHub.