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

  1. Remove empty segments (collapse `//`) and any `.`/`..` from the request path
  2. Leave real path separators as literal `/` instead of encoding them as `%2F`
  3. Strip control characters (NUL, CR/LF) from refs/filenames before building the URL
  4. 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

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


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)