From 4609bca8cf5ed772f34d2b7c9fe1ef5625c2abb5 Mon Sep 17 00:00:00 2001 From: Magnus Jensen Date: Thu, 16 Oct 2025 14:06:05 -0300 Subject: [PATCH] AP: Validate top-level keys in android profile upload (#34360) **Related issue:** Resolves #34334 # Checklist for submitter ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually --- server/fleet/android.go | 38 ++++++++++++++++++++++++++++++++++++ server/fleet/android_test.go | 15 ++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 server/fleet/android_test.go diff --git a/server/fleet/android.go b/server/fleet/android.go index 54a0363331..b949a1aa2a 100644 --- a/server/fleet/android.go +++ b/server/fleet/android.go @@ -6,6 +6,9 @@ import ( "encoding/json" "errors" "fmt" + "reflect" + "strings" + "sync" "time" "github.com/fleetdm/fleet/v4/server/mdm" @@ -71,6 +74,10 @@ func (m *MDMAndroidConfigProfile) ValidateUserProvided() error { if errMsg, ok := AndroidForbiddenJSONKeys[key]; ok { return errors.New(errMsg) } + + if !IsAndroidPolicyFieldValid(key) { + return fmt.Errorf("Invalid JSON payload. Unknown key %q", key) + } } return nil @@ -134,3 +141,34 @@ type AndroidPolicyRequestPayload struct { type AndroidPolicyRequestPayloadMetadata struct { SettingsOrigin map[string]string `json:"settings_origin"` // Map of policy setting name, to profile uuid. } + +var ( + policyFieldsCache map[string]bool + policyFieldsOnce sync.Once +) + +// Initialize the cache once, lazily, with only JSON tag names. +// Since we take in the JSON value. +func initPolicyFieldsCache() { + policyFieldsCache = make(map[string]bool) + policyType := reflect.TypeOf(androidmanagement.Policy{}) + + for i := 0; i < policyType.NumField(); i++ { + field := policyType.Field(i) + + // Add JSON tag name if it exists + jsonTag := field.Tag.Get("json") + if jsonTag != "" { + tagName := strings.Split(jsonTag, ",")[0] + if tagName != "" && tagName != "-" { + policyFieldsCache[tagName] = true + } + } + } +} + +// Fast lookup using cached field names +func IsAndroidPolicyFieldValid(fieldName string) bool { + policyFieldsOnce.Do(initPolicyFieldsCache) + return policyFieldsCache[fieldName] +} diff --git a/server/fleet/android_test.go b/server/fleet/android_test.go new file mode 100644 index 0000000000..cbdc66ca2b --- /dev/null +++ b/server/fleet/android_test.go @@ -0,0 +1,15 @@ +package fleet + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsAndroidPolicyFieldValid(t *testing.T) { + isValid := IsAndroidPolicyFieldValid("bogusKeyThatWillNeverExist") + require.False(t, isValid) + + isValid = IsAndroidPolicyFieldValid("name") // "name" is a valid top-level policy field, that we assume will exist forever + require.True(t, isValid) +}