diff --git a/pkg/server/http_server.go b/pkg/server/http_server.go index 7770e6b7..1d4bfc57 100644 --- a/pkg/server/http_server.go +++ b/pkg/server/http_server.go @@ -328,6 +328,30 @@ func (h *HTTPServer) defaultHandler(w http.ResponseWriter, req *http.Request) { } } +// b64BodyPrefix marks a request path carrying a base64 encoded response body. +const b64BodyPrefix = "/b64_body:" + +// decodeB64BodyPath decodes a /b64_body: path. +// HasPrefixI is case insensitive, so the payload offset is the prefix length +// rather than a second case-sensitive search. A single trailing slash is +// accepted as a terminator (nuclei templates use it); other slashes are left +// in place because they are valid in StdEncoding. +func decodeB64BodyPath(path string) []byte { + if !stringsutil.HasPrefixI(path, b64BodyPrefix) { + return nil + } + encoded := path[len(b64BodyPrefix):] + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err == nil { + return decoded + } + if strings.HasSuffix(encoded, "/") { + decoded, _ = base64.StdEncoding.DecodeString(strings.TrimSuffix(encoded, "/")) + return decoded + } + return nil +} + // writeResponseFromDynamicRequest writes a response to http.ResponseWriter // based on dynamic data from HTTP URL Query parameters. // @@ -340,13 +364,8 @@ func (h *HTTPServer) defaultHandler(w http.ResponseWriter, req *http.Request) { func writeResponseFromDynamicRequest(w http.ResponseWriter, req *http.Request) { values := req.URL.Query() - if stringsutil.HasPrefixI(req.URL.Path, "/b64_body:") { - firstindex := strings.Index(req.URL.Path, "/b64_body:") - lastIndex := strings.LastIndex(req.URL.Path, "/") - - decodedBytes, _ := base64.StdEncoding.DecodeString(req.URL.Path[firstindex+10 : lastIndex]) - _, _ = w.Write(decodedBytes) - + if decoded := decodeB64BodyPath(req.URL.Path); decoded != nil { + _, _ = w.Write(decoded) } if headers := values["header"]; len(headers) > 0 { for _, header := range headers { diff --git a/pkg/server/http_server_test.go b/pkg/server/http_server_test.go index f45e14a2..dd3de5b0 100644 --- a/pkg/server/http_server_test.go +++ b/pkg/server/http_server_test.go @@ -184,3 +184,69 @@ func TestSessionTotalMetric(t *testing.T) { require.Equal(t, int64(0), atomic.LoadInt64(&stats.Sessions), "sessions should be 0 after deregister") require.Equal(t, int64(1), atomic.LoadInt64(&stats.SessionsTotal), "sessions_total should remain 1 after deregister") } + +func TestDecodeB64BodyPath(t *testing.T) { + example := "this is example body" + exampleB64 := base64.StdEncoding.EncodeToString([]byte(example)) + slashPayload := []byte{0xff, 0xff, 0xff} + slashB64 := base64.StdEncoding.EncodeToString(slashPayload) + require.Equal(t, "////", slashB64) + plusPayload := []byte{0xfb} + plusB64 := base64.StdEncoding.EncodeToString(plusPayload) + require.Contains(t, plusB64, "+") + + tests := []struct { + name string + path string + want []byte + }{ + {name: "trailing slash", path: "/b64_body:" + exampleB64 + "/", want: []byte(example)}, + {name: "no trailing slash", path: "/b64_body:" + exampleB64, want: []byte(example)}, + {name: "uppercase prefix", path: "/B64_BODY:" + exampleB64 + "/", want: []byte(example)}, + {name: "empty payload", path: "/b64_body:", want: []byte{}}, + {name: "slash in payload", path: "/b64_body:" + slashB64, want: slashPayload}, + {name: "slash in payload with terminator", path: "/b64_body:" + slashB64 + "/", want: slashPayload}, + {name: "plus in payload", path: "/b64_body:" + plusB64, want: plusPayload}, + {name: "unrelated path", path: "/other", want: nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.NotPanics(t, func() { + require.Equal(t, tt.want, decodeB64BodyPath(tt.path)) + }) + }) + } +} + +func TestWriteResponseFromDynamicRequestB64Path(t *testing.T) { + example := "this is example body" + exampleB64 := base64.StdEncoding.EncodeToString([]byte(example)) + slashPayload := []byte{0xff, 0xff, 0xff} + slashB64 := base64.StdEncoding.EncodeToString(slashPayload) + + tests := []struct { + name string + path string + want []byte + }{ + {name: "trailing slash", path: "/b64_body:" + exampleB64 + "/", want: []byte(example)}, + {name: "no trailing slash", path: "/b64_body:" + exampleB64, want: []byte(example)}, + {name: "uppercase prefix", path: "/B64_BODY:" + exampleB64 + "/", want: []byte(example)}, + {name: "empty payload", path: "/b64_body:", want: []byte{}}, + {name: "slash in payload", path: "/b64_body:" + slashB64, want: slashPayload}, + {name: "slash in payload with terminator", path: "/b64_body:" + slashB64 + "/", want: slashPayload}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + req.URL.Path = tt.path + w := httptest.NewRecorder() + require.NotPanics(t, func() { + writeResponseFromDynamicRequest(w, req) + }) + body, err := io.ReadAll(w.Result().Body) + require.NoError(t, err) + require.Equal(t, tt.want, body) + }) + } +}