{"record":{"id":"2d24a76fc157b0b6","repo":"ruvnet/ruflo","slug":"forbidden-header","errorCode":"FORBIDDEN_HEADER","errorMessage":"header \"${key}\" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1","messagePattern":"header \"(.+?)\" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1","errorType":"validation","errorClass":"HttpFetchValidationError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts","lineNumber":103,"sourceCode":"    if (a === 169 && b === 254) return true;          // link-local\n    if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT\n    return false;\n  }\n  // IPv6 bracketed addresses and other special forms — be conservative and\n  // accept only public-looking literals. Reject obvious private/local forms.\n  if (host.startsWith('fc') || host.startsWith('fd')) return true;  // fc00::/7 ULA\n  if (host.startsWith('fe80:')) return true;                        // link-local\n  return false;\n}\n\nexport function validateHeaders(headers: Record<string, string>): Record<string, string> {\n  const allowAuth = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH === '1';\n  const out: Record<string, string> = {};\n  for (const [key, value] of Object.entries(headers)) {\n    const lower = key.toLowerCase();\n    if (!allowAuth) {\n      if ((FORBIDDEN_HEADERS_EXACT as readonly string[]).includes(lower)) {\n        throw new HttpFetchValidationError(\n          `header \"${key}\" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`,\n          'FORBIDDEN_HEADER',\n        );\n      }\n      if (FORBIDDEN_HEADER_PREFIXES.some((p) => lower.startsWith(p))) {\n        throw new HttpFetchValidationError(\n          `header \"${key}\" is not allowed without CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1`,\n          'FORBIDDEN_HEADER',\n        );\n      }\n    }\n    if (typeof value !== 'string') {\n      throw new HttpFetchValidationError(\n        `header \"${key}\" must be a string`,\n        'INVALID_HEADER_VALUE',\n      );\n    }\n    out[key] = value;","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts#L85-L121","documentation":"HttpFetchValidationError with code FORBIDDEN_HEADER, thrown by validateHeaders() (http-fetch-tools.ts:103) when a request header is credential-bearing: exactly 'authorization', 'cookie', 'set-cookie', or 'proxy-authorization', or any header starting with 'x-auth-' / 'x-api-key'. These are blocked unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1, so secrets are not exfiltrated through an MCP fetch tool (tool arguments are commonly logged and visible to the agent host).","triggerScenarios":"Calling http_fetch with headers: { Authorization: 'Bearer sk-...' }, { Cookie: 'session=...' }, { 'x-api-key': '...' }, or { 'X-Auth-Token': '...' } while CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH is not '1'. The check is case-insensitive (keys are lowercased) and prefix-based for x-auth-* and x-api-key*.","commonSituations":"Treating http_fetch like curl/fetch and pasting an API key header out of habit; calling internal APIs that require auth; CI pipelines forwarding credentials; users unaware the guard exists and confused why 'normal' headers fail.","solutions":["If sending credentials from this tool is intended and the environment is controlled, set CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 in the MCP server env and restart","Prefer routing authenticated calls through your own service so secrets never enter tool arguments","Never paste raw tokens into tool args even when allowed — they can be logged in transcripts"],"exampleFix":"# before\n# http_fetch { url: 'https://api.example.com/me', headers: { Authorization: 'Bearer ...' } }\n# -> FORBIDDEN_HEADER: header \"Authorization\" is not allowed\n\n# after (deliberate, trusted environment)\n$ CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 claude-flow mcp start\n# http_fetch { url: '...', headers: { Authorization: 'Bearer ...' } } -> ok","handlingStrategy":"validation","validationCode":"const FORBIDDEN_EXACT = new Set(['authorization', 'cookie', 'set-cookie', 'proxy-authorization']);\nfunction isCredentialHeader(name: string): boolean {\n  const lower = name.toLowerCase();\n  return FORBIDDEN_EXACT.has(lower) || lower.startsWith('x-auth-') || lower.startsWith('x-api-key');\n}\nconst allowAuth = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH === '1';\nconst headerNames = Object.keys(headers ?? {});\nif (!allowAuth && headerNames.some(isCredentialHeader)) {\n  throw new TypeError('credential headers require CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await callTool('http_fetch', { url, headers });\n} catch (e) {\n  if (e instanceof HttpFetchValidationError && e.code === 'FORBIDDEN_HEADER') {\n    // fall back: strip the credential header and call an unauthenticated endpoint, or proxy via your own service\n    const safeHeaders = Object.fromEntries(Object.entries(headers).filter(([k]) => !isCredentialHeader(k)));\n    return callTool('http_fetch', { url, headers: safeHeaders });\n  }\n  throw e;\n}","preventionTips":["Route authenticated API calls through your own backend; keep secrets out of tool arguments entirely","If credential headers are required, start the MCP server with CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 in a controlled environment","Never paste raw bearer tokens into tool args — transcripts and logs retain them"],"tags":["security","http","headers","credentials","mcp"],"backgroundTag":"forbidden-auth-header","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}