{"record":{"id":"3c9b20ece7768653","repo":"gitbutlerapp/gitbutler","slug":"refusing-to-remove-label-with-degenerate-name-lab","errorCode":null,"errorMessage":"Refusing to remove label with degenerate name {label:?}","messagePattern":"Refusing to remove label with degenerate name (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-github/src/client.rs","lineNumber":1720,"sourceCode":"\n#[derive(Serialize)]\nstruct ReviewersBody<'a> {\n    reviewers: &'a [String],\n}\n\n/// Build `DELETE /repos/{o}/{r}/issues/{n}/labels/{name}` with the label name\n/// percent-encoded. `PathSegmentsMut::push` silently *drops* `.` and `..`\n/// segments, which would degrade this into GitHub's remove-ALL-labels\n/// endpoint — refuse those names instead of encoding them.\nfn label_removal_url(\n    base_url: &str,\n    owner: &str,\n    repo: &str,\n    pr_number: i64,\n    label: &str,\n) -> Result<reqwest::Url> {\n    if matches!(label, \"\" | \".\" | \"..\") {\n        bail!(\"Refusing to remove label with degenerate name {label:?}\");\n    }\n\n    let mut url = reqwest::Url::parse(&format!(\n        \"{base_url}/repos/{owner}/{repo}/issues/{pr_number}/labels\"\n    ))?;\n    url.path_segments_mut()\n        .map_err(|()| anyhow::anyhow!(\"Invalid GitHub base URL\"))?\n        .push(label);\n    Ok(url)\n}\n\n/// A submitted review on a pull request, from `GET /pulls/{n}/reviews`.\n#[derive(Debug, Serialize)]\npub struct PullRequestReview {\n    pub id: i64,\n    pub author: Option<GitHubUser>,\n    /// GitHub state string: `APPROVED`, `CHANGES_REQUESTED`, `COMMENTED`,\n    /// `DISMISSED`, or `PENDING` (the caller's own unsubmitted draft).","sourceCodeStart":1702,"sourceCodeEnd":1738,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-github/src/client.rs#L1702-L1738","documentation":"Label removal builds DELETE /repos/{o}/{r}/issues/{n}/labels/{name} by pushing the label into the URL path. The url crate's PathSegmentsMut::push silently drops '.' and '..' segments, which would degrade the URL into GitHub's remove-ALL-labels endpoint and wipe every label on the issue. The guard therefore refuses empty, '.', and '..' label names instead of encoding them.","triggerScenarios":"Calling label removal with a name that is \"\", \".\", or \"..\" - typically unvalidated user input, empty pieces produced by splitting a string on separators, or config placeholders.","commonSituations":"Parsing comma-separated label lists with trailing separators; UI forms that allow empty submits; label names derived programmatically from branch or file names.","solutions":["Filter degenerate names out of the label list before calling remove_label","Validate label names at input time - GitHub labels cannot be empty anyway","Fix the upstream string handling that produced the empty or '.' value"],"exampleFix":"// before\nfor label in labels {\n    client.remove_label(owner, repo, pr, &label).await?;\n}\n\n// after\nfor label in labels.iter().filter(|l| !matches!(l.as_str(), \"\" | \".\" | \"..\")) {\n    client.remove_label(owner, repo, pr, label).await?;\n}","handlingStrategy":"validation","validationCode":"let safe: Vec<_> = labels.iter().filter(|l| is_safe_label_name(l)).cloned().collect();\nif safe.len() != labels.len() {\n    anyhow::bail!(\"refusing label removal: degenerate label names present\");\n}","typeGuard":"fn is_safe_label_name(label: &str) -> bool {\n    !matches!(label, \"\" | \".\" | \"..\")\n}","tryCatchPattern":null,"preventionTips":["Sanitize label lists at input time: reject empty strings before splitting logic can create them","Never construct path-bearing URLs from raw user strings without segment validation","Remember url::PathSegmentsMut silently drops '.' and '..' segments"],"tags":["github","labels","url-parsing","input-validation","security"],"backgroundTag":"path-traversal","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}