jdx/mise · error
invalid relay path
Error message
invalid relay path
What it means
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.
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
Example fix
// before let path = "/api/repos/org/repo/contents/dir%2Ffile.txt"; // after let path = "/api/repos/org/repo/contents/dir/file.txt";
Defensive patterns
Strategy: validation
Validate before calling
fn path_segments_are_safe(path: &str) -> bool {
path.split('/').all(|seg| {
if let Ok(d) = urlencoding::decode(seg) {
!d.is_empty() && d != "." && d != ".."
&& !d.contains(['/', '\\', '%'])
&& !d.chars().any(char::is_control)
} else { false }
})
} Try / catch
match authorize(&scope, method, path, query) {
Err(e) if e.to_string().contains("invalid relay path") => {
eprintln!("rejected unsafe path segment in: {path}");
}
Ok(t) => forward(t),
Err(e) => return Err(e),
} Prevention
- 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
When it happens
Trigger: 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`).
Common situations: 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.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- invalid relay path encoding
- brew-cask: app target
- brew-cask: binary target
- brew-cask: completion target
- brew-cask: completion target
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/7c9ace90e56ea4ce.
Report an issue: GitHub.
Appendix: source
Thrown at src/github_relay.rs:189
// encoded separators and percent signs so a second decoder cannot change scope.
for segment in path.split('/') {
for (index, byte) in segment.bytes().enumerate() {
if byte == b'%'
&& !segment
.as_bytes()
.get(index + 1..index + 3)
.is_some_and(|digits| digits.iter().all(u8::is_ascii_hexdigit))
{
bail!("invalid relay path encoding");
}
}
let decoded = urlencoding::decode(segment)?;
if decoded.is_empty()
|| matches!(decoded.as_ref(), "." | "..")
|| decoded.contains(['/', '\\', '%'])
|| decoded.chars().any(char::is_control)
{
bail!("invalid relay path");
}
}
Ok(())
}
#[cfg(any(unix, test))]
fn authorize(scope: &Scope, method: &str, path: &str, query: Option<&str>) -> Result<Target> {
validate_path(path)?;
let p: Vec<_> = path.split('/').collect();
let (owner, repo) = match p.as_slice() {
["api", "repos", owner, repo, ..] => (*owner, *repo),
["git" | "web", owner, repo, ..] => (*owner, repo.strip_suffix(".git").unwrap_or(repo)),
_ => bail!("unsupported GitHub operation"),
};
let name = repository(&format!("{owner}/{repo}"))?;
if !scope.permits(&name) {
bail!("repository is outside the approved relay scope");
}View on GitHub (pinned to 533346cc37)