Restore url.PathEscape on user-supplied path segments in url.JoinPath calls

url.JoinPath does not percent-encode slashes or reserved characters in
its arguments, so user-supplied values (group IDs, slugs, team IDs) must
be wrapped with url.PathEscape to prevent path traversal. Update cursor
rule and contrib guide to codify this as a mandatory practice.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-20 12:57:26 +04:00
parent 16876de0ae
commit f0fe70fe1c
9 changed files with 26 additions and 18 deletions

View File

@@ -262,7 +262,7 @@ var trustCenterIDKey = &ctxKey{name: "trust_center_id"}
- Use `url.URL` struct to build full URLs (scheme, host, path, query).
- Use `url.Values` to build query parameters, then call `.Encode()`.
- Use `url.QueryEscape` or `url.PathEscape` when embedding a single value into a known-safe base.
- **Always** wrap user-supplied path segments with `url.PathEscape` before passing them to `url.JoinPath`. `url.JoinPath` does **not** percent-encode slashes or reserved characters — a value like `parent/child` silently adds an extra path segment.
- Use the `pkg/baseurl.URLBuilder` when constructing URLs from configured base URLs.
```go
@@ -272,8 +272,11 @@ endpoint := fmt.Sprintf("https://api.example.com/users/%s?active=%t", userID, ac
// Bad — string concatenation
endpoint := "https://api.example.com/orgs/" + orgID + "/members"
// Good — url.JoinPath escapes each segment and sets Path + RawPath
u, err := url.JoinPath("https://api.example.com", "users", userID)
// Bad — user-supplied value without PathEscape
u, err := url.JoinPath("https://api.example.com", "groups", groupID, "members")
// Good — url.JoinPath with PathEscape on user-supplied segments
u, err := url.JoinPath("https://api.example.com", "groups", url.PathEscape(groupID), "members")
if err != nil {
return fmt.Errorf("cannot build URL: %w", err)
}