CVE-2021-21296: Other (NVD-CWE-Other)
value: Fleet is an open source osquery manager. In Fleet before version 3.7.0 a malicious actor with a valid node key can send a badly formatted request that causes the Fleet server to exit, resulting in denial of service. This is possible only while a live query is currently ongoing. We believe the impact of this vulnerability to be low given the requirement that the actor has a valid node key. There is no information disclosure, privilege escalation, or code execution. The issue is fixed in Fleet 3.7.0.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 2.7
Published: 2021-02-10T20:15Z
Repository: https://github.com/fleetdm/fleet
Language: Go
Files changed: server/service/endpoint_campaigns.go, server/service/service_campaigns.go
Commit date: 2021-02-03 08:47:43 -0800
Commit message: Merge pull request from GHSA-xwh8-9p3f-3x45

- Fix the specific case that caused panic.
- Add panic handler around entire live query results handler. This will
  prevent similar issues from causing crashes in the future.

Note that other endpoints already have panic handling but this one is
special due to the use of websockets.
Diff: diff --git a/server/service/endpoint_campaigns.go b/server/service/endpoint_campaigns.go
index 2a6bfc7f..2c376c79 100644
--- a/server/service/endpoint_campaigns.go
+++ b/server/service/endpoint_campaigns.go
@@ -3,16 +3,16 @@ package service
 import (
 	"context"
 	"encoding/json"
 	"net/http"
 
-	"github.com/go-kit/kit/endpoint"
-	kitlog "github.com/go-kit/kit/log"
-	"github.com/igm/sockjs-go/v3/sockjs"
 	"github.com/fleetdm/fleet/server/contexts/viewer"
 	"github.com/fleetdm/fleet/server/kolide"
 	"github.com/fleetdm/fleet/server/websocket"
+	"github.com/go-kit/kit/endpoint"
+	kitlog "github.com/go-kit/kit/log"
+	"github.com/igm/sockjs-go/v3/sockjs"
 )
 
 ////////////////////////////////////////////////////////////////////////////////
 // Create Distributed Query Campaign
 ////////////////////////////////////////////////////////////////////////////////
@@ -77,13 +77,18 @@ func makeCreateDistributedQueryCampaignByNamesEndpoint(svc kolide.Service) endpo
 func makeStreamDistributedQueryCampaignResultsHandler(svc kolide.Service, jwtKey string, logger kitlog.Logger) http.Handler {
 	opt := sockjs.DefaultOptions
 	opt.Websocket = true
 	opt.RawWebsocket = true
 	return sockjs.NewHandler("/api/v1/kolide/results", opt, func(session sockjs.Session) {
-		defer session.Close(0, "none")
-
 		conn := &websocket.Conn{Session: session}
+		defer func() {
+			if p := recover(); p != nil {
+				logger.Log("err", p, "msg", "panic in result handler")
+				conn.WriteJSONError("panic in result handler")
+			}
+			session.Close(0, "none")
+		}()
 
 		// Receive the auth bearer token
 		token, err := conn.ReadAuthToken()
 		if err != nil {
 			logger.Log("err", err, "msg", "failed to read auth token")
diff --git a/server/service/service_campaigns.go b/server/service/service_campaigns.go
index 45d2b829..89a5ad42 100644
--- a/server/service/service_campaigns.go
+++ b/server/service/service_campaigns.go
@@ -161,15 +161,22 @@ func (svc service) StreamCampaignResults(ctx context.Context, conn *websocket.Co
 	}
 	lastStatus := status
 	lastTotals := targetTotals{}
 
 	// to improve performance of the frontend rendering the results table, we
-	// add the "host_hostname" field to every row.
-	mapHostnameRows := func(hostname string, rows []map[string]string) {
-		for _, row := range rows {
-			row["host_hostname"] = hostname
+	// add the "host_hostname" field to every row and clean null rows.
+	mapHostnameRows := func(res *kolide.DistributedQueryResult) {
+		filteredRows := []map[string]string{}
+		for _, row := range res.Rows {
+			if row == nil {
+				continue
+			}
+			row["host_hostname"] = res.Host.HostName
+			filteredRows = append(filteredRows, row)
 		}
+
+		res.Rows = filteredRows
 	}
 
 	hostIDs, labelIDs, err := svc.ds.DistributedQueryCampaignTargetIDs(campaign.ID)
 	if err != nil {
 		conn.WriteJSONError("error retrieving campaign targets: " + err.Error())
@@ -228,11 +235,11 @@ func (svc service) StreamCampaignResults(ctx context.Context, conn *websocket.Co
 		select {
 		case res := <-readChan:
 			// Receive a result and push it over the websocket
 			switch res := res.(type) {
 			case kolide.DistributedQueryResult:
-				mapHostnameRows(res.Host.HostName, res.Rows)
+				mapHostnameRows(&res)
 				err = conn.WriteJSONMessage("result", res)
 				if errors.Cause(err) == sockjs.ErrSessionNotOpen {
 					// return and stop sending the query if the session was closed
 					// by the client
 					return

Vulnerable code: 	"github.com/go-kit/kit/endpoint"
	kitlog "github.com/go-kit/kit/log"
	"github.com/igm/sockjs-go/v3/sockjs"
		defer session.Close(0, "none")

	// add the "host_hostname" field to every row.
	mapHostnameRows := func(hostname string, rows []map[string]string) {
		for _, row := range rows {
			row["host_hostname"] = hostname
				mapHostnameRows(res.Host.HostName, res.Rows)
Fixed code: 	"github.com/go-kit/kit/endpoint"
	kitlog "github.com/go-kit/kit/log"
	"github.com/igm/sockjs-go/v3/sockjs"
		defer func() {
			if p := recover(); p != nil {
				logger.Log("err", p, "msg", "panic in result handler")
				conn.WriteJSONError("panic in result handler")
			}
			session.Close(0, "none")
		}()
	// add the "host_hostname" field to every row and clean null rows.
	mapHostnameRows := func(res *kolide.DistributedQueryResult) {
		filteredRows := []map[string]string{}
		for _, row := range res.Rows {
			if row == nil {
				continue
			}
			row["host_hostname"] = res.Host.HostName
			filteredRows = append(filteredRows, row)

		res.Rows = filteredRows
				mapHostnameRows(&res)