{"record":{"id":"e1f02dce7f46c21d","repo":"musistudio/claude-code-router","slug":"artifact-origin-is-not-the-configured-ccr-gateway","errorCode":null,"errorMessage":"Artifact origin is not the configured CCR gateway.","messagePattern":"Artifact origin is not the configured CCR gateway\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/agents/codex/media-preview-bridge.ts","lineNumber":276,"sourceCode":"      if (target) return target;\n    } catch (error) {\n      lastError = error;\n    }\n    await sleep(codexMediaPreviewPollIntervalMs);\n  }\n  throw new Error(`Codex App CDP page target was not available${lastError ? `: ${redactBridgeError(lastError)}` : \".\"}`);\n}\n\nfunction isCodexAppPageTarget(target: DevToolsTarget): boolean {\n  if (target.type !== \"page\" || !target.webSocketDebuggerUrl) return false;\n  const url = target.url || \"\";\n  return url.startsWith(\"app://codex\") || url.startsWith(\"app://chatgpt\") || /\\b(codex|chatgpt)\\b/i.test(target.title || \"\");\n}\n\nfunction validateCodexMediaArtifactUrl(value: string, endpoint: string): ValidatedArtifactUrl {\n  const expected = new URL(endpoint);\n  const url = new URL(value);\n  if (url.protocol !== \"http:\" || url.origin !== expected.origin) throw new Error(\"Artifact origin is not the configured CCR gateway.\");\n  if (url.username || url.password || url.hash) throw new Error(\"Artifact URL contains unsupported credentials or fragments.\");\n  if (!url.pathname.startsWith(MEDIA_ARTIFACT_PATH_PREFIX)) throw new Error(\"Artifact URL does not use the CCR media artifact path.\");\n  const encodedId = url.pathname.slice(MEDIA_ARTIFACT_PATH_PREFIX.length);\n  if (!encodedId || encodedId.includes(\"/\")) throw new Error(\"Artifact URL contains an invalid identifier.\");\n  const artifactId = decodeURIComponent(encodedId);\n  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(artifactId)) {\n    throw new Error(\"Artifact URL contains an invalid identifier.\");\n  }\n  const keys = [...url.searchParams.keys()];\n  const token = url.searchParams.get(\"token\") || \"\";\n  if (keys.length !== 1 || keys[0] !== \"token\" || !/^[A-Za-z0-9_-]{32}$/.test(token)) {\n    throw new Error(\"Artifact URL contains an invalid access token.\");\n  }\n  return { artifactId, url };\n}\n\nasync function loadCodexMediaArtifact(validated: ValidatedArtifactUrl, signal: AbortSignal): Promise<LoadedMediaArtifact> {\n  let response: Response;","sourceCodeStart":258,"sourceCodeEnd":294,"githubUrl":"https://github.com/musistudio/claude-code-router/blob/99f24806c6a2c660b16e53e95211c517448a6c90/packages/core/src/agents/codex/media-preview-bridge.ts#L258-L294","documentation":"validateCodexMediaArtifactUrl enforces that a media artifact URL is a plain http URL whose origin exactly matches the configured CCR gateway endpoint. If the protocol is not http: or url.origin differs from the endpoint's origin, it throws this — an SSRF-style guard ensuring artifacts are only fetched from the trusted gateway.","triggerScenarios":"Passing an artifact URL whose host/port/scheme differs from the configured gateway endpoint: https vs http, a different hostname or port, localhost vs 127.0.0.1, or a fully external URL.","commonSituations":"Gateway endpoint config says http://127.0.0.1:PORT but artifact URLs were generated with http://localhost:PORT (or the machine's LAN IP); endpoint configured behind an https proxy while artifacts use http; a crafted/misrouted URL from an untrusted message payload.","solutions":["Make the configured gateway endpoint origin exactly match the artifact URL origin (same scheme, host, port)","Regenerate artifact URLs from the same gateway endpoint string used in configuration","Normalize host aliases (localhost vs 127.0.0.1) on both sides before validating"],"exampleFix":"// before\nvalidateCodexMediaArtifactUrl(artifactUrl, \"http://127.0.0.1:8080\"); // artifactUrl is http://localhost:8080/...\n// after\nvalidateCodexMediaArtifactUrl(artifactUrl, \"http://localhost:8080\"); // origins now match","handlingStrategy":"validation","validationCode":"function sameOrigin(artifactUrl: string, endpoint: string): boolean {\n  try { return new URL(artifactUrl).origin === new URL(endpoint).origin; }\n  catch { return false; }\n}\nif (!sameOrigin(url, ccrEndpoint)) throw new Error('skipping artifact from untrusted origin');","typeGuard":"function isTrustedArtifactOrigin(value: string, endpoint: string): boolean {\n  try { const u = new URL(value); return u.protocol === 'http:' && u.origin === new URL(endpoint).origin; } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Derive artifact URLs and the endpoint config from one constant","Normalize localhost/127.0.0.1 aliases before comparing origins","Never feed user-supplied URLs straight into the validator"],"tags":["ssrf","url-validation","origin","gateway"],"backgroundTag":"url-origin-mismatch","analyzedSha":"99f24806c6a2c660b16e53e95211c517448a6c90","analyzedAt":"2026-08-27T04:11:01.184Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}