-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Modifier: Request modifications on-demand
- Loading branch information
Showing
15 changed files
with
1,294 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package internal | ||
|
||
import ( | ||
"github.com/bountysecurity/gbounty/internal/profile" | ||
"github.com/bountysecurity/gbounty/internal/request" | ||
) | ||
|
||
// Modifier defines the behavior of a request modifier, which is a component | ||
// capable of modifying the given request based on certain given requirements. | ||
type Modifier interface { | ||
Modify(*profile.Step, Template, request.Request) request.Request | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package modifier | ||
|
||
import ( | ||
scan "github.com/bountysecurity/gbounty/internal" | ||
"github.com/bountysecurity/gbounty/internal/profile" | ||
"github.com/bountysecurity/gbounty/internal/request" | ||
) | ||
|
||
// CustomTokens must implement the [scan.Modifier] interface. | ||
var _ scan.Modifier = CustomTokens{} | ||
|
||
// CustomTokens is a [scan.Modifier] implementation that modifies the request | ||
// with [scan.CustomTokens]. That's it, keys (placeholders) replaced with specific | ||
// values. | ||
type CustomTokens struct { | ||
ct scan.CustomTokens | ||
} | ||
|
||
// NewCustomTokens is a constructor function that creates a new instance of | ||
// the [CustomTokens] modifier with the given [scan.CustomTokens]. | ||
func NewCustomTokens(ct scan.CustomTokens) CustomTokens { | ||
return CustomTokens{ | ||
ct: ct, | ||
} | ||
} | ||
|
||
// Modify modifies the request. | ||
func (m CustomTokens) Modify(_ *profile.Step, _ scan.Template, req request.Request) request.Request { | ||
return replace(req, m.ct) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package modifier | ||
|
||
import ( | ||
scan "github.com/bountysecurity/gbounty/internal" | ||
"github.com/bountysecurity/gbounty/internal/profile" | ||
"github.com/bountysecurity/gbounty/internal/request" | ||
) | ||
|
||
// Email must implement the [scan.Modifier] interface. | ||
var _ scan.Modifier = Email{} | ||
|
||
// Email is a [scan.Modifier] implementation that modifies the request | ||
// by replacing the {EMAIL} placeholder with the given email address. | ||
type Email struct { | ||
email string | ||
} | ||
|
||
const ( | ||
// {EMAIL} is the label used by GBounty for email address. | ||
emailLabel = "{EMAIL}" | ||
) | ||
|
||
// NewEmail is a constructor function that creates a new instance of | ||
// the [Email] modifier with the given email address. | ||
func NewEmail(email string) Email { | ||
return Email{ | ||
email: email, | ||
} | ||
} | ||
|
||
// Modify modifies the request by replacing the {EMAIL} placeholder with the given email address. | ||
func (e Email) Modify(_ *profile.Step, _ scan.Template, req request.Request) request.Request { | ||
return replace(req, map[string]string{emailLabel: e.email}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package modifier | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
scan "github.com/bountysecurity/gbounty/internal" | ||
"github.com/bountysecurity/gbounty/internal/profile" | ||
"github.com/bountysecurity/gbounty/internal/request" | ||
) | ||
|
||
// HTTPMethod must implement the [scan.Modifier] interface. | ||
var _ scan.Modifier = HTTPMethod{} | ||
|
||
// HTTPMethod is a [scan.Modifier] implementation that changes the HTTP method of a [request.Request]. | ||
type HTTPMethod struct{} | ||
|
||
// NewHTTPMethod is a constructor function that creates a new instance of the [HTTPMethod] modifier. | ||
func NewHTTPMethod() HTTPMethod { | ||
return HTTPMethod{} | ||
} | ||
|
||
// Modify modifies the request by changing the HTTP method. | ||
// It follows the following behavior: | ||
// - In case of switching POST => GET, it sets the body as path, and removes the "Content-Length" header. | ||
// - In case of switching GET => POST, it sets the query as the body, and adds the "Content-Length" header. | ||
// - In case of swapping GET <=> POST, it sets the query as the body, the body as path, | ||
// and updates the "Content-Length" header accordingly. | ||
func (HTTPMethod) Modify(step *profile.Step, _ scan.Template, req request.Request) request.Request { | ||
cloned := req.Clone() | ||
if step == nil || !step.ChangeHTTPMethod { | ||
return cloned | ||
} | ||
|
||
switch { | ||
case step.ChangeHTTPMethodType.PostToGet() && req.Method == http.MethodPost: | ||
cloned.Method = http.MethodGet | ||
path, _ := split(req.Path) | ||
cloned.Path = merge(path, strings.TrimSpace(string(cloned.Body))) | ||
cloned.Body = nil | ||
delete(cloned.Headers, "Content-Length") | ||
|
||
case step.ChangeHTTPMethodType.GetToPost() && req.Method == http.MethodGet: | ||
cloned.Method = http.MethodPost | ||
path, query := split(req.Path) | ||
cloned.Path = path | ||
cloned.SetBody([]byte(query)) | ||
|
||
case step.ChangeHTTPMethodType.SwapGetAndPost() && (req.Method == http.MethodGet || req.Method == http.MethodPost): | ||
if req.Method == http.MethodPost { | ||
cloned.Method = http.MethodGet | ||
} else if req.Method == http.MethodGet { | ||
cloned.Method = http.MethodPost | ||
} | ||
|
||
path, query := split(req.Path) | ||
cloned.Path = merge(path, strings.TrimSpace(string(cloned.Body))) | ||
cloned.SetBody([]byte(query)) | ||
} | ||
|
||
return cloned | ||
} | ||
|
||
func merge(path, body string) string { | ||
if !strings.Contains(path, "?") { | ||
return fmt.Sprintf("%s?%s", path, body) | ||
} | ||
|
||
return fmt.Sprintf("%s&%s", path, body) | ||
} | ||
|
||
func split(path string) (string, string) { | ||
idx := strings.Index(path, "?") | ||
|
||
if idx < 0 { | ||
return path, "" | ||
} | ||
|
||
return path[:idx], path[idx+1:] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
package modifier_test | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
scan "github.com/bountysecurity/gbounty/internal" | ||
"github.com/bountysecurity/gbounty/internal/modifier" | ||
"github.com/bountysecurity/gbounty/internal/profile" | ||
"github.com/bountysecurity/gbounty/internal/request" | ||
) | ||
|
||
func TestHTTPMethod_Modify(t *testing.T) { | ||
t.Parallel() | ||
|
||
tcs := map[string]struct { | ||
tpl scan.Template | ||
req request.Request | ||
step profile.Step | ||
|
||
expMethod string | ||
expPath string | ||
expBody []byte | ||
}{ | ||
"disabled change http method does nothing": { | ||
req: request.Request{ | ||
Method: http.MethodPost, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: false, | ||
ChangeHTTPMethodType: profile.ChangePostToGet, | ||
}, | ||
expMethod: http.MethodPost, | ||
expPath: "search.php?test=query", | ||
expBody: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
"post to get on get does nothing": { | ||
req: request.Request{ | ||
Method: http.MethodGet, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: true, | ||
ChangeHTTPMethodType: profile.ChangePostToGet, | ||
}, | ||
expMethod: http.MethodGet, | ||
expPath: "search.php?test=query", | ||
expBody: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
"post to get on put does nothing": { | ||
req: request.Request{ | ||
Method: http.MethodPut, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: true, | ||
ChangeHTTPMethodType: profile.ChangePostToGet, | ||
}, | ||
expMethod: http.MethodPut, | ||
expPath: "search.php?test=query", | ||
expBody: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
"post to get on post sets get": { | ||
req: request.Request{ | ||
Method: http.MethodPost, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: true, | ||
ChangeHTTPMethodType: profile.ChangePostToGet, | ||
}, | ||
expMethod: http.MethodGet, | ||
expPath: "search.php?searchFor=bananas&goButton=oooo", | ||
expBody: nil, | ||
}, | ||
"get to post on post does nothing": { | ||
req: request.Request{ | ||
Method: http.MethodPost, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: true, | ||
ChangeHTTPMethodType: profile.ChangeGetToPost, | ||
}, | ||
expMethod: http.MethodPost, | ||
expPath: "search.php?test=query", | ||
expBody: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
"get to post on put does nothing": { | ||
req: request.Request{ | ||
Method: http.MethodPut, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: true, | ||
ChangeHTTPMethodType: profile.ChangeGetToPost, | ||
}, | ||
expMethod: http.MethodPut, | ||
expPath: "search.php?test=query", | ||
expBody: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
"get to post on get sets post": { | ||
req: request.Request{ | ||
Method: http.MethodGet, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: true, | ||
ChangeHTTPMethodType: profile.ChangeGetToPost, | ||
}, | ||
expMethod: http.MethodPost, | ||
expPath: "search.php", | ||
expBody: []byte("test=query"), | ||
}, | ||
"swap get and post on put does nothing": { | ||
req: request.Request{ | ||
Method: http.MethodPut, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: true, | ||
ChangeHTTPMethodType: profile.ChangeSwapGetAndPost, | ||
}, | ||
expMethod: http.MethodPut, | ||
expPath: "search.php?test=query", | ||
expBody: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
"swap get and post on get sets post": { | ||
req: request.Request{ | ||
Method: http.MethodGet, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: true, | ||
ChangeHTTPMethodType: profile.ChangeSwapGetAndPost, | ||
}, | ||
expMethod: http.MethodPost, | ||
expPath: "search.php?searchFor=bananas&goButton=oooo", | ||
expBody: []byte("test=query"), | ||
}, | ||
"swap get and post on post sets get": { | ||
req: request.Request{ | ||
Method: http.MethodPost, | ||
Path: "search.php?test=query", | ||
Body: []byte("searchFor=bananas&goButton=oooo"), | ||
}, | ||
step: profile.Step{ | ||
ChangeHTTPMethod: true, | ||
ChangeHTTPMethodType: profile.ChangeSwapGetAndPost, | ||
}, | ||
expMethod: http.MethodGet, | ||
expPath: "search.php?searchFor=bananas&goButton=oooo", | ||
expBody: []byte("test=query"), | ||
}, | ||
} | ||
|
||
for name, tc := range tcs { | ||
tc := tc | ||
|
||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
m := modifier.NewHTTPMethod() | ||
modified := m.Modify(&tc.step, tc.tpl, tc.req) | ||
assert.Equal(t, tc.expMethod, modified.Method) | ||
assert.Equal(t, tc.expPath, modified.Path) | ||
assert.Equal(t, tc.expBody, modified.Body) | ||
}) | ||
} | ||
} |
Oops, something went wrong.