Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions server/datastore/mysql/android_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2699,7 +2699,7 @@ func testAddDeleteAndroidAppWithConfiguration(t *testing.T, ds *Datastore) {

test.CreateInsertGlobalVPPToken(t, ds)

testConfig := json.RawMessage(`{"ManagedConfiguration": {"DisableShareScreen": true, "DisableComputerAudio": true}}`)
testConfig := []byte(`{"ManagedConfiguration": {"DisableShareScreen": true, "DisableComputerAudio": true}}`)
// Create android and VPP apps
app1, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "android1", BundleIdentifier: "android1",
Expand Down Expand Up @@ -2738,7 +2738,7 @@ func testAddDeleteAndroidAppWithConfiguration(t *testing.T, ds *Datastore) {
require.NotZero(t, meta2.VPPAppsTeamsID)

// Edit android app
newConfig := json.RawMessage(`{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED"}`)
newConfig := []byte(`{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED"}`)
app1.VPPAppTeam.Configuration = newConfig
_, err = ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID)
require.NoError(t, err)
Expand All @@ -2750,7 +2750,7 @@ func testAddDeleteAndroidAppWithConfiguration(t *testing.T, ds *Datastore) {
require.Equal(t, newConfig, meta.Configuration)

// Add invalid configuration
badConfig := json.RawMessage(`"-": "-"`)
badConfig := []byte(`"-": "-"`)
app1.VPPAppTeam.Configuration = badConfig
_, err = ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID)
require.Error(t, err)
Expand Down
14 changes: 7 additions & 7 deletions server/datastore/mysql/vpp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2588,7 +2588,7 @@ func testAndroidAppConfigs(t *testing.T, ds *Datastore) {
require.NoError(t, err)

config1 := json.RawMessage(`{"workProfileWidgets":"WORK_PROFILE_WIDGETS_ALLOWED", "managedConfiguration": {"1":1}}`)
expectedConfig1 := json.RawMessage(`{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED", "managedConfiguration": {"1": 1}}`)
expectedConfig1 := []byte(`{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED", "managedConfiguration": {"1": 1}}`)

_, err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
{VPPAppID: app1.VPPAppID, SelfService: true, DisplayName: ptr.String("name 1")},
Expand All @@ -2611,9 +2611,9 @@ func testAndroidAppConfigs(t *testing.T, ds *Datastore) {
}
}

require.Equal(t, json.RawMessage(nil), assigned[app1.VPPAppID].Configuration)
require.Equal(t, json.RawMessage(nil), assigned[app2.VPPAppID].Configuration)
require.Equal(t, json.RawMessage(`{}`), assigned[app3.VPPAppID].Configuration)
require.Equal(t, []byte(nil), assigned[app1.VPPAppID].Configuration)
require.Equal(t, []byte(nil), assigned[app2.VPPAppID].Configuration)
require.Equal(t, []byte(`{}`), assigned[app3.VPPAppID].Configuration)
require.Equal(t, expectedConfig1, assigned[app4.VPPAppID].Configuration)

_, err = ds.SetTeamVPPApps(ctx, &team.ID, []fleet.VPPAppTeam{
Expand Down Expand Up @@ -2645,9 +2645,9 @@ func testAndroidAppConfigs(t *testing.T, ds *Datastore) {
}
}

require.Equal(t, json.RawMessage(nil), assigned[app1.VPPAppID].Configuration)
require.Equal(t, json.RawMessage(`{"managedConfiguration": 1}`), assigned[app2.VPPAppID].Configuration)
require.Equal(t, json.RawMessage(`{}`), assigned[app3.VPPAppID].Configuration)
require.Equal(t, []byte(nil), assigned[app1.VPPAppID].Configuration)
require.Equal(t, []byte(`{"managedConfiguration": 1}`), assigned[app2.VPPAppID].Configuration)
require.Equal(t, []byte(`{}`), assigned[app3.VPPAppID].Configuration)
require.Equal(t, expectedConfig1, assigned[app4.VPPAppID].Configuration)

// Delete all
Expand Down
107 changes: 96 additions & 11 deletions server/fleet/vpp.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package fleet

import (
"encoding/json"
"fmt"
"slices"
"time"

"github.com/fleetdm/fleet/v4/server/variables"
"howett.net/plist"
)

type VPPAppID struct {
Expand Down Expand Up @@ -56,12 +59,12 @@ type VPPAppTeam struct {
// app creation if AddAutoInstallPolicy is true.
AddedAutomaticInstallPolicy *Policy `json:"-"`
DisplayName *string `json:"display_name"`
// Configuration is a json file used to customize Android app
// behavior/settings. Applicable to Android apps only.
Configuration json.RawMessage `json:"configuration,omitempty"`
AutoUpdateEnabled *bool `json:"-"`
AutoUpdateStartTime *string `json:"-"`
AutoUpdateEndTime *string `json:"-"`
// Configuration is the managed app configuration payload. JSON for Android,
// XML for iOS / iPadOS.
Configuration []byte `json:"configuration,omitempty"`
AutoUpdateEnabled *bool `json:"-"`
AutoUpdateStartTime *string `json:"-"`
AutoUpdateEndTime *string `json:"-"`
Comment on lines +62 to +67
Comment on lines +62 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Find every place that reads/writes app_store_app.configuration in non-Go code (UI, gitops, terraform, docs).
fd -t f -e ts -e tsx -e js -e jsx -e py -e go -e md -e yml -e yaml | xargs rg -n --no-messages 'app_store_app[^a-zA-Z0-9_]*\.?configuration|"configuration"\s*:'  | rg -v '_test\.go|integration_.*_test\.go' | head -200

# 2) Confirm the request-side types are still json.RawMessage (asymmetry check).
rg -nP --type=go -C2 '\b(addAppStoreAppRequest|updateAppStoreAppRequest|VPPBatchPayload)\b' | rg -nP -C2 'Configuration'

# 3) See whether any place attaches a custom (Un)MarshalJSON to VPPAppStoreApp or VPPAppTeam to neutralize the wire-format change.
ast-grep --pattern $'func ($_ $_VPPAppStoreApp) MarshalJSON() ($_, $_) { $$$ }'
ast-grep --pattern $'func ($_ $_VPPAppTeam) MarshalJSON() ($_, $_) { $$$ }'
ast-grep --pattern $'func ($_ $_AppStoreAppUpdatePayload) MarshalJSON() ($_, $_) { $$$ }'

Repository: fleetdm/fleet

Length of output: 9918


🏁 Script executed:

# Get the current state of VPPAppStoreApp and related structures
sed -n '50,80p' server/fleet/vpp.go | cat -n

Repository: fleetdm/fleet

Length of output: 1535


🏁 Script executed:

# Get more context around the mentioned lines
sed -n '55,75p' server/fleet/vpp.go | cat -n

Repository: fleetdm/fleet

Length of output: 1028


🏁 Script executed:

# Search for the request-side type definitions
rg -n 'type (addAppStoreAppRequest|updateAppStoreAppRequest|VPPBatchPayload)' --type=go -A 15

Repository: fleetdm/fleet

Length of output: 6038


🏁 Script executed:

# Look for marshal implementations on the response types
rg -n 'func.*VPPAppStoreApp.*Marshal' --type=go

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Check the specific test line 419 mentioned in the comment
sed -n '410,430p' server/service/integration_android_software_test.go | cat -n

Repository: fleetdm/fleet

Length of output: 995


🏁 Script executed:

# Check the other affected lines mentioned (135-137, 196-205)
sed -n '130,210p' server/fleet/vpp.go | cat -n

Repository: fleetdm/fleet

Length of output: 4022


🏁 Script executed:

# Search for any related API documentation or changelog
fd -i -name '*.md' | xargs rg -l 'app.store|vpp|configuration' | head -10

Repository: fleetdm/fleet

Length of output: 623


🏁 Script executed:

# Check if there are any changelog or migration notes
find . -type f \( -name 'CHANGELOG*' -o -name 'changelog*' -o -name 'MIGRATION*' \) | head -5

Repository: fleetdm/fleet

Length of output: 214


🏁 Script executed:

# Look for TypeScript/JavaScript client code that might handle this
fd -type f \( -e ts -e tsx -e js -e jsx \) | xargs rg -l 'configuration' | grep -i client | head -10

Repository: fleetdm/fleet

Length of output: 589


🏁 Script executed:

# Look for recent changes to vpp.go to understand the intent
git log --oneline -20 -- server/fleet/vpp.go 2>/dev/null || echo "Git history not available"

Repository: fleetdm/fleet

Length of output: 95


🏁 Script executed:

# Search for any TODOs or FIXMEs related to Configuration
rg -n 'TODO|FIXME' server/fleet/vpp.go server/service/vpp.go

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Look for any frontend/TypeScript references to configuration in app store app context
rg -n 'appStoreApp|app_store_app' --type=ts --type=tsx --type=js --type=jsx | head -20

Repository: fleetdm/fleet

Length of output: 85


🏁 Script executed:

# Check if there are any API integration tests that validate the wire format
rg -n 'Configuration.*base64|base64.*Configuration' --type=go

Repository: fleetdm/fleet

Length of output: 513


🏁 Script executed:

# Look for JSON marshaling tests
rg -n 'json.Marshal.*Configuration|Configuration.*json.Marshal' --type=go | head -10

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Look at how Configuration is handled in responses - find the handler that returns it
rg -n 'getSoftwareTitle|software/titles' server/service/vpp.go -A 10 | head -40

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Check if there's any special handling when retrieving Configuration from DB
rg -n 'Configuration' server/datastore/mysql/vpp.go -B 2 -A 2 | head -50

Repository: fleetdm/fleet

Length of output: 1959


🏁 Script executed:

# Look for how the response is constructed
rg -n 'type.*Response.*struct' server/service/vpp.go -A 15 | grep -A 15 'getSoftwareTitleResponse\|getSoftwareTitle'

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Check how configuration is serialized when storing/retrieving
rg -n 'db:.*configuration|json:.*configuration' server/fleet/vpp.go --ignore-case

Repository: fleetdm/fleet

Length of output: 181


🏁 Script executed:

# Check git blame for when Configuration changed from json.RawMessage to []byte
git log -p --all -- server/fleet/vpp.go | grep -B 20 -A 5 'Configuration.*\[\]byte' | head -60

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Look for PR/commit messages mentioning this change
git log --all --grep='Configuration\|json.RawMessage\|\[\]byte' --oneline | head -20

Repository: fleetdm/fleet

Length of output: 1341


🏁 Script executed:

# Check if there's documentation about the wire format
fd -type f -name '*.md' | xargs rg -l 'software.*title\|app.*store' | head -5

Repository: fleetdm/fleet

Length of output: 228


🏁 Script executed:

# Search for any UI code consuming the configuration endpoint
find . -type f \( -name '*.ts' -o -name '*.js' \) -path '*/frontend/*' -o -path '*/web/*' | xargs rg -l 'configuration' 2>/dev/null | head -10

Repository: fleetdm/fleet

Length of output: 442


🏁 Script executed:

# Check the frontend software interface to see how configuration is handled
cat frontend/interfaces/software.ts | head -150

Repository: fleetdm/fleet

Length of output: 4564


🏁 Script executed:

# Look for any handling of app_store_app in the frontend
rg -n 'app_store_app\|appStoreApp' frontend/ | head -30

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Check if there's any base64 encoding/decoding in the frontend
rg -n 'btoa\|atob\|base64' frontend/services/entities/software.ts | head -20

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Look at the actual git commit that changed the type
git log --all -p -- server/fleet/vpp.go | grep -B 30 -A 5 'Configuration.*\[\]byte.*json' | head -80

Repository: fleetdm/fleet

Length of output: 4311


🏁 Script executed:

# Try to find when this change was introduced
git log --all --pretty=format:"%H %s" -- server/fleet/vpp.go | head -30

Repository: fleetdm/fleet

Length of output: 534


🏁 Script executed:

# Look at the specific commit that changed Configuration to []byte
git show 1da4c58d --stat

Repository: fleetdm/fleet

Length of output: 50370


🏁 Script executed:

# Get the full commit message
git show 1da4c58d --no-patch --format=fuller

Repository: fleetdm/fleet

Length of output: 317


🏁 Script executed:

# Check if there are any issues or PRs related to this
rg -n 'plist\|xml.*format\|base64' server/fleet/vpp.go server/service/vpp.go | head -20

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Look at the actual implementation of how Configuration is stored/retrieved
sed -n '100,150p' server/datastore/mysql/vpp.go | cat -n

Repository: fleetdm/fleet

Length of output: 1929


🏁 Script executed:

# Check if the test actually validates the wire format properly
sed -n '410,450p' server/service/integration_android_software_test.go | cat -n

Repository: fleetdm/fleet

Length of output: 2032


🏁 Script executed:

# Check what the commit actually changed - look at the diff for the relevant lines
git show 1da4c58d -- server/fleet/vpp.go | grep -A 5 -B 5 'Configuration'

Repository: fleetdm/fleet

Length of output: 2787


🏁 Script executed:

# Search for any issues or PRs referencing this change
rg -n 'base64|RawMessage.*byte|byte.*RawMessage' server/fleet/vpp.go server/service/vpp.go

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Check if there are tests that specifically validate JSON marshaling
rg -n 'json.Marshal.*Configuration\|MarshalJSON' server/service/ --type=go -B 2 -A 2 | head -40

Repository: fleetdm/fleet

Length of output: 39


🏁 Script executed:

# Look at the Android configuration storage to understand how it flows
rg -n 'GetAndroidAppConfiguration\|updateAndroidAppConfigurationTx' server/datastore/mysql/android* --type=go -A 10 | head -60

Repository: fleetdm/fleet

Length of output: 39


Confirm this wire format change is intentional and verify downstream consumers have been updated.

Configuration was changed from json.RawMessage to []byte in VPPAppTeam, VPPAppStoreApp, and AppStoreAppUpdatePayload response types. This introduces a breaking wire format change:

  • Requests still accept raw JSON via json.RawMessage in addAppStoreAppRequest, updateAppStoreAppRequest, and VPPBatchPayload
  • Responses now emit base64-encoded bytes instead of raw JSON/XML, since encoding/json base64-encodes []byte fields
  • No custom MarshalJSON/UnmarshalJSON implementations exist to preserve the previous wire shape

Go integration tests pass because Go's decoder accepts both formats transparently. However, non-Go consumers (UI, fleetctl/gitops, Terraform, REST clients) will receive base64-encoded configuration instead of the expected raw JSON/XML structure, requiring code changes.

Ensure this change is intentional and that API documentation, UI code, and any clients consuming the software-titles endpoints have been updated to handle the new base64-encoded format. If backward compatibility is required, implement custom marshal/unmarshal methods to preserve the original wire format.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/fleet/vpp.go` around lines 62 - 67, The Configuration field was
changed from json.RawMessage to []byte in VPPAppTeam, VPPAppStoreApp, and
AppStoreAppUpdatePayload which causes responses to be base64-encoded; either
revert Configuration back to json.RawMessage on those types or implement custom
MarshalJSON/UnmarshalJSON on each type (or a shared wrapper type) to emit/accept
raw JSON/XML to preserve the original wire shape; then ensure request types
addAppStoreAppRequest, updateAppStoreAppRequest, and VPPBatchPayload remain
compatible and update API docs and downstream consumers (UI, fleetctl/gitops,
Terraform, REST clients) to handle the chosen format.

}

func (v VPPAppTeam) GetPlatform() string {
Expand Down Expand Up @@ -129,9 +132,9 @@ type VPPAppStoreApp struct {
// "Browsers", etc.
Categories []string `json:"categories"`
DisplayName string `json:"display_name"`
// Configuration is a json file used to customize Android app
// behavior/settings. Applicable to Android apps only.
Configuration json.RawMessage `json:"configuration,omitempty"`
// Configuration is the managed app configuration payload. JSON for Android,
// XML for iOS / iPadOS.
Configuration []byte `json:"configuration,omitempty"`
Comment on lines 132 to +137
Comment thread
cdcme marked this conversation as resolved.
}

// VPPAppStatusSummary represents aggregated status metrics for a VPP app.
Expand Down Expand Up @@ -197,6 +200,88 @@ type AppStoreAppUpdatePayload struct {
LabelsIncludeAll []string
Categories []string
DisplayName *string
Configuration json.RawMessage
Configuration []byte
SoftwareAutoUpdateConfig
}

// FleetVarsSupportedInAppleAppConfig is the allow-list of Fleet variables that
// can appear in an iOS / iPadOS managed app configuration plist. Subset of the
// variables supported in Apple configuration profiles — credential variables
// (NDES, SCEP, DigiCert) don't fit the InstallApplication command shape.
var FleetVarsSupportedInAppleAppConfig = []FleetVarName{
FleetVarHostUUID,
FleetVarHostHardwareSerial,
FleetVarHostPlatform,
FleetVarHostEndUserEmailIDP,
FleetVarHostEndUserIDPUsername,
FleetVarHostEndUserIDPUsernameLocalPart,
FleetVarHostEndUserIDPGroups,
FleetVarHostEndUserIDPDepartment,
FleetVarHostEndUserIDPFullname,
}

// ValidateAppleAppConfiguration validates a managed app configuration payload
// for an iOS or iPadOS InstallApplication command. The payload must be an XML
// plist whose root element is a <dict>, and any Fleet variable tokens used in
// string values or keys must be drawn from FleetVarsSupportedInAppleAppConfig.
// Empty input is allowed — callers decide whether to store or clear.
func ValidateAppleAppConfiguration(config []byte) error {
if len(config) == 0 {
return nil
}

var root map[string]any
format, err := plist.Unmarshal(config, &root)
if err != nil {
return NewInvalidArgumentError("configuration", fmt.Sprintf("invalid plist: %s", err))
}
// Apple's MDM InstallApplication only accepts XML plist for the
// Configuration dict; reject binary, OpenStep and GNUStep formats up front
// so admins don't get surprised when devices reject the install.
if format != plist.XMLFormat {
return NewInvalidArgumentError("configuration", "configuration must be an XML plist")
}

// Raw-bytes scan catches structural bypasses (duplicate keys, trailing
// siblings) invisible to the decoded tree. The decoded-tree walk below
// catches XML-entity-encoded tokens the regex won't match.
for _, name := range variables.Find(string(config)) {
if !slices.Contains(FleetVarsSupportedInAppleAppConfig, FleetVarName(name)) {
return NewInvalidArgumentError("configuration", fmt.Sprintf("unsupported variable $FLEET_VAR_%s", name))
}
}
if name, ok := findUnsupportedFleetVar(root); ok {
return NewInvalidArgumentError("configuration", fmt.Sprintf("unsupported variable $FLEET_VAR_%s", name))
}
Comment thread
cdcme marked this conversation as resolved.
return nil
}

// findUnsupportedFleetVar walks v's keys and string values and returns the
// first $FLEET_VAR_* token that is not in FleetVarsSupportedInAppleAppConfig.
// The second return is false when every referenced variable is allowed.
func findUnsupportedFleetVar(v any) (string, bool) {
switch t := v.(type) {
case string:
for _, name := range variables.Find(t) {
if !slices.Contains(FleetVarsSupportedInAppleAppConfig, FleetVarName(name)) {
return name, true
}
}
case map[string]any:
for k, val := range t {
if name, ok := findUnsupportedFleetVar(k); ok {
return name, ok
}
if name, ok := findUnsupportedFleetVar(val); ok {
return name, ok
}
}
case []any:
for _, val := range t {
if name, ok := findUnsupportedFleetVar(val); ok {
return name, ok
}
}
}
return "", false
}
172 changes: 172 additions & 0 deletions server/fleet/vpp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package fleet

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestValidateAppleAppConfiguration(t *testing.T) {
const fragment = `<dict>
<key>ServerURL</key>
<string>https://example.com</string>
<key>EnableTelemetry</key>
<true/>
</dict>`

const fullDoc = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ServerURL</key>
<string>https://example.com</string>
</dict>
</plist>`

const nested = `<dict>
<key>Outer</key>
<dict>
<key>Inner</key>
<string>value</string>
</dict>
<key>List</key>
<array>
<dict>
<key>K</key>
<string>v</string>
</dict>
</array>
</dict>`

cases := []struct {
name string
input string
wantErr bool
errSub string
}{
{name: "empty", input: ""},
{name: "bare dict fragment", input: fragment},
{name: "full plist document", input: fullDoc},
{name: "nested dict and array of dicts", input: nested},
{name: "garbage non-XML", input: "not a plist", wantErr: true, errSub: "invalid plist"},
{name: "malformed XML unclosed tag", input: "<dict><key>foo</key><string>bar", wantErr: true, errSub: "invalid plist"},
{name: "root is array", input: `<array><string>x</string></array>`, wantErr: true, errSub: "invalid plist"},
{name: "root is string", input: `<string>oops</string>`, wantErr: true, errSub: "invalid plist"},
{
name: "allowed variable",
input: `<dict><key>HostID</key><string>$FLEET_VAR_HOST_UUID</string></dict>`,
},
{
name: "allowed variable with braces",
input: `<dict><key>HostID</key><string>${FLEET_VAR_HOST_UUID}</string></dict>`,
},
{
name: "multiple allowed variables in one string",
input: `<dict><key>K</key><string>https://x/$FLEET_VAR_HOST_UUID/$FLEET_VAR_HOST_HARDWARE_SERIAL</string></dict>`,
},
{
name: "credential variable not allowed in app config",
input: `<dict><key>K</key><string>$FLEET_VAR_NDES_SCEP_CHALLENGE</string></dict>`,
wantErr: true,
errSub: "unsupported variable $FLEET_VAR_NDES_SCEP_CHALLENGE",
},
{
name: "unknown variable name",
input: `<dict><key>K</key><string>$FLEET_VAR_BOGUS_NAME</string></dict>`,
wantErr: true,
errSub: "unsupported variable $FLEET_VAR_BOGUS_NAME",
},
{
name: "all standard plist value types accepted",
input: `<dict>
<key>S</key><string>val</string>
<key>I</key><integer>42</integer>
<key>R</key><real>3.14</real>
<key>T</key><true/>
<key>F</key><false/>
<key>D</key><data>YWJj</data>
<key>A</key><array><string>x</string><integer>1</integer></array>
</dict>`,
},
{
name: "ASCII control character in string value",
input: "<dict><key>K</key><string>x\x01y</string></dict>",
wantErr: true,
errSub: "invalid plist",
},
{
name: "json null token",
input: "null",
wantErr: true,
errSub: "invalid plist",
},
{
name: "hex-entity-encoded $ does not bypass disallow list",
input: `<dict><key>K</key><string>&#x24;FLEET_VAR_NDES_SCEP_CHALLENGE</string></dict>`,
wantErr: true,
errSub: "unsupported variable $FLEET_VAR_NDES_SCEP_CHALLENGE",
},
{
name: "decimal-entity-encoded $ does not bypass disallow list",
input: `<dict><key>K</key><string>&#36;FLEET_VAR_NDES_SCEP_CHALLENGE</string></dict>`,
wantErr: true,
errSub: "unsupported variable $FLEET_VAR_NDES_SCEP_CHALLENGE",
},
{
name: "disallowed variable inside a CDATA section is caught",
input: `<dict><key>K</key><string><![CDATA[$FLEET_VAR_NDES_SCEP_CHALLENGE]]></string></dict>`,
wantErr: true,
errSub: "unsupported variable $FLEET_VAR_NDES_SCEP_CHALLENGE",
},
{
name: "disallowed variable nested inside an array is caught",
input: `<dict><key>K</key><array><dict><key>Inner</key><string>$FLEET_VAR_BOGUS</string></dict></array></dict>`,
wantErr: true,
errSub: "unsupported variable $FLEET_VAR_BOGUS",
},
{
name: "disallowed variable used as a key is caught",
input: `<dict><key>$FLEET_VAR_BOGUS</key><string>x</string></dict>`,
wantErr: true,
errSub: "unsupported variable $FLEET_VAR_BOGUS",
},
{
name: "duplicate key bypass: disallowed var hidden by last-wins map semantics",
input: `<dict><key>K</key><string>$FLEET_VAR_NDES_SCEP_CHALLENGE</string><key>K</key><string>safe_value</string></dict>`,
wantErr: true,
errSub: "unsupported variable $FLEET_VAR_NDES_SCEP_CHALLENGE",
},
{
name: "trailing sibling bypass: disallowed var in element dropped by parser",
input: `<dict><key>K</key><string>safe</string></dict><dict><key>X</key><string>$FLEET_VAR_NDES_SCEP_CHALLENGE</string></dict>`,
wantErr: true,
errSub: "unsupported variable $FLEET_VAR_NDES_SCEP_CHALLENGE",
},
{
name: "openstep dict format rejected",
input: `{ServerURL = "https://x.com";}`,
wantErr: true,
errSub: "must be an XML plist",
},
{
name: "binary plist rejected",
input: "bplist00\xd1\x01\x02Q1Q2\x08\x0b\r\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f",
wantErr: true,
errSub: "must be an XML plist",
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := ValidateAppleAppConfiguration([]byte(c.input))
if c.wantErr {
require.Error(t, err)
require.Contains(t, err.Error(), c.errSub)
var iae *InvalidArgumentError
require.ErrorAs(t, err, &iae)
return
}
require.NoError(t, err)
})
}
}
4 changes: 2 additions & 2 deletions server/service/integration_android_software_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ func (s *integrationMDMTestSuite) TestBatchAndroidApps() {
TeamID: teamID,
}, http.StatusOK, &titleResp)
require.Equal(t, "app_1", *titleResp.SoftwareTitle.ApplicationID)
require.Equal(t, json.RawMessage(`{}`), titleResp.SoftwareTitle.AppStoreApp.Configuration)
require.Equal(t, []byte(`{}`), titleResp.SoftwareTitle.AppStoreApp.Configuration)

s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleApp2), &getSoftwareTitleRequest{
ID: titleApp2,
Expand Down Expand Up @@ -800,7 +800,7 @@ func (s *integrationMDMTestSuite) TestBatchAndroidApps() {
TeamID: teamID,
}, http.StatusOK, &titleResp)
require.Equal(t, "app_1", *titleResp.SoftwareTitle.ApplicationID)
require.Equal(t, json.RawMessage(`{}`), titleResp.SoftwareTitle.AppStoreApp.Configuration)
require.Equal(t, []byte(`{}`), titleResp.SoftwareTitle.AppStoreApp.Configuration)

s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleApp2), &getSoftwareTitleRequest{
ID: titleApp2,
Expand Down
Loading