{"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":795,"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":777,"sourceCodeEnd":813,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/cli/src/cloud.rs#L777-L813","documentation":"validate_verification_url parses the service-provided device-flow URL and requires the raw string to be byte-identical to its parsed/serialized form (value == url.as_str()). Any difference — percent-encoding normalization, lowercase host rewrites, added trailing slashes, unicode punycode changes — means the URL contains components the validator did not inspect, so it is rejected as unsafe rather than canonicalized.","triggerScenarios":"Server returns a verification URL with non-canonical spelling: uppercase percent-encodings (%7E vs ~), mixed-case scheme/host that url::Url normalizes, unencoded characters the parser re-encodes, or trailing punctuation the parser moves.","commonSituations":"A backend or template change that stops emitting canonical URLs, middlewares that re-serialize URLs, or test servers hand-building the URL string with unnormalized characters.","solutions":["Make the server emit a canonical URL exactly as url::Url would serialize it (lowercase scheme/host, standard percent-encoding, explicit default port omitted).","Reproduce locally: parse the URL with the url crate in a scratch test and compare with the raw string to see the differing byte(s).","If you control a test server, build the URL via Url::join/parse instead of string concatenation."],"exampleFix":"// test server before: hand-built, non-canonical string\nlet uri = format!(\"https://App.Codewhale.NET/cli/authorize?code={}\", code);\n\n// after: canonical, matches url::Url serialization\nlet uri = format!(\"https://app.codewhale.net/cli/authorize?code={}\", code);","handlingStrategy":"validation","validationCode":"// Server-side: emit exactly what url::Url would serialize\nlet url: url::Url = format!(\"https://app.codewhale.net/cli/authorize?user_code={code}\").parse()?;\nlet raw = url.as_str(); // canonical bytes; return this string to clients","typeGuard":"fn is_canonical_url(value: &str) -> bool {\n    match url::Url::parse(value) { Ok(u) => u.as_str() == value, Err(_) => false }\n}","tryCatchPattern":null,"preventionTips":["Construct URLs with a parser, not string concatenation.","Round-trip-assert emitted URLs in server tests (parse -> serialize -> compare).","Lowercase scheme and host; use standard percent-encoding."],"tags":["url-validation","security","oauth","cloud","cli"],"backgroundTag":"non-canonical-url-rejected","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}