{"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/533346cc374382b41ec5ff70536252b2e96e725c/src/github_relay.rs#L171-L207","documentation":"After percent-decoding each path segment, `validate_path` rejects segments that are empty, `.` or `..`, contain `/`, `\\`, or `%`, or contain control characters. This prevents path traversal and double-decode attacks from changing which repository or path the relay ultimately touches. The relay only forwards paths whose decoded segments name a repo, ref, or asset cleanly.","triggerScenarios":"A request path with an empty segment (double slash, e.g. `/api/repos//repo`), a `.` or `..` segment (`/git/org/../repo/...`), a segment that decodes to a slash or backslash (`%2F`, `%5C`), or a segment containing a control character (e.g. `%00`, `%0a`).","commonSituations":"A client builds URLs by joining untrimmed parts leaving `//`; an attacker or buggy script tries `%2e%2e` traversal; a ref or asset name embeds a slash that was fully encoded instead of left as a path separator.","solutions":["Remove empty segments (collapse `//`) and any `.`/`..` from the request path","Leave real path separators as literal `/` instead of encoding them as `%2F`","Strip control characters (NUL, CR/LF) from refs/filenames before building the URL","Percent-encode only what the URL spec requires; do not double-encode"],"exampleFix":"// before\nlet path = \"/api/repos/org/repo/contents/dir%2Ffile.txt\";\n// after\nlet path = \"/api/repos/org/repo/contents/dir/file.txt\";","handlingStrategy":"validation","validationCode":"fn path_segments_are_safe(path: &str) -> bool {\n    path.split('/').all(|seg| {\n        if let Ok(d) = urlencoding::decode(seg) {\n            !d.is_empty() && d != \".\" && d != \"..\"\n                && !d.contains(['/', '\\\\', '%'])\n                && !d.chars().any(char::is_control)\n        } else { false }\n    })\n}","typeGuard":null,"tryCatchPattern":"match authorize(&scope, method, path, query) {\n    Err(e) if e.to_string().contains(\"invalid relay path\") => {\n        eprintln!(\"rejected unsafe path segment in: {path}\");\n    }\n    Ok(t) => forward(t),\n    Err(e) => return Err(e),\n}","preventionTips":["Collapse duplicate slashes and drop . / .. segments when building URLs","Leave `/` as a literal separator; do not encode as %2F","Strip control characters from refs and filenames before URL construction","Never build relay URLs from unvalidated user input"],"tags":["github","path-traversal","security","url-encoding","relay"],"backgroundTag":"path-traversal-blocked","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"}