Skip to content

Commit ebd3857

Browse files
authored
Merge pull request #51 from projectdiscovery/issue-50-double-encoding
fix double url encoding in path
2 parents 0c040d6 + 54f3176 commit ebd3857

File tree

7 files changed

+150
-60
lines changed

7 files changed

+150
-60
lines changed

.github/workflows/build-test.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ jobs:
2121
uses: actions/checkout@v3
2222

2323
- name: Test
24-
run: go test ./...
24+
run: go test ./...
25+
26+
- name: Run Example
27+
run: go run .
28+
working-directory: examples/

README.md

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,35 @@ Heavily inspired from [https://github.com/hashicorp/go-retryablehttp](https://gi
44

55
### Usage
66

7-
```go
8-
package main
9-
10-
import (
11-
"fmt"
12-
"io/ioutil"
13-
14-
"github.com/projectdiscovery/retryablehttp-go"
15-
)
16-
17-
func main() {
18-
opts := retryablehttp.DefaultOptionsSpraying
19-
// opts := retryablehttp.DefaultOptionsSingle // use single options for single host
20-
client := retryablehttp.NewClient(opts)
21-
resp, err := client.Get("https://example.com")
22-
if err != nil {
23-
panic(err)
24-
}
25-
defer resp.Body.Close()
26-
27-
data, err := io.ReadAll(resp.Body)
28-
if err != nil {
29-
panic(err)
30-
}
31-
fmt.Printf("Data: %v\n", string(data))
32-
}
7+
Example of using `retryablehttp` in Go Code is available in [examples](examples/) folder
8+
Examples of using Nuclei From Go Code to run templates on targets are provided in the examples folder.
9+
10+
11+
12+
13+
### url encoding and parsing issues
14+
15+
`retryablehttp.Request` by default handles some [url encoding and parameters issues](https://github.com/projectdiscovery/utils/blob/main/url/README.md). since `http.Request` internally uses `url.Parse()` to parse url specified in request it creates some inconsistencies for below urls and other non-RFC compilant urls
16+
17+
```
18+
// below urls are either normalized or returns error when used in `http.NewRequest()`
19+
https://scanme.sh/%invalid
20+
https://scanme.sh/w%0d%2e/
21+
scanme.sh/with/path?some'param=`'+OR+ORDER+BY+1--
3322
```
23+
All above mentioned cases are handled internally in `retryablehttp`.
24+
25+
26+
### request with unsafe urls
27+
28+
`retryablehttp` allows creating requests with unsafe urls but requires some extra steps if `path` of url contains encoded characters (ex: `/%invalid/path`).
29+
30+
- `Request.Prepare()` method should be called before request is executed and this applies a quick fix to avoid double url encoding (ex: `%e5` => `%25e5`)
31+
32+
Note: this is a optional feature and only required if we want to allow unsafe urls. If `Request.Prepare()` is not called it follows the standard behaviour.
33+
34+
### Note
35+
It is not recommended to update `url.URL` instance of `Request` once a new request is created (ex `req.URL.Path = xyz`) due to internal logic or urls.
36+
In any case if it is not possible to follow above point due to some reason helper methods are available to reflect such changes
37+
38+
- `Request.Update()` commits any changes made to query parameters (ex: `Request.URL.Query().Add(x,y)`)

examples/main.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/projectdiscovery/retryablehttp-go"
8+
)
9+
10+
func main() {
11+
opts := retryablehttp.DefaultOptionsSpraying
12+
// opts := retryablehttp.DefaultOptionsSingle // use single options for single host
13+
client := retryablehttp.NewClient(opts)
14+
resp, err := client.Get("https://scanme.sh")
15+
if err != nil {
16+
panic(err)
17+
}
18+
defer resp.Body.Close()
19+
20+
data, err := io.ReadAll(resp.Body)
21+
if err != nil {
22+
panic(err)
23+
}
24+
fmt.Printf("Data: %v\n", string(data))
25+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.18
44

55
require (
66
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809
7-
github.com/projectdiscovery/utils v0.0.4-0.20230117135930-7371ae6a739d
7+
github.com/projectdiscovery/utils v0.0.7
88
golang.org/x/net v0.5.0
99
)
1010

go.sum

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,10 @@ github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl
88
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
99
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
1010
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
11-
github.com/projectdiscovery/utils v0.0.4-0.20221201124851-f8524345b6d3 h1:sOvfN3xHLiBMb6GJ3yDxBmPnN0dh3xllaQXQYo7CFUo=
12-
github.com/projectdiscovery/utils v0.0.4-0.20221201124851-f8524345b6d3/go.mod h1:PCwA5YuCYWPgHaGiZmr53/SA9iGQmAnw7DSHuhr8VPQ=
13-
github.com/projectdiscovery/utils v0.0.4-0.20230117121210-1eaffe0d0834 h1:ehoX21rVDm+i7/o8OpTTtDdbesHshF0AD13gbc21wBA=
14-
github.com/projectdiscovery/utils v0.0.4-0.20230117121210-1eaffe0d0834/go.mod h1:PCwA5YuCYWPgHaGiZmr53/SA9iGQmAnw7DSHuhr8VPQ=
15-
github.com/projectdiscovery/utils v0.0.4-0.20230117132455-e51a5b2e562c h1:+iHkNvGP/1Cbq6lo8htaQZd3fWch8E9OKD0xTUwS+Zo=
16-
github.com/projectdiscovery/utils v0.0.4-0.20230117132455-e51a5b2e562c/go.mod h1:PCwA5YuCYWPgHaGiZmr53/SA9iGQmAnw7DSHuhr8VPQ=
17-
github.com/projectdiscovery/utils v0.0.4-0.20230117135930-7371ae6a739d h1:iB/n2/NL4oh1IaEcqX6pBxj0WHfYN7finzNOKVNVISM=
18-
github.com/projectdiscovery/utils v0.0.4-0.20230117135930-7371ae6a739d/go.mod h1:PCwA5YuCYWPgHaGiZmr53/SA9iGQmAnw7DSHuhr8VPQ=
11+
github.com/projectdiscovery/utils v0.0.7 h1:jqDuZedy3t66o6ejQUXjgNWbyAHqiBqLAUDkst9DA2M=
12+
github.com/projectdiscovery/utils v0.0.7/go.mod h1:PCwA5YuCYWPgHaGiZmr53/SA9iGQmAnw7DSHuhr8VPQ=
13+
github.com/projectdiscovery/utils v0.0.8-0.20230206142604-469c07cf5050 h1:PwtYD40LMJag5jpB3F2bi1y4tLAMUPIeuWO37txfbOI=
14+
github.com/projectdiscovery/utils v0.0.8-0.20230206142604-469c07cf5050/go.mod h1:PCwA5YuCYWPgHaGiZmr53/SA9iGQmAnw7DSHuhr8VPQ=
1915
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
2016
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
2117
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=

request.go

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ func (r *Request) Update() {
107107
updateScheme(r.URL.URL)
108108
}
109109

110+
// Prepares request (applies hot patch if any. Ex: Path Unescape to prevent double url encoding)
111+
// calling multiple times may have unexpected results unlike Update() method
112+
func (r *Request) Prepare() {
113+
// hot patch to avoid url path encoding issues
114+
// by default we decode encoded/escaped path and internally http.Request encodes them again
115+
// this avoid double url encoding (or reencoding or path)
116+
if rawPath, err := url.QueryUnescape(r.URL.Path); err == nil {
117+
r.URL.Path = rawPath
118+
}
119+
r.Update()
120+
}
121+
110122
// SetURL updates request url (i.e http.Request.URL) with given url
111123
func (r *Request) SetURL(u *urlutil.URL) {
112124
r.URL = u
@@ -215,23 +227,29 @@ func FromRequestWithTrace(r *http.Request) (*Request, error) {
215227
}
216228

217229
// NewRequest creates a new wrapped request.
218-
func NewRequest(method, url string, body interface{}) (*Request, error) {
230+
func NewRequestFromURL(method string, urlx *urlutil.URL, body interface{}) (*Request, error) {
231+
return NewRequestFromURLWithContext(context.Background(), method, urlx, body)
232+
}
233+
234+
// NewRequestWithContext creates a new wrapped request with context
235+
func NewRequestFromURLWithContext(ctx context.Context, method string, urlx *urlutil.URL, body interface{}) (*Request, error) {
219236
bodyReader, contentLength, err := getReusableBodyandContentLength(body)
220237
if err != nil {
221238
return nil, err
222239
}
223240

224-
urlx, err := urlutil.Parse(url)
225-
if err != nil {
226-
return nil, err
227-
}
228-
httpReq, err := http.NewRequest(method, url, nil)
241+
// we provide a url without path to http.NewRequest at start and then replace url instance directly
242+
// because `http.NewRequest()` internally parses using `url.Parse()` this removes/overrides any
243+
// patches done by urlutil.URL in unsafe mode (ex: https://scanme.sh/%invalid)
244+
// Note: this does not have any impact on actual path when sending request
245+
// `http.NewRequestxxx` internally only uses `u.Host` and all other data is stored in `url.URL` instance
246+
httpReq, err := http.NewRequestWithContext(ctx, method, "https://"+urlx.Host, nil)
229247
if err != nil {
230248
return nil, err
231249
}
250+
urlx.Update()
232251
httpReq.URL = urlx.URL
233252
updateScheme(httpReq.URL)
234-
235253
// content-length and body should be assigned only
236254
// if request has body
237255
if bodyReader != nil {
@@ -242,31 +260,22 @@ func NewRequest(method, url string, body interface{}) (*Request, error) {
242260
return &Request{httpReq, urlx, Metrics{}, nil}, nil
243261
}
244262

245-
// NewRequestWithContext creates a new wrapped request with context
246-
func NewRequestWithContext(ctx context.Context, method, url string, body interface{}) (*Request, error) {
247-
bodyReader, contentLength, err := getReusableBodyandContentLength(body)
263+
// NewRequest creates a new wrapped request
264+
func NewRequest(method, url string, body interface{}) (*Request, error) {
265+
urlx, err := urlutil.Parse(url)
248266
if err != nil {
249267
return nil, err
250268
}
269+
return NewRequestFromURL(method, urlx, body)
270+
}
251271

272+
// NewRequest creates a new wrapped request with given context
273+
func NewRequestWithContext(ctx context.Context, method, url string, body interface{}) (*Request, error) {
252274
urlx, err := urlutil.Parse(url)
253275
if err != nil {
254276
return nil, err
255277
}
256-
httpReq, err := http.NewRequestWithContext(ctx, method, url, nil)
257-
if err != nil {
258-
return nil, err
259-
}
260-
httpReq.URL = urlx.URL
261-
updateScheme(httpReq.URL)
262-
// content-length and body should be assigned only
263-
// if request has body
264-
if bodyReader != nil {
265-
httpReq.ContentLength = contentLength
266-
httpReq.Body = bodyReader
267-
}
268-
269-
return &Request{httpReq, urlx, Metrics{}, nil}, nil
278+
return NewRequestFromURLWithContext(ctx, method, urlx, body)
270279
}
271280

272281
func updateScheme(u *url.URL) {

request_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package retryablehttp_test
22

33
import (
4+
"bufio"
5+
"bytes"
46
"os"
7+
"strings"
58
"testing"
69

710
"github.com/projectdiscovery/retryablehttp-go"
@@ -35,3 +38,51 @@ func TestRequestUrls(t *testing.T) {
3538
}
3639
}
3740
}
41+
42+
func TestEncodedPaths(t *testing.T) {
43+
44+
// test this on all valid crlf payloads
45+
payloads := []string{"%00", "%0a", "%0a%20", "%0d", "%0d%09", "%0d%0a", "%0d%0a%09", "%0d%0a%20", "%0d%20", "%20", "%20%0a", "%20%0d", "%20%0d%0a", "%23%0a", "%23%0a%20", "%23%0d", "%23%0d%0a", "%23%0a", "%25%30", "%25%30%61", "%2e%2e%2f%0d%0a", "%2f%2e%2e%0d%0a", "%2f..%0d%0a", "%3f", "%3f%0a", "%3f%0d", "%3f%0d%0a", "%e5%98%8a%e5%98%8d", "%e5%98%8a%e5%98%8d%0a", "%e5%98%8a%e5%98%8d%0d", "%e5%98%8a%e5%98%8d%0d%0a", "%e5%98%8a%e5%98%8d%e5%98%8a%e5%98%8d"}
46+
47+
// create url using below data and payload
48+
suffix := "/path?param=true"
49+
50+
for _, v := range payloads {
51+
exURL := "https://scanme.sh/" + v + suffix
52+
req, err := retryablehttp.NewRequest("GET", exURL, nil)
53+
if err != nil {
54+
t.Fatalf("got %v with payload %v", err.Error(), v)
55+
}
56+
57+
req.Prepare()
58+
bin, err := req.Dump()
59+
if err != nil {
60+
t.Errorf("failed to dump request body for payload %v got %v", v, err)
61+
}
62+
63+
relPath := getPathFromRaw(bin)
64+
payload := strings.TrimSuffix(relPath, suffix)
65+
payload = strings.TrimPrefix(payload, "/")
66+
67+
if v != payload {
68+
t.Errorf("something went wrong expected `%v` in outgoing request but got-----\n%v\n------", v, string(bin))
69+
}
70+
}
71+
}
72+
73+
func getPathFromRaw(bin []byte) (relpath string) {
74+
buff := bufio.NewReader(bytes.NewReader(bin))
75+
readline:
76+
line, err := buff.ReadString('\n')
77+
if err != nil {
78+
return
79+
}
80+
if strings.Contains(line, "HTTP/1.1") {
81+
parts := strings.Split(line, " ")
82+
if len(parts) == 3 {
83+
relpath = parts[1]
84+
return
85+
}
86+
}
87+
goto readline
88+
}

0 commit comments

Comments
 (0)