{"record":{"id":"0e347f1c6aba3aa1","repo":"vercel/ai","slug":"disallowed-ui-open-link-scheme-scheme","errorCode":null,"errorMessage":"Disallowed ui/open-link scheme: ${scheme}","messagePattern":"Disallowed ui/open-link scheme: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/react/src/mcp-apps/bridge.ts","lineNumber":98,"sourceCode":"\n/**\n * Validates `ui/open-link` params and allows only `https:`/`http:`/`mailto:`\n * URLs.\n */\nfunction assertOpenLinkParams(params: unknown): { url: string } {\n  if (!isJSONObject(params) || typeof params.url !== 'string') {\n    throw new Error('Invalid ui/open-link params');\n  }\n\n  let scheme: string;\n  try {\n    scheme = new URL(params.url).protocol;\n  } catch {\n    throw new Error(`Invalid ui/open-link url: ${params.url}`);\n  }\n\n  if (scheme !== 'https:' && scheme !== 'http:' && scheme !== 'mailto:') {\n    throw new Error(`Disallowed ui/open-link scheme: ${scheme}`);\n  }\n\n  return { url: params.url };\n}\n\n/**\n * Validates params for `ui/request-display-mode`.\n */\nfunction assertDisplayModeParams(params: unknown): {\n  mode: 'inline' | 'fullscreen' | 'pip';\n} {\n  if (\n    !isJSONObject(params) ||\n    (params.mode !== 'inline' &&\n      params.mode !== 'fullscreen' &&\n      params.mode !== 'pip')\n  ) {\n    throw new Error('Invalid ui/request-display-mode params');","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/react/src/mcp-apps/bridge.ts#L80-L116","documentation":"This is a security allow-list on `ui/open-link`: the iframe may only ask the host to open `https:`, `http:`, or `mailto:` URLs. Any other scheme is rejected so untrusted apps cannot trigger `javascript:`, `data:`, `file:`, or custom-protocol navigations from the host page.","triggerScenarios":"The iframe sends `ui/open-link` with a well-formed URL whose protocol is not in the allow-list, e.g. `javascript:alert(1)`, `data:text/html,...`, `vscode://...`, or `intent://...`.","commonSituations":"App tries to deep-link into a desktop app via custom scheme; malicious/compromised iframe attempting XSS via `javascript:` urls; app embedding `data:` URLs generated from content.","solutions":["Use an `https://` (or `http:`/`mailto:`) URL instead of a custom scheme.","If a custom protocol is genuinely needed, implement a host-side `openLink` wrapper and a dedicated allow-listed tool, not ui/open-link.","Serve deep-link content via a regular web URL that redirects to the app scheme.","Audit the app's link sources to ensure no untrusted `javascript:`/`data:` values can reach ui/open-link."],"exampleFix":"// before\nopenLink({ url: 'javascript:void(window.print())' })\n// after\nopenLink({ url: 'https://example.com/print' })","handlingStrategy":"validation","validationCode":"// app-side, before requesting:\nconst protocol = new URL(url).protocol;\nif (protocol !== 'https:' && protocol !== 'http:' && protocol !== 'mailto:') {\n  throw new Error(`Scheme ${protocol} cannot be opened; use https/http/mailto`);\n}","typeGuard":"function isAllowedLinkUrl(value: string): boolean {\n  try {\n    const p = new URL(value).protocol;\n    return p === 'https:' || p === 'http:' || p === 'mailto:';\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await openLink({ url });\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith('Disallowed ui/open-link scheme')) {\n    console.warn('Blocked non-web scheme; convert deep link to an https redirect url');\n  }\n}","preventionTips":["Only generate https/http/mailto links in app UI.","Never pass raw user or model text directly as an open-link url (javascript:/data: risk).","Replace custom deep-link schemes with web URLs that redirect.","Sanitize link sources (CMS, LLM output) before opening."],"tags":["mcp-apps","security","url","iframe","policy"],"backgroundTag":"url-scheme-not-allowed","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}