{"record":{"id":"8e9c3a6ccafbbc33","repo":"Hmbown/CodeWhale","slug":"the-codewhale-service-returned-an-unsafe-verificat","errorCode":null,"errorMessage":"The Codewhale service returned an unsafe verification URL","messagePattern":"The Codewhale service returned an unsafe verification URL","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/cli/src/cloud.rs","lineNumber":1049,"sourceCode":"        bail!(\n            \"Codewhale account API base URL must use HTTPS (loopback HTTP is allowed for testing)\"\n        );\n    }\n    url.set_path(\"/\");\n    let display = url.as_str().trim_end_matches('/').to_string();\n    Ok(ValidatedApiBase { url, display })\n}\n\nfn validate_verification_url(\n    value: &str,\n    api_base: &str,\n    user_code: &str,\n    complete: bool,\n) -> Result<String> {\n    let url =\n        Url::parse(value).context(\"The Codewhale service returned an invalid verification URL\")?;\n    if value != url.as_str() {\n        bail!(\"The Codewhale service returned an unsafe verification URL\");\n    }\n    let host = url.host_str().ok_or_else(|| {\n        anyhow!(\"The Codewhale service returned a verification URL without a host\")\n    })?;\n    if !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() {\n        bail!(\"The Codewhale service returned an unsafe verification URL\");\n    }\n    if url.path() != \"/cli/authorize\" {\n        bail!(\"The Codewhale service returned an unsafe verification URL\");\n    }\n\n    let api = Url::parse(api_base).context(\"invalid Codewhale account API base URL\")?;\n    let canonical_api = api.scheme() == \"https\"\n        && api.host_str() == Some(\"api.codewhale.net\")\n        && api.port_or_known_default() == Some(443);\n    let loopback_api = api.host_str().is_some_and(is_loopback_host);\n    if canonical_api {\n        if url.scheme() != \"https\"","sourceCodeStart":1031,"sourceCodeEnd":1067,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/cli/src/cloud.rs#L1031-L1067","documentation":"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.","triggerScenarios":"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.","commonSituations":"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`.","solutions":["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","Remove whitespace, backslashes, or redundant encoding from the URL the server emits","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","Capture the raw response body from POST /device/authorize and inspect the verification_url field for hidden characters"],"exampleFix":"// server response\n{ \"verification_url\": \"https://app.codewhale.net/cli/authorize/\" }\n// after\n{ \"verification_url\": \"https://app.codewhale.net/cli/authorize\" }","handlingStrategy":"validation","validationCode":"fn is_canonical_url(value: &str) -> bool {\n    url::Url::parse(value).map(|u| u.as_str() == value).unwrap_or(false)\n}","typeGuard":"fn looks_canonical(value: &str) -> bool {\n    !value.chars().any(|c| c.is_whitespace() || c == '\\\\')\n        && url::Url::parse(value).map(|u| u.as_str() == value).unwrap_or(false)\n}","tryCatchPattern":"match validate_verification_url(&value, api_base, &code, complete) {\n    Ok(url) => open_browser(&url),\n    Err(e) if e.to_string().contains(\"unsafe verification URL\") => {\n        eprintln!(\"Refusing non-canonical verification URL from server: {value:?}\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Always round-trip server URLs through Url::parse and compare as_str() before display","Reject URLs containing whitespace, backslashes, or control characters at the source","Test staging servers with the same URL validator the CLI uses","Keep verification URLs generated by a single canonical serializer on the server"],"tags":["security","url-validation","device-login","oauth"],"backgroundTag":"invalid-url-format","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}