{"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/533346cc374382b41ec5ff70536252b2e96e725c/src/github_relay.rs#L211-L247","documentation":"The relay allowlists only read-only operations per path: git smart-HTTP upload-pack handshake and POST, GET/HEAD on repo metadata, refs, contents, releases, tags, branches, tarballs/zipballs, and web release/archive downloads. Any request whose method or path shape falls outside these patterns is rejected because the relay must never forward credentials to mutating or arbitrary GitHub operations.","triggerScenarios":"A `POST` to `git/OWNER/REPO/info/refs` (only GET is allowed); a git push attempt (`POST git-upload-pack` is allowed, but `git-receive-pack` paths are not in the allowlist, so pushes fail); `DELETE`/`PATCH`/`PUT` on `api/repos/...`; `POST` to any `web/` download path; any unrecognized path under an allowed prefix (e.g. `api/repos/o/r/commits`).","commonSituations":"Trying to `git push` through the relay (receive-pack is not permitted); a tool issuing write API calls (create release, edit file) through the proxy; a client using an endpoint kind not in the allowlist like `commits`, `pulls`, or `issues`.","solutions":["Restrict the client to read-only operations (clone/fetch, GET/HEAD API reads, release and archive downloads)","Perform write operations (push, API mutations) directly against github.com with your own credentials instead of via the relay","Check the endpoint kind is one of: info/refs, git-upload-pack, repo metadata, git/refs|matching-refs, contents, releases, tags, branches, tarball, zipball, releases/download, archive","Use GET or HEAD (except the git-upload-pack POST) — switch any mutating method"],"exampleFix":"// before\nPOST https://relay/git/org/repo/info/refs?service=git-receive-pack\n// after\ngit push https://github.com/org/repo.git  # writes bypass the read-only relay","handlingStrategy":"validation","validationCode":"const READ_ONLY: [(&str, [&str; 2]); 2] = [(\"GET\", []), (\"HEAD\", [])];\nfn is_read_only(method: &str, is_git_upload_pack_post: bool) -> bool {\n    method == \"GET\" || method == \"HEAD\" || is_git_upload_pack_post\n}\nassert!(is_read_only(\"GET\", false));","typeGuard":null,"tryCatchPattern":"match authorize(&scope, method, path, query) {\n    Err(e) if e.to_string().contains(\"read-only repository operations\") => {\n        eprintln!(\"{method} {path} is not permitted; perform writes directly on github.com\");\n    }\n    Ok(t) => forward(t),\n    Err(e) => return Err(e),\n}","preventionTips":["Never point write workflows (push, API mutations) at the relay","Restrict relayed clients to clone/fetch and GET/HEAD reads","Check endpoint kind against the allowlist before issuing requests","Configure git remotes so push goes directly to github.com while fetch uses the relay"],"tags":["github","relay","read-only","allowlist","authorization"],"backgroundTag":"operation-not-supported","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}