ruvnet/ruflo · error · Error

refusing to open non-http(s) URL scheme: ${parsed.protocol}

Error message

refusing to open non-http(s) URL scheme: ${parsed.protocol}

What it means

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.

Source

Thrown at v3/@claude-flow/security/src/oauth/browser.ts:27

 * authorize URL's query string (`?a=1&b=2&...`), so passing one through would
 * false-positive-reject on every real invocation. That blocklist is the wrong
 * tool for a URL argument: with `shell: false` (used here, same as
 * `SafeExecutor`), a single argv element containing `&` is inert — there's no
 * shell to interpret it. The actual safety property that matters is "the URL
 * was constructed by us from validated components, never from raw external
 * input" (this module's `authorizeUrl()` in `client.ts` is the only caller),
 * which `assertSafeUrl` below checks directly instead.
 *
 * @module v3/security/oauth/browser
 */

import { execFile } from 'node:child_process';

/** Throws if `url` isn't a well-formed https/http URL — the one check that matters here. */
function assertSafeUrl(url: string): void {
  const parsed = new URL(url); // throws on malformed input
  if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
    throw new Error(`refusing to open non-http(s) URL scheme: ${parsed.protocol}`);
  }
}

/**
 * Attempts to open `url` in the system default browser. Resolves whether or
 * not a browser window actually appeared — this cannot be confirmed in
 * general, which is why the caller always also prints the URL as a fallback.
 */
export function openBrowser(url: string): Promise<void> {
  assertSafeUrl(url);

  const { cmd, args } = browserCommand(url);

  return new Promise((resolve) => {
    execFile(cmd, args, { shell: false, windowsHide: true }, () => resolve());
  });
}

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Only pass URLs produced by authorizeUrl() — it always builds from an http(s) base
  2. Check and normalize process.env.COGNITUM_AUTH_URL; it must be a full http(s) origin
  3. If you need a custom-scheme redirect for a native app, launch it with your own validated mechanism — never this function

Example fix

// before
await openBrowser(`myapp://sso/login?state=${state}`); // refuses: non-http(s)

// after
const url = authorizeUrl(redirectUri, state, codeChallenge); // https://auth.cognitum.one/...
await openBrowser(url);
Defensive patterns

Strategy: validation

Validate before calling

function isSafeBrowserUrl(url: string): boolean {
  try {
    const { protocol } = new URL(url);
    return protocol === 'https:' || protocol === 'http:';
  } catch {
    return false;
  }
}
if (!isSafeBrowserUrl(url)) throw new Error(`refusing to open: ${url}`);
await openBrowser(url);

Type guard

function isUnsafeSchemeError(e: unknown): boolean {
  return e instanceof Error && e.message.startsWith('refusing to open non-http(s) URL scheme:');
}

Try / catch

try {
  await openBrowser(url);
} catch (e) {
  if (isUnsafeSchemeError(e)) {
    console.error(`Could not auto-open ${url} — open it manually in a browser`);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/01c6f15e0a11b803. Report an issue: GitHub.