{"record":{"id":"93d87aa227a2b2d3","repo":"unclecode/crawl4ai","slug":"control-characters-in-value-for-header-name-r","errorCode":null,"errorMessage":"control characters in value for header {name!r}","messagePattern":"control characters in value for header (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"deploy/docker/hook_registry.py","lineNumber":78,"sourceCode":"\n\nclass AddCookiesParams(BaseModel):\n    cookies: List[_Cookie] = Field(..., min_length=1, max_length=_MAX_COOKIES)\n\n\nclass SetHeadersParams(BaseModel):\n    headers: Dict[str, str]\n\n    @field_validator(\"headers\")\n    @classmethod\n    def _check(cls, v):\n        if len(v) > _MAX_HEADERS:\n            raise ValueError(f\"too many headers (max {_MAX_HEADERS})\")\n        for name, value in v.items():\n            if not _HEADER_NAME_RE.match(name):\n                raise ValueError(f\"invalid header name {name!r}\")\n            if any(c in value for c in \"\\r\\n\\x00\"):\n                raise ValueError(f\"control characters in value for header {name!r}\")\n        return v\n\n\nclass ScrollToBottomParams(BaseModel):\n    max_steps: int = Field(10, ge=1, le=_MAX_SCROLL_STEPS)\n    delay_ms: int = Field(500, ge=0, le=_MAX_SCROLL_DELAY_MS)\n\n\nclass WaitForTimeoutParams(BaseModel):\n    timeout_ms: int = Field(..., ge=0, le=_MAX_WAIT_MS)\n\n\n# ───────────────────────── server-authored hook factories ─────────────────────────\ndef _factory_block_resources(p: BlockResourcesParams):\n    types = set(p.resource_types)\n\n    async def hook(page, **kwargs):\n        context = kwargs.get(\"context\")","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/deploy/docker/hook_registry.py#L60-L96","documentation":"SetHeadersParams' validator rejects header values containing \\r, \\n, or \\x00. This is a classic CRLF/header-injection guard: a newline in a value would let a crafted config smuggle extra headers or split responses. Raised while validating a set_headers hook spec.","triggerScenarios":"Passing a multi-line value (e.g. a pasted cookie block or a value with a trailing \\n), or embedding CR/LF/NUL in any set_headers value.","commonSituations":"Pasting a value from a file that keeps a trailing newline; building values with '\\n'.join(...) for multi-value headers instead of the comma convention; JSON configs where an escaped \\n slips into a string.","solutions":["Strip/replace newlines: value.strip().replace('\\r', '').replace('\\n', '').","Encode multi-value headers per RFC: 'Accept-Encoding: gzip, deflate, br' (comma-separated, single line).","If a NUL appears, the value is binary — pass it base64-encoded or not at all."],"exampleFix":"# before\n{\"headers\": {\"Cookie\": \"a=1\\nb=2\\n\"}}\n\n# after\n{\"headers\": {\"Cookie\": \"a=1; b=2\"}}","handlingStrategy":"validation","validationCode":"def clean_header_value(v: str) -> str:\n    return v.replace(\"\\r\", \"\").replace(\"\\n\", \"\").replace(\"\\x00\", \"\").strip()\n\ndef safe_headers(headers: dict) -> dict:\n    return {k: clean_header_value(v) for k, v in headers.items()}","typeGuard":"def is_crlf_free(value) -> bool:\n    return isinstance(value, str) and not any(c in value for c in \"\\r\\n\\x00\")","tryCatchPattern":"try:\n    hooks = build_declarative_hooks(specs)\nexcept HookValidationError as e:\n    if \"control characters\" in str(e):\n        specs = sanitize_header_values(specs)  # strip \\r\\n\\x00, rebuild\n    else:\n        raise","preventionTips":["Strip newlines from any value pasted from files or env vars.","Use comma/semicolon joining for multi-value headers, never '\\n'.","Unit-test generated webhook/cookie/header configs for CRLF before submit."],"tags":["validation","security","header-injection","hooks"],"backgroundTag":null,"analyzedSha":"7e801521428ee12509994d39151006f64055ebe3","analyzedAt":"2026-08-14T20:46:20.673Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}