{"record":{"id":"332a4b41aa6b3a55","repo":"jdx/mise","slug":"github-relay-permits-read-only-repository-operatio","errorCode":null,"errorMessage":"GitHub relay permits read-only repository operations only","messagePattern":"GitHub relay permits read-only repository operations only","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/github_relay.rs","lineNumber":229,"sourceCode":"            method == \"GET\" && query == Some(\"service=git-upload-pack\")\n        }\n        [\"git\", _, _, \"git-upload-pack\"] => method == \"POST\" && query.is_none(),\n        [\"api\", \"repos\", _, _] => method == \"GET\" || method == \"HEAD\",\n        [\"api\", \"repos\", _, _, \"git\", kind, ..] => {\n            matches!(*kind, \"refs\" | \"matching-refs\") && matches!(method, \"GET\" | \"HEAD\")\n        }\n        [\"api\", \"repos\", _, _, kind, ..] => {\n            matches!(\n                *kind,\n                \"contents\" | \"releases\" | \"tags\" | \"branches\" | \"tarball\" | \"zipball\"\n            ) && matches!(method, \"GET\" | \"HEAD\")\n        }\n        [\"web\", _, _, \"releases\", \"download\", _, ..] => matches!(method, \"GET\" | \"HEAD\"),\n        [\"web\", _, _, \"archive\", _, ..] => matches!(method, \"GET\" | \"HEAD\"),\n        _ => false,\n    };\n    if !allowed {\n        bail!(\"GitHub relay permits read-only repository operations only\");\n    }\n    if !git && let Some(query) = query {\n        for (key, _) in url::form_urlencoded::parse(query.as_bytes()) {\n            if !matches!(key.as_ref(), \"ref\" | \"page\" | \"per_page\") {\n                bail!(\"unsupported query parameter\");\n            }\n        }\n    }\n    let host = if p[0] == \"api\" {\n        \"api.github.com\"\n    } else {\n        \"github.com\"\n    };\n    let suffix = path.split_once('/').expect(\"validated path\").1;\n    let mut url = format!(\"https://{host}/{suffix}\");\n    let archive_repo = match p.as_slice() {\n        [\"api\", \"repos\", _, _, \"tarball\" | \"zipball\", ..] => Some(name.clone()),\n        [\"web\", _, _, \"archive\", rest @ ..] => {","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/github_relay.rs#L211-L247","documentation":"The GitHub relay is strictly read-only: `authorize` matches the method + path against a small allow-list (git info/refs upload-pack GET, API repo GETs, release/archive downloads with GET|HEAD, etc.) and fails with this error for anything else. Write operations (POST/PUT/PATCH/DELETE) and non-allow-listed read endpoints are rejected before any network call.","triggerScenarios":"Calling `forward`/`operation` with a mutating method (e.g. POST to create a release, DELETE a file), or a method/path combo not in the allow-list such as `POST /api/repos/o/r/issues` or `GET /git/o/r/git-receive-pack`.","commonSituations":"Pointing a generic GitHub API client (which may POST or PATCH) through the relay; attempting `git push` (receive-pack) instead of clone/fetch; tooling that creates comments, releases, or checks while installing dependencies.","solutions":["Use only read-only operations: GET (and HEAD for downloads) against allowed paths — clone/fetch via git upload-pack, tarball/zipball downloads, repo metadata reads.","Remove write calls (releases, issues, push) from code that runs through the relay; perform them outside the relay with proper credentials if truly needed.","Check the method constant passed to `forward`/`operation` — a wrong default (e.g. POST for a metadata fetch) will be rejected."],"exampleFix":"// before\nrelay::forward(&scope, \"POST\", \"/api/repos/o/r/releases\", Some(body))?;\n// after\nrelay::forward(&scope, \"GET\", \"/api/repos/o/r/releases/latest\", None)?;","handlingStrategy":"validation","validationCode":"fn is_read_only(method: &str) -> bool {\n    matches!(method, \"GET\" | \"HEAD\")\n}\n// guard before calling forward/operation\nassert!(is_read_only(\"GET\"));","typeGuard":null,"tryCatchPattern":"match relay::forward(&scope, method, path, None) {\n    Err(e) if e.to_string().contains(\"read-only\") => eprintln!(\"{method} {path} is not permitted by the relay\"),\n    Err(e) => return Err(e),\n    Ok(resp) => resp,\n}","preventionTips":["Audit GitHub client code for POST/PUT/PATCH/DELETE calls; move them out of relayed code paths.","Use clone/fetch (upload-pack) only; never route git push through the relay.","Default HTTP clients to GET for metadata and downloads."],"tags":["github","relay","authorization","read-only"],"backgroundTag":"permission-denied","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}