{"record":{"id":"58c65012a8bb2d09","repo":"different-ai/openwork","slug":"an-https-request-cannot-redirect-to-a-less-secure","errorCode":null,"errorMessage":"an HTTPS request cannot redirect to a less secure protocol","messagePattern":"an HTTPS request cannot redirect to a less secure protocol","errorType":"exception","errorClass":"LocalManagedMcpPrivateUrlError","httpStatus":null,"severity":"error","filePath":"apps/server/src/local-managed-mcp-url-guard.ts","lineNumber":208,"sourceCode":"\ntype FetchLike = (url: string | URL, init?: RequestInit) => Promise<Response>;\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\nconst guardedDispatcher = new Agent({\n  connect: {\n    lookup: createLocalManagedMcpPublicLookup(),\n    // Node's 250 ms family-attempt default is too aggressive for otherwise\n    // healthy dual-stack MCP providers on some macOS networks. Keep fallback\n    // enabled, but give the first family enough time to establish TLS before\n    // trying the validated alternative address.\n    autoSelectFamily: true,\n    autoSelectFamilyAttemptTimeout: 1_000,\n  },\n});\n\nfunction redirectedRequestInit(init: RequestInit | undefined, status: number, from: URL, to: URL): RequestInit {\n  const headers = new Headers(init?.headers);\n  if (from.protocol === \"https:\" && to.protocol !== \"https:\") {\n    throw new LocalManagedMcpPrivateUrlError(to.toString(), \"an HTTPS request cannot redirect to a less secure protocol\");\n  }\n  const method = (init?.method ?? \"GET\").toUpperCase();\n  if (from.origin !== to.origin) {\n    if ((method !== \"GET\" && method !== \"HEAD\") || init?.body != null) {\n      throw new LocalManagedMcpPrivateUrlError(to.toString(), \"a request body cannot be redirected to another origin\");\n    }\n    for (const name of [\"authorization\", \"cookie\", \"proxy-authorization\", \"mcp-session-id\", \"last-event-id\", \"x-api-key\", \"x-auth-token\"]) {\n      headers.delete(name);\n    }\n  }\n  const switchToGet = (status === 303 && method !== \"HEAD\") || ((status === 301 || status === 302) && method === \"POST\");\n  if (switchToGet) {\n    headers.delete(\"content-length\");\n    headers.delete(\"content-type\");\n    return { ...init, method: \"GET\", body: undefined, headers, redirect: \"manual\" };\n  }\n  return { ...init, headers, redirect: \"manual\" };\n}","sourceCodeStart":190,"sourceCodeEnd":226,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/local-managed-mcp-url-guard.ts#L190-L226","documentation":"When following redirects, the guarded fetch refuses to let an https: request be redirected to a non-https protocol (http:, ftp:, etc.). This blocks protocol-downgrade attacks during the redirect chain and throws LocalManagedMcpPrivateUrlError for the target URL.","triggerScenarios":"An HTTPS MCP request receives a 301/302/303/307/308 response whose Location header points at an http: (or otherwise non-https) URL, while private URLs are not allowed.","commonSituations":"Misconfigured reverse proxy or load balancer redirecting https to http; redirect target built with a wrong scheme in a server config; intercepted redirect chain during corporate proxy rewriting.","solutions":["Fix the server/proxy redirect so it targets an https: URL","If the destination only supports HTTP, use it directly as an intentionally insecure connection via the allowPrivateUrls() path (dev only)","Trace the redirect chain (curl -I) to find which hop downgrades the scheme","Update hardcoded redirect/base-URL config to include the https scheme"],"exampleFix":"// before (server)\nreturn Response.redirect(\"http://mcp.example.com/v2\", 302);\n// after\nreturn Response.redirect(\"https://mcp.example.com/v2\", 302);","handlingStrategy":"try-catch","validationCode":"const res = await fetch(url, { redirect: \"manual\" });\nif ([301,302,303,307,308].includes(res.status)) {\n  const next = new URL(res.headers.get(\"location\"), url);\n  if (next.protocol !== \"https:\") throw new Error(`Redirect downgrades to ${next.protocol}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await createLocalManagedMcpConnection({ url });\n} catch (error) {\n  if (error instanceof LocalManagedMcpPrivateUrlError && error.message.includes(\"less secure protocol\")) {\n    // inspect server redirect chain; fix scheme on the redirecting hop\n  }\n  throw error;\n}","preventionTips":["Audit server redirects so they always target https://","Check TLS termination at the proxy preserves the original scheme","Trace redirect chains with curl -sIL when configuring new endpoints","Treat any http Location header on a production endpoint as a bug"],"tags":["security","https","redirect","tls-downgrade"],"backgroundTag":"https-to-http-redirect-downgrade","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}