{"record":{"id":"7c9ace90e56ea4ce","repo":"jdx/mise","slug":"invalid-relay-path","errorCode":null,"errorMessage":"invalid relay path","messagePattern":"invalid relay path","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/github_relay.rs","lineNumber":189,"sourceCode":"    // encoded separators and percent signs so a second decoder cannot change scope.\n    for segment in path.split('/') {\n        for (index, byte) in segment.bytes().enumerate() {\n            if byte == b'%'\n                && !segment\n                    .as_bytes()\n                    .get(index + 1..index + 3)\n                    .is_some_and(|digits| digits.iter().all(u8::is_ascii_hexdigit))\n            {\n                bail!(\"invalid relay path encoding\");\n            }\n        }\n        let decoded = urlencoding::decode(segment)?;\n        if decoded.is_empty()\n            || matches!(decoded.as_ref(), \".\" | \"..\")\n            || decoded.contains(['/', '\\\\', '%'])\n            || decoded.chars().any(char::is_control)\n        {\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    }","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/github_relay.rs#L171-L207","documentation":"After percent-decoding each path segment, `validate_path` rejects segments that are empty, `.` or `..` (path traversal), still contain `/`, `\\`, or `%` after decoding, or contain control characters. Any such segment triggers this error, keeping relayed requests pinned to safe repository paths.","triggerScenarios":"Calling `authorize` or `archive_redirect` with a path containing an empty segment (`//`), a `.`/`..` segment, an encoded traversal (`..%2F`), an encoded slash/backslash inside a segment, or control chars (e.g. `%00`, `%0a`) in a ref or file path.","commonSituations":"Refs built from untrusted input containing `../`; tags or branch names with embedded slashes that were encoded; NUL or newline characters introduced by shell interpolation; trailing slashes producing empty segments.","solutions":["Sanitize the path: remove empty, `.`, and `..` segments and reject refs containing `/`, `\\`, `%`, or control characters.","Encode special characters correctly (a branch like `feature/x` cannot be relayed via a single segment; use the ref query parameter instead).","Trim trailing slashes from the path before calling."],"exampleFix":"// before\nlet path = \"/api/repos/o/r/tarball/../../etc\";\n// after\nlet path = \"/api/repos/o/r/tarball/v1.2.0\";","handlingStrategy":"validation","validationCode":"fn safe_path(p: &str) -> bool {\n    p.split('/').skip(1).all(|seg| !seg.is_empty() && !matches!(seg, \".\" | \"..\")\n        && !seg.contains(['/', '\\\\', '%'])\n        && !seg.chars().any(char::is_control))\n}\nassert!(safe_path(\"/api/repos/o/r/tarball/v1.0\"));","typeGuard":null,"tryCatchPattern":"match relay::authorize(&scope, \"GET\", path, None) {\n    Err(e) if e.to_string().contains(\"invalid relay path\") => eprintln!(\"sanitize path segments: {e}\"),\n    Err(e) => return Err(e),\n    Ok(t) => t,\n}","preventionTips":["Treat all path segments derived from user input as untrusted; reject `.`/`..` and control chars early.","Trim trailing slashes and collapse duplicate slashes before building relay paths.","Use branch refs via the allowed `ref` query param instead of embedding slashes in path segments."],"tags":["security","path-traversal","validation","url"],"backgroundTag":"path-traversal-blocked","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"}