From 155ddb97baa32d3e6b11315ed17f405e2fe276fd Mon Sep 17 00:00:00 2001 From: sid Date: Sat, 2 May 2026 15:48:59 -0600 Subject: [PATCH] fix(api): validate same-origin before forwarding auth on --paginate Codex flagged: the --paginate loop rebuilt the next request from the raw `Link: rel="next"` URL and reattached the bearer token without checking that the next URL was on the same host. Forgejo emits same-origin next- links in practice, but a buggy or malicious upstream could redirect us to a foreign host, at which point the token would leak. Now the loop: - url.Parse the Link target. - Resolve relative URLs against the original base (https:///api/v1). - Refuse to proceed if the resolved URL's scheme isn't https or its host doesn't match `host.Hostname`. The error names both the foreign URL and the expected origin so the user can tell why pagination stopped. Verified: same-origin pagination still works (`--paginate` against forgejo.zerova.net commits returns 44 across 22 pages). --- cmd/api.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/cmd/api.go b/cmd/api.go index cafa38f..753ff49 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -291,7 +291,22 @@ func runAPI(cmd *cobra.Command, args []string) error { bodies := [][]byte{respBody} nextURL := parseLinkHeaderNext(respHeader.Get("Link")) for nextURL != "" { - nextReq, err := http.NewRequest(http.MethodGet, nextURL, nil) + // Forgejo emits same-origin next-links in practice, but a buggy + // or hostile upstream could redirect us to a foreign host — at + // which point we'd leak the bearer token. Validate origin (and + // resolve relative URLs against `base`) before forwarding auth. + parsedNext, err := url.Parse(nextURL) + if err != nil { + return fmt.Errorf("invalid Link rel=\"next\" URL %q: %w", nextURL, err) + } + if !parsedNext.IsAbs() { + parsedNext = base.ResolveReference(parsedNext) + } + if parsedNext.Scheme != "https" || parsedNext.Host != host.Hostname { + return fmt.Errorf("paginated next URL %s is not same-origin as https://%s; refusing to forward credentials", parsedNext.String(), host.Hostname) + } + + nextReq, err := http.NewRequest(http.MethodGet, parsedNext.String(), nil) if err != nil { return fmt.Errorf("failed to build paginated request: %w", err) } @@ -311,7 +326,7 @@ func runAPI(cmd *cobra.Command, args []string) error { return err } if pageStatus < 200 || pageStatus >= 300 { - return fmt.Errorf("paginated request to %s failed with status %d", nextURL, pageStatus) + return fmt.Errorf("paginated request to %s failed with status %d", parsedNext.String(), pageStatus) } bodies = append(bodies, pageBody) nextURL = parseLinkHeaderNext(pageHeader.Get("Link"))