{"record":{"id":"e701f804c18f747f","repo":"unclecode/crawl4ai","slug":"llmconfig-api-token-may-not-reference-an-environme","errorCode":null,"errorMessage":"LLMConfig.api_token may not reference an environment variable from an untrusted request","messagePattern":"LLMConfig\\.api_token may not reference an environment variable from an untrusted request","errorType":"validation","errorClass":"UntrustedConfigError","httpStatus":null,"severity":"critical","filePath":"crawl4ai/async_configs.py","lineNumber":2243,"sourceCode":"        frequency_penalty: Optional[float] = None,\n        presence_penalty: Optional[float] = None,\n        stop: Optional[List[str]] = None,\n        n: Optional[int] = None,\n        backoff_base_delay: Optional[int] = None,\n        backoff_max_attempts: Optional[int] = None,\n        backoff_exponential_factor: Optional[int] = None,\n        provenance: \"Provenance\" = None,\n    ):\n        \"\"\"Configuaration class for LLM provider and API token.\"\"\"\n        if provenance is None:\n            provenance = Provenance.TRUSTED\n        # Defense in depth: untrusted callers can already not reach here (the\n        # type gate forbids constructing LLMConfig from a request body), but if\n        # they ever do, never resolve env vars or read provider keys from the\n        # environment - that is the credential-exfil gadget.\n        if provenance == Provenance.UNTRUSTED:\n            if api_token and api_token.startswith(\"env:\"):\n                raise UntrustedConfigError(\n                    \"LLMConfig.api_token may not reference an environment variable \"\n                    \"from an untrusted request\"\n                )\n            self.provider = provider\n            self.api_token = api_token  # never os.getenv\n            self.base_url = base_url\n            self.temperature = temperature\n            self.max_tokens = max_tokens\n            self.top_p = top_p\n            self.frequency_penalty = frequency_penalty\n            self.presence_penalty = presence_penalty\n            self.stop = stop\n            self.n = n\n            self.backoff_base_delay = backoff_base_delay if backoff_base_delay is not None else 2\n            self.backoff_max_attempts = backoff_max_attempts if backoff_max_attempts is not None else 3\n            self.backoff_exponential_factor = backoff_exponential_factor if backoff_exponential_factor is not None else 2\n            return\n        self.provider = provider","sourceCodeStart":2225,"sourceCodeEnd":2261,"githubUrl":"https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/async_configs.py#L2225-L2261","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (server handling untrusted request body)\nllm_cfg = LLMConfig(api_token=body[\"api_token\"], provenance=Provenance.UNTRUSTED)\n// body[\"api_token\"] == \"env:OPENAI_API_KEY\" -> raises\n// after\nif str(body.get(\"api_token\", \"\")).startswith(\"env:\"):\n    abort(400, \"env-referenced tokens are not allowed\")\nllm_cfg = LLMConfig(api_token=body[\"api_token\"], provenance=Provenance.UNTRUSTED)","handlingStrategy":"validation","validationCode":"def safe_untrusted_llm_config(body: dict):\n    tok = body.get(\"api_token\")\n    if isinstance(tok, str) and tok.startswith(\"env:\"):\n        raise HTTPException(400, \"env-referenced tokens are not allowed in requests\")\n    return LLMConfig(provider=body.get(\"provider\"), api_token=tok,\n                     provenance=Provenance.UNTRUSTED)","typeGuard":"def is_literal_token(tok) -> bool:\n    return tok is None or (isinstance(tok, str) and not tok.startswith(\"env:\"))","tryCatchPattern":"from crawl4ai.exceptions import UntrustedConfigError\n\ntry:\n    llm_cfg = LLMConfig(api_token=client_token, provenance=Provenance.UNTRUSTED)\nexcept UntrustedConfigError:\n    abort(400, \"invalid api_token\")  # reject, never fall back to env resolution","preventionTips":["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"],"tags":["security","secrets","validation","llm-config","provenance"],"backgroundTag":null,"analyzedSha":"7e801521428ee12509994d39151006f64055ebe3","analyzedAt":"2026-08-14T20:46:20.673Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}