Skip to content

Commit

Permalink
Modifier: Request modifications on-demand
Browse files Browse the repository at this point in the history
  • Loading branch information
joanlopez committed May 5, 2024
1 parent 4113162 commit 3016bd6
Show file tree
Hide file tree
Showing 15 changed files with 1,294 additions and 0 deletions.
12 changes: 12 additions & 0 deletions internal/interfaces.go
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
}
30 changes: 30 additions & 0 deletions internal/modifier/custom_tokens.go
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)
}
34 changes: 34 additions & 0 deletions internal/modifier/email.go
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})
}
81 changes: 81 additions & 0 deletions internal/modifier/http_method.go
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:]
}
181 changes: 181 additions & 0 deletions internal/modifier/http_method_test.go
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)
})
}
}
Loading

0 comments on commit 3016bd6

Please sign in to comment.