Skip to content

307/308 Redirects Fail for POST Requests with Non-Empty Body (Missing req.GetBody) #493

@zzyjsj

Description

@zzyjsj

retryablehttp version:

v1.0.133

Current Behavior:

When sending POST requests with a non-empty body (e.g., multipart/form-data) that trigger 307/308 redirects via retryablehttp-go (especially with DefaultOptionsSpraying), the redirects fail.

Root cause: The underlying http.Request.GetBody is nil by default (not auto-populated by the library), and Go’s net/http blocks 307/308 redirects for POST requests with non-empty bodies if GetBody is unimplemented。

Steps To Reproduce:

I have written the test code as follows

package main

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/projectdiscovery/retryablehttp-go"
)

var (
	url      = "http://127.0.0.1:8080/redirect"
	boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad"
)

func body() *bytes.Buffer {
	var formBuilder strings.Builder
	formBuilder.WriteString(fmt.Sprintf("--%s\r\n", boundary))
	formBuilder.WriteString(`Content-Disposition: form-data; name="1"` + "\r\n\r\n")
	formBuilder.WriteString(`"$@0"` + "\r\n")

	formBuilder.WriteString(fmt.Sprintf("--%s--\r\n", boundary))

	return bytes.NewBufferString(formBuilder.String())
}

func normalHttp() {
	req, err := http.NewRequest("POST", url, body())
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}

	req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
	req.Header.Set("Host", "127.0.0.1:8080")

	client := &http.Client{}

	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	defer resp.Body.Close()

	fmt.Printf("code: %d\n", resp.StatusCode)
	fmt.Printf("currentUrl:%s\noriginUrl:%s\n", resp.Request.URL.String(), req.URL.String())
}

func retryHttp() {
	opts := retryablehttp.DefaultOptionsSpraying
	httpclient := retryablehttp.NewClient(opts)
	req, _ := retryablehttp.NewRequestWithContext(context.Background(), "POST", url, body())
	//req.GetBody = func() (io.ReadCloser, error) {
	//	return io.NopCloser(body()), nil
	//} // issue  NOT set req.GetBody
	req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
	req.Header.Set("Host", "127.0.0.1:8080")
	resp, err := httpclient.Do(req)
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	defer resp.Body.Close()

	fmt.Printf("code: %d\n", resp.StatusCode)
	fmt.Printf("currentUrl:%s\noriginUrl:%s\n", resp.Request.URL.String(), req.URL.String())
}

func main() {
	fmt.Println("-----------http.Client-----------")
	normalHttp()
	fmt.Println("-----------retryablehttp-----------")
	retryHttp()
}

Redirect service for testing

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

const (
	ListenAddr   = ":8080"                 
	RedirectPath = "/redirect"             
	TargetPath   = "/target"              
	TargetHost   = "http://127.0.0.1:8080"
)

func redirectHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.WriteHeader(http.StatusMethodNotAllowed)
		fmt.Fprintf(w, "only post:%s\n", r.Method)
		return
	}
	log.Printf("[307 redirect] path:%s", r.URL.Path)

	_, err := io.ReadAll(r.Body)
	if err != nil {
		log.Printf("err:%v", err)
	}
	defer r.Body.Close()

	w.Header().Set("Location", TargetHost+TargetPath)
	w.WriteHeader(http.StatusTemporaryRedirect)
	fmt.Fprintf(w, "redirect to target:%s%s\n", TargetHost, TargetPath)
}

func targetHandler(w http.ResponseWriter, r *http.Request) {
	log.Printf("[307 target] path:%s ", r.URL.Path)
	_, err := io.ReadAll(r.Body)
	if err != nil {
		log.Printf("err:%v", err)
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, "err:%v\n", err)
		return
	}
	defer r.Body.Close()

	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "307 success\n")
}

func main() {
	http.HandleFunc(RedirectPath, redirectHandler) // 307 redirect
	http.HandleFunc(TargetPath, targetHandler)     // 307 target

	log.Printf("307 redirect:%s%s", TargetHost, RedirectPath)
	log.Printf("307 target:%s%s", TargetHost, TargetPath)
	if err := http.ListenAndServe(ListenAddr, nil); err != nil {
		log.Fatalf("err:%v", err)
	}
}

This library (retryablehttp-go) may have an impact on Nuclei.

Anything else:

Metadata

Metadata

Assignees

Labels

Type: BugInconsistencies or issues which will cause an issue or problem for users or implementors.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions