{"record":{"id":"76ad5a868dd7184a","repo":"different-ai/openwork","slug":"external-url-protocol-parsed-protocol-is-not","errorCode":null,"errorMessage":"External URL protocol \"${parsed.protocol}\" is not allowed.","messagePattern":"External URL protocol \"(.+?)\" is not allowed\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/app/src/app/lib/desktop.ts","lineNumber":478,"sourceCode":"  }\n  return desktopFetchThroughMain(input, init, {\n    agentContextDiagnosticsDeadlineAtMs: deadlineAtMs,\n  });\n}\n\n// ---------------------------------------------------------------------------\n// Convenience wrappers\n// ---------------------------------------------------------------------------\n\nexport function assertDesktopWebUrl(url: string): string {\n  let parsed: URL;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error(\"Only valid web links can be opened externally.\");\n  }\n  if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n    throw new Error(`External URL protocol \"${parsed.protocol}\" is not allowed.`);\n  }\n  return parsed.toString();\n}\n\nexport async function openDesktopUrl(url: string): Promise<void> {\n  const safeUrl = assertDesktopWebUrl(url);\n  const openExternal = window.__OPENWORK_ELECTRON__?.shell?.openExternal;\n  if (openExternal) {\n    const result = await openExternal(safeUrl);\n    if (result && result.ok === false) {\n      throw new Error(result.error ?? \"Failed to open browser\");\n    }\n    return;\n  }\n  if (typeof window !== \"undefined\") {\n    window.open(safeUrl, \"_blank\", \"noopener,noreferrer\");\n  }\n}","sourceCodeStart":460,"sourceCodeEnd":496,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/app/src/app/lib/desktop.ts#L460-L496","documentation":"After the URL parses, assertDesktopWebUrl enforces an allow-list of protocols: only http: and https: may be opened externally. Anything else — file:, javascript:, data:, custom schemes — is rejected with a message naming the offending protocol, blocking protocol-handler abuse (e.g. opening local files or arbitrary app handlers via crafted links).","triggerScenarios":"openDesktopUrl / safeUrl / waitForManagedMcpAuthorization receiving a URL whose scheme is not http/https, e.g. `file:///Users/x/doc.pdf`, `javascript:void(0)`, `data:text/html,...`, or a custom app scheme like `slack://` or `vscode://` passed to the external opener.","commonSituations":"Attempting to reveal a local file path via openDesktopUrl instead of openDesktopPath; rendering attacker-influenced link content (markdown/chat) and opening it directly; passing internal app schemes or workspace:// URIs that the desktop bridge does not allow; tests constructing `about:blank` or `blob:` URLs.","solutions":["Convert non-web inputs to a permitted form first: local files should use openDesktopPath/revealDesktopItemInDir, not openDesktopUrl.","Normalize the scheme: if the target is meant to be a website but uses a wrong scheme, rewrite it to https:// before calling.","If a custom scheme genuinely must open, add an explicit allow-listed code path (e.g. shell.openExternal via the Electron bridge) rather than bypassing the guard silently.","Sanitize dynamic link sources (chat messages, imported configs) to reject or rewrite non-http(s) URLs at ingestion."],"exampleFix":"// before\nawait openDesktopUrl(\"file:///tmp/report.pdf\");\n\n// after\nawait openDesktopPath(\"/tmp/report.pdf\"); // opens/locally reveals instead","handlingStrategy":"validation","validationCode":"const parsed = new URL(url);\nif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n  throw new Error(`Refusing to open ${parsed.protocol} link externally`);\n}","typeGuard":"function isWebUrl(u: URL): boolean {\n  return u.protocol === \"http:\" || u.protocol === \"https:\";\n}","tryCatchPattern":"try {\n  await openDesktopUrl(link);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"is not allowed\")) {\n    showToast(\"Only web (http/https) links can be opened in the browser\");\n  } else { throw err; }\n}","preventionTips":["Route local files to openDesktopPath/revealDesktopItemInDir, never openDesktopUrl.","Sanitize dynamic link content (chat, markdown, imports) to http(s) only at ingestion.","Never attempt to open blob:, data:, or custom app schemes through this helper.","Keep an explicit, reviewed allow-list if custom schemes ever need support."],"tags":["security","protocol-allowlist","url-validation","desktop-integration"],"backgroundTag":"url-protocol-not-allowed","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}