mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 06:19:49 +00:00
223c9734d8
BUG-3645: workspace slugs and refs were concatenated raw into request paths, so a .pad.toml workspace of "victim/#" turned `pad item delete TASK-1` into DELETE /workspaces/victim/. Every dynamic segment in the client builders now goes through url.PathEscape. Client.newRequest refuses a path with a raw '#', an empty, "." or ".." segment (PathEscape leaves dots alone), or a '/' after its '?', which catches any builder that misses escaping, plus the raw-path methods' callers. A reflection census calls every *Client method with a hostile string in each string argument and asserts that the string stays escaped inside one segment. BUG-3646: PAD_TOKEN was attached to whatever URL the client was given, including a committed .pad.toml's url. It is now bound to the server chosen before that override (--url, PAD_URL, the config or the default). Requests to a server that only .pad.toml names use the credential saved for it, and the CLI warns once. Setting PAD_URL to that server or passing --url opts in. Every check of whether the env token is in play (whoami, server info, init, login/logout, 2fa) goes through the same accessor. Claude-Session: https://claude.ai/code/session_01Rj4CMyK2EqxerMihbGs3Kr
44 lines
1.6 KiB
Go
44 lines
1.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// checkRequestPath refuses an API path that would route somewhere other than
|
|
// the segments it was built from (BUG-3645).
|
|
//
|
|
// Every builder escapes its dynamic segments with url.PathEscape, so a
|
|
// workspace or ref carrying '/', '?' or '#' stays inside its segment. This is
|
|
// the backstop for a builder that does not: the CLI would otherwise send an
|
|
// item delete for workspace "victim/#" to /workspaces/victim/ (the rest is a
|
|
// fragment, never sent), and the server deletes the workspace. It also covers
|
|
// what escaping cannot: PathEscape leaves "." and ".." unchanged, and a proxy
|
|
// or server that cleans the path then lands on a different route.
|
|
//
|
|
// Refused: a raw '#' anywhere; an empty, "." or ".." segment in the path; a
|
|
// raw '/' in the query, which no builder emits (query values go through
|
|
// url.Values.Encode or url.QueryEscape) and which is what a '?' smuggled into
|
|
// a segment leaves behind.
|
|
func checkRequestPath(path string) error {
|
|
if strings.ContainsRune(path, '#') {
|
|
return fmt.Errorf("refusing request: path %q contains '#'", path)
|
|
}
|
|
p, query, hasQuery := strings.Cut(path, "?")
|
|
if hasQuery && strings.ContainsRune(query, '/') {
|
|
return fmt.Errorf("refusing request: path %q has a '/' after its '?'", path)
|
|
}
|
|
if p == "" {
|
|
return nil
|
|
}
|
|
for _, seg := range strings.Split(strings.TrimPrefix(p, "/"), "/") {
|
|
switch seg {
|
|
case "":
|
|
return fmt.Errorf("refusing request: path %q has an empty segment", path)
|
|
case ".", "..":
|
|
return fmt.Errorf("refusing request: path %q has a %q segment", path, seg)
|
|
}
|
|
}
|
|
return nil
|
|
}
|