From 0cac01a73972e4c70b62625a4a63b074a3ee8a2f Mon Sep 17 00:00:00 2001 From: bruce-riley <96066700+bruce-riley@users.noreply.github.com> Date: Fri, 13 Oct 2023 17:04:43 -0500 Subject: [PATCH] CCQ: Server should check api key first (#3443) * CCQ: Server should check api key first * Add integration tests --- node/cmd/ccq/http.go | 35 ++++++++++++++------- sdk/js-query/src/query/ethCall.test.ts | 42 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/node/cmd/ccq/http.go b/node/cmd/ccq/http.go index d3387add7..5ef19cfe2 100644 --- a/node/cmd/ccq/http.go +++ b/node/cmd/ccq/http.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "sort" + "strings" "time" "github.com/certusone/wormhole/node/pkg/common" @@ -18,6 +19,8 @@ import ( "google.golang.org/protobuf/proto" ) +const MAX_BODY_SIZE = 5 * 1024 * 1024 + type queryRequest struct { Bytes string `json:"bytes"` Signature string `json:"signature"` @@ -43,25 +46,35 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { // Set CORS headers for the preflight request if r.Method == http.MethodOptions { - w.Header().Set("Access-Control-Allow-Methods", "PUT, POST") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Api-Key") w.Header().Set("Access-Control-Max-Age", "3600") w.WriteHeader(http.StatusNoContent) return } - var q queryRequest - err := json.NewDecoder(r.Body).Decode(&q) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + + // There should be one and only one API key in the header. + apiKeys, exists := r.Header["X-Api-Key"] + if !exists || len(apiKeys) != 1 { + s.logger.Debug("received a request with the wrong number of api keys", zap.Stringer("url", r.URL), zap.Int("numApiKeys", len(apiKeys))) + http.Error(w, "api key is missing", http.StatusUnauthorized) + return + } + apiKey := apiKeys[0] + + // Make sure the user is authorized before we go any farther. + _, exists = s.permissions[strings.ToLower(apiKey)] + if !exists { + s.logger.Debug("invalid api key", zap.String("apiKey", apiKey)) + http.Error(w, "invalid api key", http.StatusForbidden) return } - // There should be one and only one API key in the header. - apiKey, exists := r.Header["X-Api-Key"] - if !exists || len(apiKey) != 1 { - s.logger.Debug("received a request without an api key", zap.Stringer("url", r.URL), zap.Error(err)) - http.Error(w, "api key is missing", http.StatusUnauthorized) + var q queryRequest + err := json.NewDecoder(http.MaxBytesReader(w, r.Body, MAX_BODY_SIZE)).Decode(&q) + if err != nil { + s.logger.Debug("failed to decode body", zap.Error(err)) + http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -84,7 +97,7 @@ func (s *httpServer) handleQuery(w http.ResponseWriter, r *http.Request) { Signature: signature, } - if status, err := validateRequest(s.logger, s.env, s.permissions, s.signerKey, apiKey[0], signedQueryRequest); err != nil { + if status, err := validateRequest(s.logger, s.env, s.permissions, s.signerKey, apiKey, signedQueryRequest); err != nil { // Don't need to log here because the details were logged in the function. http.Error(w, err.Error(), status) return diff --git a/sdk/js-query/src/query/ethCall.test.ts b/sdk/js-query/src/query/ethCall.test.ts index cc39590b4..eeb26c311 100644 --- a/sdk/js-query/src/query/ethCall.test.ts +++ b/sdk/js-query/src/query/ethCall.test.ts @@ -277,4 +277,46 @@ describe("eth call", () => { const response = await axios.get(HEALTH_URL); expect(response.status).toBe(200); }); + test("valid api key but payload too large should fail based on size", async () => { + const serialized = new Uint8Array(6000000); // Buffer should be larger than MAX_BODY_SIZE in node/cmd/ccq/http.go. + const signature = ""; + let err = false; + await axios + .put( + QUERY_URL, + { + signature, + // bytes: Buffer.alloc(6000000).toString("hex"), + bytes: Buffer.from(serialized).toString("hex"), + }, + { headers: { "X-API-Key": "my_secret_key" } } + ) + .catch(function (error) { + err = true; + expect(error.response.status).toBe(400); + expect(error.response.data).toBe(`http: request body too large\n`); + }); + expect(err).toBe(true); + }); + test("invalid api key with payload too large should fail based on api key", async () => { + const serialized = new Uint8Array(6000000); // Buffer should be larger than MAX_BODY_SIZE in node/cmd/ccq/http.go. + const signature = ""; + let err = false; + await axios + .put( + QUERY_URL, + { + signature, + // bytes: Buffer.alloc(6000000).toString("hex"), + bytes: Buffer.from(serialized).toString("hex"), + }, + { headers: { "X-API-Key": "some_junk" } } + ) + .catch(function (error) { + err = true; + expect(error.response.status).toBe(403); + expect(error.response.data).toBe(`invalid api key\n`); + }); + expect(err).toBe(true); + }); });