{"record":{"id":"01c6f15e0a11b803","repo":"ruvnet/ruflo","slug":"refusing-to-open-non-http-s-url-scheme-parsed","errorCode":null,"errorMessage":"refusing to open non-http(s) URL scheme: ${parsed.protocol}","messagePattern":"refusing to open non-http\\(s\\) URL scheme: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/security/src/oauth/browser.ts","lineNumber":27,"sourceCode":" * authorize URL's query string (`?a=1&b=2&...`), so passing one through would\n * false-positive-reject on every real invocation. That blocklist is the wrong\n * tool for a URL argument: with `shell: false` (used here, same as\n * `SafeExecutor`), a single argv element containing `&` is inert — there's no\n * shell to interpret it. The actual safety property that matters is \"the URL\n * was constructed by us from validated components, never from raw external\n * input\" (this module's `authorizeUrl()` in `client.ts` is the only caller),\n * which `assertSafeUrl` below checks directly instead.\n *\n * @module v3/security/oauth/browser\n */\n\nimport { execFile } from 'node:child_process';\n\n/** Throws if `url` isn't a well-formed https/http URL — the one check that matters here. */\nfunction assertSafeUrl(url: string): void {\n  const parsed = new URL(url); // throws on malformed input\n  if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n    throw new Error(`refusing to open non-http(s) URL scheme: ${parsed.protocol}`);\n  }\n}\n\n/**\n * Attempts to open `url` in the system default browser. Resolves whether or\n * not a browser window actually appeared — this cannot be confirmed in\n * general, which is why the caller always also prints the URL as a fallback.\n */\nexport function openBrowser(url: string): Promise<void> {\n  assertSafeUrl(url);\n\n  const { cmd, args } = browserCommand(url);\n\n  return new Promise((resolve) => {\n    execFile(cmd, args, { shell: false, windowsHide: true }, () => resolve());\n  });\n}\n","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/security/src/oauth/browser.ts#L9-L45","documentation":"openBrowser() only spawns a platform opener for http/https URLs; assertSafeUrl throws for any other scheme before any child process is started. This is a command-injection guard: the URL flows into xdg-open/open/start, where crafted schemes (file:, ssh:, custom handlers) can invoke arbitrary helpers.","triggerScenarios":"Passing a hand-built URL with a custom app scheme to openBrowser(); COGNITUM_AUTH_URL overridden to a file:// or non-http origin so authorizeUrl() output inherits the scheme; test fixtures using example:// URLs fed to the real opener.","commonSituations":"Misconfigured COGNITUM_AUTH_URL pointing at an internal non-http endpoint; code bypassing authorizeUrl() to construct deep links for a native SSO app; security scanners probing the browser-open path with javascript: payloads.","solutions":["Only pass URLs produced by authorizeUrl() — it always builds from an http(s) base","Check and normalize process.env.COGNITUM_AUTH_URL; it must be a full http(s) origin","If you need a custom-scheme redirect for a native app, launch it with your own validated mechanism — never this function"],"exampleFix":"// before\nawait openBrowser(`myapp://sso/login?state=${state}`); // refuses: non-http(s)\n\n// after\nconst url = authorizeUrl(redirectUri, state, codeChallenge); // https://auth.cognitum.one/...\nawait openBrowser(url);","handlingStrategy":"validation","validationCode":"function isSafeBrowserUrl(url: string): boolean {\n  try {\n    const { protocol } = new URL(url);\n    return protocol === 'https:' || protocol === 'http:';\n  } catch {\n    return false;\n  }\n}\nif (!isSafeBrowserUrl(url)) throw new Error(`refusing to open: ${url}`);\nawait openBrowser(url);","typeGuard":"function isUnsafeSchemeError(e: unknown): boolean {\n  return e instanceof Error && e.message.startsWith('refusing to open non-http(s) URL scheme:');\n}","tryCatchPattern":"try {\n  await openBrowser(url);\n} catch (e) {\n  if (isUnsafeSchemeError(e)) {\n    console.error(`Could not auto-open ${url} — open it manually in a browser`);\n  } else {\n    throw e;\n  }\n}","preventionTips":["Always build OAuth URLs with authorizeUrl() instead of string concatenation","Validate COGNITUM_AUTH_URL overrides to http(s) origins at startup","Treat this throw as a security control — never catch-and-bypass it with a raw spawn"],"tags":["security","oauth","url-validation","command-injection"],"backgroundTag":"url-scheme-not-allowed","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}