{"record":{"id":"2e996438a50ab455","repo":"Hmbown/CodeWhale","slug":"unsafe-mcp-http-header-name","errorCode":null,"errorMessage":"unsafe MCP HTTP header '{name}'","messagePattern":"unsafe MCP HTTP header '(.+?)'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp/oauth.rs","lineNumber":592,"sourceCode":"    env_headers: &HashMap<String, String>,\n) -> Result<HeaderMap> {\n    let mut headers = HeaderMap::new();\n    for (name, value) in http_headers {\n        insert_header(&mut headers, name, value)?;\n    }\n    for (name, env_var) in env_headers {\n        if let Ok(value) = std::env::var(env_var)\n            && !value.trim().is_empty()\n        {\n            insert_header(&mut headers, name, &value)?;\n        }\n    }\n    Ok(headers)\n}\n\nfn insert_header(headers: &mut HeaderMap, name: &str, value: &str) -> Result<()> {\n    if !super::headers::is_safe_custom_header(name, value) {\n        bail!(\"unsafe MCP HTTP header '{name}'\");\n    }\n    let name = HeaderName::from_bytes(name.as_bytes())\n        .with_context(|| format!(\"invalid MCP HTTP header name '{name}'\"))?;\n    let value = HeaderValue::from_str(value).with_context(|| \"invalid MCP HTTP header value\")?;\n    headers.insert(name, value);\n    Ok(())\n}\n\npub fn apply_default_headers(\n    builder: reqwest::ClientBuilder,\n    headers: &HeaderMap,\n) -> reqwest::ClientBuilder {\n    if headers.is_empty() {\n        builder\n    } else {\n        builder.default_headers(headers.clone())\n    }\n}","sourceCodeStart":574,"sourceCodeEnd":610,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/mcp/oauth.rs#L574-L610","documentation":"insert_header (oauth.rs:592) validates every header built by build_default_headers with super::headers::is_safe_custom_header (headers.rs:38), which rejects three cases: empty/whitespace-only header names, names that override the protocol-framing headers Accept or Content-Type, and values containing CR or LF (response-splitting defense). Unlike apply_safe_custom_headers, which only warns and skips, the default-headers builder hard-fails the whole call because the map is constructed up front.","triggerScenarios":"An McpServerConfig headers or env_headers entry has an empty key, an `Accept`/`Content-Type` key (any case), or a value with embedded \\r or \\n. For env_headers the value comes from the environment variable at runtime, so a token file read with a trailing newline triggers it.","commonSituations":"TOKEN=$(cat token.txt) leaves a trailing newline that lands in a header value; copy-pasted header blocks that set Accept: text/event-stream, fighting the transport's own negotiation; YAML/TOML template with an empty header placeholder key.","solutions":["Strip CR/LF from environment values before they reach config: tr -d '\\r\\n' < token.txt","Rename or remove empty header keys and any Accept/Content-Type overrides — the transport sets those itself","Validate the header map with the same three rules before passing config to the OAuth/login path"],"exampleFix":"# before\nexport MY_SERVER_TOKEN=\"$(cat token.txt)\"   # trailing \\n in value\n[mcp.servers.my-server.env-headers]\nX-Api-Token = \"MY_SERVER_TOKEN\"\nAccept = \"text/event-stream\"\n\n# after\nexport MY_SERVER_TOKEN=\"$(tr -d '\\r\\n' < token.txt)\"\n[mcp.servers.my-server.env-headers]\nX-Api-Token = \"MY_SERVER_TOKEN\"","handlingStrategy":"validation","validationCode":"fn is_safe_custom_header(key: &str, value: &str) -> bool {\n    let trimmed = key.trim();\n    !trimmed.is_empty()\n        && !trimmed.eq_ignore_ascii_case(\"accept\")\n        && !trimmed.eq_ignore_ascii_case(\"content-type\")\n        && !value.contains('\\r')\n        && !value.contains('\\n')\n}\n\nfor (k, v) in &server.headers {\n    assert!(is_safe_custom_header(k, v), \"unsafe header {k:?}\");\n}","typeGuard":null,"tryCatchPattern":"match build_default_headers(&server.headers, &server.env_headers) {\n    Err(err) if err.to_string().contains(\"unsafe MCP HTTP header\") => {\n        // strip the named header from config and retry once\n    }\n    other => other?,\n}","preventionTips":["Trim CR/LF from any secret read from files or env before it becomes a header value","Never configure Accept or Content-Type on MCP servers — the transport owns protocol framing","Validate header maps with the same three rules in config-loading code so mistakes fail at load, not at connect"],"tags":["http-headers","mcp","security","configuration","response-splitting"],"backgroundTag":"header-validation-failed","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}