Hmbown/CodeWhale · error · anyhow::Error

The Codewhale service returned an unsafe verification URL

Error message

The Codewhale service returned an unsafe verification URL

What it means

Thrown by `validate_verification_url` in crates/cli/src/cloud.rs:1049 when the device-login verification URL string returned by the Codewhale service does not round-trip through `Url::parse` exactly (`value != url.as_str()`). Any non-canonical serialization — extra characters, backslashes, embedded whitespace, percent-encoded tricks, Unicode confusables — is rejected as a potential URL-spoofing / open-redirect attempt before the URL is ever opened in a browser.

Solutions

  1. Verify the service returns an already-canonical verification URL: paste it into a `Url::parse(...)` round-trip test and compare `as_str()` to the original
  2. Remove whitespace, backslashes, or redundant encoding from the URL the server emits
  3. If a custom API base is in use, point login back at the canonical api.codewhale.net endpoint or fix the staging server's URL emission
  4. Capture the raw response body from POST /device/authorize and inspect the verification_url field for hidden characters

Example fix

// server response
{ "verification_url": "https://app.codewhale.net/cli/authorize/" }
// after
{ "verification_url": "https://app.codewhale.net/cli/authorize" }
Defensive patterns

Strategy: validation

Validate before calling

fn is_canonical_url(value: &str) -> bool {
    url::Url::parse(value).map(|u| u.as_str() == value).unwrap_or(false)
}

Type guard

fn looks_canonical(value: &str) -> bool {
    !value.chars().any(|c| c.is_whitespace() || c == '\\')
        && url::Url::parse(value).map(|u| u.as_str() == value).unwrap_or(false)
}

Try / catch

match validate_verification_url(&value, api_base, &code, complete) {
    Ok(url) => open_browser(&url),
    Err(e) if e.to_string().contains("unsafe verification URL") => {
        eprintln!("Refusing non-canonical verification URL from server: {value:?}");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A compromised or misbehaving device-authorization endpoint returns a verification_url whose raw string differs from its canonical `Url::as_str()` serialization, e.g. `https://app.codewhale.net/cli/authorize ` (trailing space), `https://app.codewhale.net\/cli/authorize`, uppercase percent-encoding, or any string the `url` crate normalizes.

Common situations: A staging/mock API (CODEWHALE_API_BASE override) returning hand-written or template-generated verification URLs instead of canonical ones; a proxy rewriting the URL; a man-in-the-middle injecting a look-alike URL during `codewhale login`.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/8e9c3a6ccafbbc33. Report an issue: GitHub.

Appendix: source

Thrown at crates/cli/src/cloud.rs:1049

        bail!(
            "Codewhale account API base URL must use HTTPS (loopback HTTP is allowed for testing)"
        );
    }
    url.set_path("/");
    let display = url.as_str().trim_end_matches('/').to_string();
    Ok(ValidatedApiBase { url, display })
}

fn validate_verification_url(
    value: &str,
    api_base: &str,
    user_code: &str,
    complete: bool,
) -> Result<String> {
    let url =
        Url::parse(value).context("The Codewhale service returned an invalid verification URL")?;
    if value != url.as_str() {
        bail!("The Codewhale service returned an unsafe verification URL");
    }
    let host = url.host_str().ok_or_else(|| {
        anyhow!("The Codewhale service returned a verification URL without a host")
    })?;
    if !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() {
        bail!("The Codewhale service returned an unsafe verification URL");
    }
    if url.path() != "/cli/authorize" {
        bail!("The Codewhale service returned an unsafe verification URL");
    }

    let api = Url::parse(api_base).context("invalid Codewhale account API base URL")?;
    let canonical_api = api.scheme() == "https"
        && api.host_str() == Some("api.codewhale.net")
        && api.port_or_known_default() == Some(443);
    let loopback_api = api.host_str().is_some_and(is_loopback_host);
    if canonical_api {
        if url.scheme() != "https"

View on GitHub (pinned to 73e0f67d83)