{"record":{"id":"6f438ae7a7e260e4","repo":"xai-org/grok-build","slug":"refusing-unsafe-fetch-refspec","errorCode":null,"errorMessage":"refusing unsafe fetch refspec","messagePattern":"refusing unsafe fetch refspec","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/codegen/xai-grok-workspace/src/restore_fetch.rs","lineNumber":326,"sourceCode":"    repo: &Path,\n    oid: &str,\n    is_shallow: bool,\n) -> std::process::Command {\n    let mut cmd = git_command_locking();\n    cmd.current_dir(repo)\n        .args(targeted_fetch_args(oid, is_shallow))\n        .stdout(Stdio::null())\n        .stderr(Stdio::piped());\n    cmd\n}\n\nfn fetch_oid_from_origin(repo: &Path, oid: &str, timeout: Duration) -> Result<()> {\n    fetch_refspec_from_origin(repo, oid, timeout)\n}\n\nfn fetch_refspec_from_origin(repo: &Path, spec: &str, timeout: Duration) -> Result<()> {\n    if !is_safe_fetch_refspec(spec) {\n        bail!(\"refusing unsafe fetch refspec\");\n    }\n    let is_shallow = is_shallow_repository(repo);\n    tracing::info!(\n        spec = %spec,\n        is_shallow,\n        timeout_secs = timeout.as_secs(),\n        \"restore_fetch: targeted fetch\"\n    );\n\n    let mut child = FetchChild::spawn(targeted_fetch_command(repo, spec, is_shallow))?;\n    let result = child.wait_success(timeout, spec);\n    if result.is_err() {\n        warn_leftover_git_locks(repo);\n    }\n    result\n}\n\nstruct FetchChild {","sourceCodeStart":308,"sourceCodeEnd":344,"githubUrl":"https://github.com/xai-org/grok-build/blob/bc7f02eddd3d84085849dc19ed216f11c23b0571/crates/codegen/xai-grok-workspace/src/restore_fetch.rs#L308-L344","documentation":"fetch_refspec_from_origin runs `git fetch origin <spec>` and first validates the spec with is_safe_fetch_refspec. A spec is safe only if it is a full 40/64-hex object id, or a git ref free of option-injection and refspec metacharacters ('-', ':', '*', '?', '[', backslash, whitespace, NUL, '..', '@{') and not a plausible abbreviated SHA. This error is a deliberate guard: it refuses to hand untrusted or malformed specs to git, preventing argument injection (specs starting with '-') and meaningless fetches of short SHAs, which are not remote refspecs.","triggerScenarios":"fetch_refspec_from_origin invoked (via fetch_oid_from_origin or fetch_checkout_target_if_missing) with a spec that fails is_safe_fetch_refspec: a dash-prefixed string, a refspec containing ':' or glob characters, a 4–39 or 41–63 character hex string (abbreviated SHA), an empty spec, or a ref containing spaces/'..'/'@{'.","commonSituations":"Upstream code or config passing abbreviated SHAs to fetch_oid_from_origin; restoring from untrusted snapshot metadata containing crafted targets (option-injection attempt); targets derived from user input like 'HEAD^..HEAD' or 'refs/heads/*'; whitespace-contaminated ref names from copied log output.","solutions":["Validate/normalize the spec before calling: use only full lowercase hex oids or plain ref names, and run is_safe_fetch_refspec on it first","Replace abbreviated SHAs with full 40/64-char object ids (git rev-parse --verify <abbrev>^{commit}) or fetch the branch/tag that contains the commit","Strip illegal characters/whitespace from the target at its source (config, manifest, or user input) and reject ranges/wildcards — they are not fetchable single refs","If this fires on data you did not construct, treat it as suspicious input: log the raw target and refuse restore rather than loosening the safety check"],"exampleFix":"// before: abbreviated hex is not a valid origin refspec\nfetch_oid_from_origin(repo, \"deadbee\", timeout)?;\n// after: expand to the full oid or validate first\nlet full_oid = resolve_full_oid(repo, \"deadbee\")?; // 40/64 hex chars\nassert!(is_safe_fetch_refspec(&full_oid));\nfetch_oid_from_origin(repo, &full_oid, timeout)?;","handlingStrategy":"validation","validationCode":"use crate::restore_fetch::is_safe_fetch_refspec;\nfn ensure_safe_spec(spec: &str) -> Result<(), String> {\n    if is_safe_fetch_refspec(spec) { Ok(()) }\n    else { Err(format!(\"unsafe fetch refspec: {spec:?} (use a full oid or plain ref)\")) }\n}","typeGuard":"fn is_fetch_safe(value: &str) -> bool {\n    let hex_ok = value.len() == 40 || value.len() == 64;\n    let is_full_oid = hex_ok && value.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));\n    let plausible_short_sha = (4..40).contains(&value.len()) && value.bytes().all(|b| b.is_ascii_hexdigit());\n    is_full_oid || (!value.is_empty()\n        && !value.starts_with('-')\n        && !plausible_short_sha\n        && !value.contains([':', '*', '?', '[', '\\\\', ' ', '\\t', '\\n', '\\r'])\n        && !value.contains(\"..\")\n        && !value.contains(\"{\"))\n}","tryCatchPattern":"match fetch_oid_from_origin(repo, spec, timeout) {\n    Err(e) if e.to_string() == \"refusing unsafe fetch refspec\" => {\n        // never loosen the check; normalize the input instead\n        let normalized = normalize_to_full_oid_or_ref(repo, spec)?;\n        fetch_oid_from_origin(repo, &normalized, timeout)\n    }\n    other => other,\n}","preventionTips":["Expand abbreviated SHAs to full oids (git rev-parse --verify <abbrev>^{commit}) before any fetch","Sanitize user/config-supplied refs: strip whitespace, reject '-', ':', '*', '?', '[', backslash, '..', '@{', and empty strings","Treat snapshot/manifest targets as untrusted input and validate with is_safe_fetch_refspec before fetch calls","Never construct fetch specs by concatenating user input with options; pass plain ref names or full oids only"],"tags":["git","security","validation","refspec","input-validation"],"backgroundTag":"unsafe-fetch-refspec","analyzedSha":"bc7f02eddd3d84085849dc19ed216f11c23b0571","analyzedAt":"2026-08-31T04:59:42.031Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}