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://<host>/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).
This commit is contained in:
parent
133fb2fea4
commit
155ddb97ba
1 changed files with 17 additions and 2 deletions
19
cmd/api.go
19
cmd/api.go
|
|
@ -291,7 +291,22 @@ func runAPI(cmd *cobra.Command, args []string) error {
|
||||||
bodies := [][]byte{respBody}
|
bodies := [][]byte{respBody}
|
||||||
nextURL := parseLinkHeaderNext(respHeader.Get("Link"))
|
nextURL := parseLinkHeaderNext(respHeader.Get("Link"))
|
||||||
for nextURL != "" {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to build paginated request: %w", err)
|
return fmt.Errorf("failed to build paginated request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -311,7 +326,7 @@ func runAPI(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if pageStatus < 200 || pageStatus >= 300 {
|
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)
|
bodies = append(bodies, pageBody)
|
||||||
nextURL = parseLinkHeaderNext(pageHeader.Get("Link"))
|
nextURL = parseLinkHeaderNext(pageHeader.Get("Link"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue