{"record":{"id":"e3c6ccfdda809dc5","repo":"jdx/mise","slug":"repository-is-outside-the-approved-relay-scope","errorCode":null,"errorMessage":"repository is outside the approved relay scope","messagePattern":"repository is outside the approved relay scope","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/github_relay.rs","lineNumber":206,"sourceCode":"        {\n            bail!(\"invalid relay path\");\n        }\n    }\n    Ok(())\n}\n\n#[cfg(any(unix, test))]\nfn authorize(scope: &Scope, method: &str, path: &str, query: Option<&str>) -> Result<Target> {\n    validate_path(path)?;\n    let p: Vec<_> = path.split('/').collect();\n    let (owner, repo) = match p.as_slice() {\n        [\"api\", \"repos\", owner, repo, ..] => (*owner, *repo),\n        [\"git\" | \"web\", owner, repo, ..] => (*owner, repo.strip_suffix(\".git\").unwrap_or(repo)),\n        _ => bail!(\"unsupported GitHub operation\"),\n    };\n    let name = repository(&format!(\"{owner}/{repo}\"))?;\n    if !scope.permits(&name) {\n        bail!(\"repository is outside the approved relay scope\");\n    }\n    let git = p[0] == \"git\";\n    let allowed = match p.as_slice() {\n        [\"git\", _, _, \"info\", \"refs\"] => {\n            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\"),","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/github_relay.rs#L188-L224","documentation":"After extracting OWNER/REPO from the path, `authorize` checks it against the relay's approved scope via `scope.permits(&name)` (lowercased). If the requested repository is not in the allowlist configured for this relay session, the request is refused, even if the path shape and operation would otherwise be allowed. The relay is deliberately scoped so credentials can only reach specific repositories.","triggerScenarios":"A client requests `api/repos/other/repo`, `git/other/repo`, or `web/other/repo/...` where `other/repo` was not included in the scope passed at relay startup (e.g. scope was `octocat/hello-world` but the clone URL points at a fork under another owner).","commonSituations":"Cloning a fork whose owner differs from the scoped repo; a tool following a redirect or submodule URL to a different repository; a typo in either the scope flag or the client URL; case differences are fine (name is lowercased) but owner/repo spelling must match the scope.","solutions":["Add the requested repository to the relay's approved scope (restart the relay with the additional OWNER/REPO in the scope flag)","Correct the client to use the scoped repository's exact owner/repo spelling","If a tool is following redirects to other repos, pin it to the original repository or disable the redirecting behavior"],"exampleFix":"// before\nmise github-relay --scope \"octocat/hello-world\"\ngit clone https://relay/git/octocat/hello-world-2.git\n// after\nmise github-relay --scope \"octocat/hello-world\"\ngit clone https://relay/git/octocat/hello-world.git","handlingStrategy":"validation","validationCode":"fn scope_permits(scope_repos: &[String], url_repo: &str) -> bool {\n    scope_repos.iter().any(|r| r.to_ascii_lowercase() == url_repo.to_ascii_lowercase())\n}\n// call before issuing the request\nassert!(scope_permits(&[\"octocat/hello-world\".into()], \"octocat/hello-world\"));","typeGuard":null,"tryCatchPattern":"match authorize(&scope, method, path, query) {\n    Err(e) if e.to_string().contains(\"outside the approved relay scope\") => {\n        eprintln!(\"repo not in scope; restart relay with it added to --scope\");\n    }\n    Ok(t) => forward(t),\n    Err(e) => return Err(e),\n}","preventionTips":["List every repository (including forks/submodules) the client may touch in the relay scope","Normalize owner/repo to lowercase before comparing","Audit tools for redirect-following that could hop to other repositories","Double-check scope flag spelling against the URLs clients will use"],"tags":["github","relay","authorization","scope","allowlist"],"backgroundTag":"permission-denied","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"}