Skip to content

Commit 4dff892

Browse files
author
Jim Kalafut
committed
Support processing parameters sent as a URL-encoded form
The approach taken is intended to be conservative since we've always assumed JSON and not required Content-Type. Tools and libraries that default to "application/x-www-form-urlencoded" even when JSON is being sent (e.g. curl) will break without these extra backwards compatibility measures. This PR is a reboot of: #5935
1 parent 8cc4d92 commit 4dff892

9 files changed

+218
-18
lines changed

http/handler.go

+73-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"io"
1010
"io/ioutil"
11+
"mime"
1112
"net"
1213
"net/http"
1314
"net/textproto"
@@ -566,7 +567,7 @@ func parseQuery(values url.Values) map[string]interface{} {
566567
return nil
567568
}
568569

569-
func parseRequest(perfStandby bool, r *http.Request, w http.ResponseWriter, out interface{}) (io.ReadCloser, error) {
570+
func parseJSONRequest(perfStandby bool, r *http.Request, w http.ResponseWriter, out interface{}) (io.ReadCloser, error) {
570571
// Limit the maximum number of bytes to MaxRequestSize to protect
571572
// against an indefinite amount of data being read.
572573
reader := r.Body
@@ -598,6 +599,43 @@ func parseRequest(perfStandby bool, r *http.Request, w http.ResponseWriter, out
598599
return nil, err
599600
}
600601

602+
// parseFormRequest parses values from a form POST.
603+
//
604+
// A nil map will be returned if the format is empty or invalid.
605+
func parseFormRequest(r *http.Request) (map[string]interface{}, error) {
606+
maxRequestSize := r.Context().Value("max_request_size")
607+
if maxRequestSize != nil {
608+
max, ok := maxRequestSize.(int64)
609+
if !ok {
610+
return nil, errors.New("could not parse max_request_size from request context")
611+
}
612+
if max > 0 {
613+
r.Body = ioutil.NopCloser(io.LimitReader(r.Body, max))
614+
}
615+
}
616+
if err := r.ParseForm(); err != nil {
617+
return nil, err
618+
}
619+
620+
var data map[string]interface{}
621+
622+
if len(r.PostForm) != 0 {
623+
data = make(map[string]interface{}, len(r.PostForm))
624+
for k, v := range r.PostForm {
625+
switch len(v) {
626+
case 0, 1:
627+
data[k] = v[0]
628+
default:
629+
// Almost anywhere taking in a string list can take in comma
630+
// separated values, and really this is super niche anyways
631+
data[k] = strings.Join(v, ",")
632+
}
633+
}
634+
}
635+
636+
return data, nil
637+
}
638+
601639
// handleRequestForwarding determines whether to forward a request or not,
602640
// falling back on the older behavior of redirecting the client
603641
func handleRequestForwarding(core *vault.Core, handler http.Handler) http.Handler {
@@ -960,6 +998,40 @@ func parseMFAHeader(req *logical.Request) error {
960998
return nil
961999
}
9621000

1001+
// isForm tries to determine whether the request should be
1002+
// processed as a form or as JSON.
1003+
//
1004+
// Virtually all existing use cases have assumed processing as JSON,
1005+
// and there has not been a Content-Type requirement in the API. In order to
1006+
// maintain backwards compatibility, this will err on the side of JSON.
1007+
// The request will be considered a form only if:
1008+
//
1009+
// 1. The content type is "application/x-www-form-urlencoded"
1010+
// 2. The start of the request doesn't look like JSON. For this test we
1011+
// we expect the body to begin with { or [, ignoring leading whitespace.
1012+
func isForm(head []byte, contentType string) bool {
1013+
contentType, _, err := mime.ParseMediaType(contentType)
1014+
1015+
if err != nil || contentType != "application/x-www-form-urlencoded" {
1016+
return false
1017+
}
1018+
1019+
// Look for the start of JSON or not-JSON, skipping any insignificant
1020+
// whitespace (per https://tools.ietf.org/html/rfc7159#section-2).
1021+
for _, c := range head {
1022+
switch c {
1023+
case ' ', '\t', '\n', '\r':
1024+
continue
1025+
case '[', '{': // JSON
1026+
return false
1027+
default: // not JSON
1028+
return true
1029+
}
1030+
}
1031+
1032+
return true
1033+
}
1034+
9631035
func respondError(w http.ResponseWriter, status int, err error) {
9641036
logical.RespondError(w, status, err)
9651037
}

http/handler_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package http
22

33
import (
44
"context"
5+
"crypto/tls"
56
"encoding/json"
67
"errors"
8+
"io/ioutil"
79
"net/http"
810
"net/http/httptest"
911
"net/textproto"
12+
"net/url"
1013
"reflect"
1114
"strings"
1215
"testing"
@@ -676,3 +679,63 @@ func testNonPrintable(t *testing.T, disable bool) {
676679
testResponseStatus(t, resp, 400)
677680
}
678681
}
682+
683+
func TestHandler_Parse_Form(t *testing.T) {
684+
cluster := vault.NewTestCluster(t, &vault.CoreConfig{}, &vault.TestClusterOptions{
685+
HandlerFunc: Handler,
686+
})
687+
cluster.Start()
688+
defer cluster.Cleanup()
689+
690+
cores := cluster.Cores
691+
692+
core := cores[0].Core
693+
vault.TestWaitActive(t, core)
694+
695+
c := cleanhttp.DefaultClient()
696+
c.Transport = &http.Transport{
697+
TLSClientConfig: &tls.Config{
698+
RootCAs: cluster.RootCAs,
699+
},
700+
}
701+
702+
values := url.Values{
703+
"zip": []string{"zap"},
704+
"abc": []string{"xyz"},
705+
"multi": []string{"first", "second"},
706+
}
707+
req, err := http.NewRequest("POST", cores[0].Client.Address()+"/v1/secret/foo", nil)
708+
if err != nil {
709+
t.Fatal(err)
710+
}
711+
req.Body = ioutil.NopCloser(strings.NewReader(values.Encode()))
712+
req.Header.Set("x-vault-token", cluster.RootToken)
713+
req.Header.Set("content-type", "application/x-www-form-urlencoded")
714+
resp, err := c.Do(req)
715+
if err != nil {
716+
t.Fatal(err)
717+
}
718+
719+
if resp.StatusCode != 204 {
720+
t.Fatalf("bad response: %#v\nrequest was: %#v\nurl was: %#v", *resp, *req, req.URL)
721+
}
722+
723+
client := cores[0].Client
724+
client.SetToken(cluster.RootToken)
725+
726+
apiResp, err := client.Logical().Read("secret/foo")
727+
if err != nil {
728+
t.Fatal(err)
729+
}
730+
if apiResp == nil {
731+
t.Fatal("api resp is nil")
732+
}
733+
expected := map[string]interface{}{
734+
"zip": "zap",
735+
"abc": "xyz",
736+
"multi": "first,second",
737+
}
738+
if diff := deep.Equal(expected, apiResp.Data); diff != nil {
739+
t.Fatal(diff)
740+
}
741+
}

http/logical.go

+49-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package http
22

33
import (
4+
"bufio"
45
"encoding/base64"
56
"encoding/json"
67
"fmt"
@@ -20,6 +21,24 @@ import (
2021
"go.uber.org/atomic"
2122
)
2223

24+
// bufferedReader can be used to replace a request body with a buffered
25+
// version. The Close method invokes the original Closer.
26+
type bufferedReader struct {
27+
*bufio.Reader
28+
rOrig io.ReadCloser
29+
}
30+
31+
func newBufferedReader(r io.ReadCloser) *bufferedReader {
32+
return &bufferedReader{
33+
Reader: bufio.NewReader(r),
34+
rOrig: r,
35+
}
36+
}
37+
38+
func (b *bufferedReader) Close() error {
39+
return b.rOrig.Close()
40+
}
41+
2342
func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http.Request) (*logical.Request, io.ReadCloser, int, error) {
2443
ns, err := namespace.FromContext(r.Context())
2544
if err != nil {
@@ -71,16 +90,37 @@ func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http.
7190

7291
case "POST", "PUT":
7392
op = logical.UpdateOperation
74-
// Parse the request if we can
75-
if op == logical.UpdateOperation {
76-
// If we are uploading a snapshot we don't want to parse it. Instead
77-
// we will simply add the HTTP request to the logical request object
78-
// for later consumption.
79-
if path == "sys/storage/raft/snapshot" || path == "sys/storage/raft/snapshot-force" {
80-
passHTTPReq = true
81-
origBody = r.Body
93+
94+
// Buffer the request body in order to allow us to peek at the beginning
95+
// without consuming it. This approach involves no copying.
96+
bufferedBody := newBufferedReader(r.Body)
97+
r.Body = bufferedBody
98+
99+
// If we are uploading a snapshot we don't want to parse it. Instead
100+
// we will simply add the HTTP request to the logical request object
101+
// for later consumption.
102+
if path == "sys/storage/raft/snapshot" || path == "sys/storage/raft/snapshot-force" {
103+
passHTTPReq = true
104+
origBody = r.Body
105+
} else {
106+
// Sample the first bytes to determine whether this should be parsed as
107+
// a form or as JSON. The amount to look ahead (512 bytes) is arbitrary
108+
// but extremely tolerant (i.e. allowing 511 bytes of leading whitespace
109+
// and an incorrect content-type).
110+
head, err := bufferedBody.Peek(512)
111+
if err != nil && err != bufio.ErrBufferFull && err != io.EOF {
112+
return nil, nil, http.StatusBadRequest, err
113+
}
114+
115+
if isForm(head, r.Header.Get("Content-Type")) {
116+
formData, err := parseFormRequest(r)
117+
if err != nil {
118+
return nil, nil, http.StatusBadRequest, fmt.Errorf("error parsing form data: %w", err)
119+
}
120+
121+
data = formData
82122
} else {
83-
origBody, err = parseRequest(perfStandby, r, w, &data)
123+
origBody, err = parseJSONRequest(perfStandby, r, w, &data)
84124
if err == io.EOF {
85125
data = nil
86126
err = nil

http/logical_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,28 @@ func TestLogical_Audit_invalidWrappingToken(t *testing.T) {
437437
}
438438
}
439439
}
440+
441+
func TestLogical_ShouldParseForm(t *testing.T) {
442+
const formCT = "application/x-www-form-urlencoded"
443+
444+
tests := map[string]struct {
445+
prefix string
446+
contentType string
447+
isForm bool
448+
}{
449+
"JSON": {`{"a":42}`, formCT, false},
450+
"JSON 2": {`[42]`, formCT, false},
451+
"JSON w/leading space": {" \n\n\r\t [42] ", formCT, false},
452+
"Form": {"a=42&b=dog", formCT, true},
453+
"Form w/wrong CT": {"a=42&b=dog", "application/json", false},
454+
}
455+
456+
for name, test := range tests {
457+
isForm := isForm([]byte(test.prefix), test.contentType)
458+
459+
if isForm != test.isForm {
460+
t.Fatalf("%s fail: expected isForm %t, got %t", name, test.isForm, isForm)
461+
}
462+
}
463+
464+
}

http/sys_generate_root.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func handleSysGenerateRootAttemptGet(core *vault.Core, w http.ResponseWriter, r
8686
func handleSysGenerateRootAttemptPut(core *vault.Core, w http.ResponseWriter, r *http.Request, generateStrategy vault.GenerateRootStrategy) {
8787
// Parse the request
8888
var req GenerateRootInitRequest
89-
if _, err := parseRequest(core.PerfStandby(), r, w, &req); err != nil && err != io.EOF {
89+
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil && err != io.EOF {
9090
respondError(w, http.StatusBadRequest, err)
9191
return
9292
}
@@ -132,7 +132,7 @@ func handleSysGenerateRootUpdate(core *vault.Core, generateStrategy vault.Genera
132132
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
133133
// Parse the request
134134
var req GenerateRootUpdateRequest
135-
if _, err := parseRequest(core.PerfStandby(), r, w, &req); err != nil {
135+
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
136136
respondError(w, http.StatusBadRequest, err)
137137
return
138138
}

http/sys_init.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func handleSysInitPut(core *vault.Core, w http.ResponseWriter, r *http.Request)
3939

4040
// Parse the request
4141
var req InitRequest
42-
if _, err := parseRequest(core.PerfStandby(), r, w, &req); err != nil {
42+
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
4343
respondError(w, http.StatusBadRequest, err)
4444
return
4545
}

http/sys_raft.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func handleSysRaftJoin(core *vault.Core) http.Handler {
2626
func handleSysRaftJoinPost(core *vault.Core, w http.ResponseWriter, r *http.Request) {
2727
// Parse the request
2828
var req JoinRequest
29-
if _, err := parseRequest(core.PerfStandby(), r, w, &req); err != nil && err != io.EOF {
29+
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil && err != io.EOF {
3030
respondError(w, http.StatusBadRequest, err)
3131
return
3232
}

http/sys_rekey.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func handleSysRekeyInitGet(ctx context.Context, core *vault.Core, recovery bool,
108108
func handleSysRekeyInitPut(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) {
109109
// Parse the request
110110
var req RekeyRequest
111-
if _, err := parseRequest(core.PerfStandby(), r, w, &req); err != nil {
111+
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
112112
respondError(w, http.StatusBadRequest, err)
113113
return
114114
}
@@ -158,7 +158,7 @@ func handleSysRekeyUpdate(core *vault.Core, recovery bool) http.Handler {
158158

159159
// Parse the request
160160
var req RekeyUpdateRequest
161-
if _, err := parseRequest(core.PerfStandby(), r, w, &req); err != nil {
161+
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
162162
respondError(w, http.StatusBadRequest, err)
163163
return
164164
}
@@ -306,7 +306,7 @@ func handleSysRekeyVerifyDelete(ctx context.Context, core *vault.Core, recovery
306306
func handleSysRekeyVerifyPut(ctx context.Context, core *vault.Core, recovery bool, w http.ResponseWriter, r *http.Request) {
307307
// Parse the request
308308
var req RekeyVerificationUpdateRequest
309-
if _, err := parseRequest(core.PerfStandby(), r, w, &req); err != nil {
309+
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
310310
respondError(w, http.StatusBadRequest, err)
311311
return
312312
}

http/sys_seal.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func handleSysUnseal(core *vault.Core) http.Handler {
8686

8787
// Parse the request
8888
var req UnsealRequest
89-
if _, err := parseRequest(core.PerfStandby(), r, w, &req); err != nil {
89+
if _, err := parseJSONRequest(core.PerfStandby(), r, w, &req); err != nil {
9090
respondError(w, http.StatusBadRequest, err)
9191
return
9292
}

0 commit comments

Comments
 (0)