Files
George KarrandCopilot Autofix powered by AI e0edd46448 Windows MDM: accept discovery RequestVersion >= 4.0 (#49329) (#49380)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #49329

## What & why

Fresh **Windows 11 25H2** (build 10.0.26200) devices failed Microsoft
Entra / Autopilot MDM auto-enrollment during OOBE with error
**80180006**. The device advertises an MS-MDE2 discovery
`RequestVersion` of `"9.0"`, and Fleet's `IsValidDiscoveryMsg()`
rejected it via an exact-match allow-list (`{"4.0","5.0","6.0","7.0"}`)
that could only be changed by rebuilding the server.

The discovery **response** already pins the protocol to
`EnrollmentVersionV4` (`"4.0"`) and the client negotiates down, so the
exact-match check was the only blocker. This PR implements the issue's
preferred fix: **accept any `RequestVersion >= 4.0`**, which is
forward-compatible with future Windows version bumps.

- `server/mdm/microsoft/syncml/syncml.go`: replaced the
`SupportedEnrollmentVersions` allow-list var with a
`MinSupportedEnrollmentVersion` constant.
- `server/fleet/microsoft_mdm.go`: added `enrollmentVersionAtLeast`
(numeric component-wise compare, so `"10.0" > "9.0"`) and changed
discovery validation to accept any version at or above the minimum.

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- [ ] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [x] Confirmed that the fix is not expected to adversely impact load
test results


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Fixed MDM enrollment failures on fresh Windows 11 25H2 and other
recent builds.
* Discovery requests now accept supported MDE2 `RequestVersion` values
at or above the minimum supported version (instead of requiring an exact
match).
* Invalid or outdated discovery versions now return more specific
validation errors.
* **Tests**
* Added unit test coverage for minimum, equal, newer, and invalid
enrollment version comparisons.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-15 17:42:22 -07:00

369 lines
14 KiB
Go

package syncml
import (
"bytes"
"fmt"
)
// XML Namespaces and type URLs used by the Microsoft Device Enrollment v2 protocol (MS-MDE2)
const (
DiscoverNS = "http://schemas.microsoft.com/windows/management/2012/01/enrollment"
PolicyNS = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy"
EnrollWSTrust = "http://docs.oasis-open.org/ws-sx/ws-trust/200512"
EnrollSecExt = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
EnrollTType = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken"
EnrollPDoc = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc"
EnrollEncode = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary"
EnrollReq = "http://schemas.microsoft.com/windows/pki/2009/01/enrollment"
EnrollNSS = "http://www.w3.org/2003/05/soap-envelope"
EnrollNSA = "http://www.w3.org/2005/08/addressing"
EnrollXSI = "http://www.w3.org/2001/XMLSchema-instance"
EnrollXSD = "http://www.w3.org/2001/XMLSchema"
EnrollXSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
ActionNsDiag = "http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics"
ActionNsDiscovery = "http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/DiscoverResponse"
ActionNsPolicy = "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPoliciesResponse"
ActionNsEnroll = EnrollReq + "/RSTRC/wstep"
EnrollReqTypePKCS10 = EnrollReq + "#PKCS10"
EnrollReqTypePKCS7 = EnrollReq + "#PKCS7"
BinarySecurityDeviceEnroll = "http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentUserToken"
BinarySecurityAzureEnroll = "urn:ietf:params:oauth:token-type:jwt"
)
// Soap Error constants
// Details here: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/0a78f419-5fd7-4ddb-bc76-1c0f7e11da23
const (
// Message format is bad
SoapErrorMessageFormat = "s:messageformat"
// User not recognized
SoapErrorAuthentication = "s:authentication"
// User not allowed to enroll
SoapErrorAuthorization = "s:authorization"
// Failed to get certificate
SoapErrorCertificateRequest = "s:certificaterequest"
// Generic failure from management server, such as a database access error
SoapErrorEnrollmentServer = "s:enrollmentserver"
// The server hit an unexpected issue
SoapErrorInternalServiceFault = "s:internalservicefault"
// Cannot parse the security header
SoapErrorInvalidSecurity = "a:invalidsecurity"
)
// MS-MDM Status Code constants
// Details here: https://learn.microsoft.com/en-us/windows/client-management/oma-dm-protocol-support
const (
// The SyncML command completed successfully
CmdStatusOK = "200"
// Accepted for processing
// This code denotes an asynchronous operation, such as a request to run a remote execution of an application
CmdStatusAcceptedForProcessing = "202"
// Authentication accepted
// Normally you'll only see this code in response to the SyncHdr element (used for authentication in the OMA-DM standard)
// You may see this code if you look at OMA DM logs, but CSPs don't typically generate this code.
CmdStatusAuthenticationAccepted = "212"
// Operation canceled
// The SyncML command completed successfully, but no more commands will be processed within the session.
CmdStatusOperationCancelled = "214"
// Not executed
// A command wasn't executed as a result of user interaction to cancel the command.
CmdStatusNotExecuted = "215"
// Atomic roll back OK
// A command was inside an Atomic element and Atomic failed, thhis command was rolled back successfully
CmdStatusAtomicRollbackAccepted = "216"
// Bad request. The requested command couldn't be performed because of malformed syntax.
// CSPs don't usually generate this error, however you might see it if your SyncML is malformed.
CmdStatusBadRequest = "400"
// Invalid credentials
// The requested command failed because the requestor must provide proper authentication. CSPs don't usually generate this error
CmdStatusInvalidCredentials = "401"
// Forbidden
// The requested command failed, but the recipient understood the requested command
CmdStatusForbidden = "403"
// Not found
// The requested target wasn't found. This code will be generated if you query a node that doesn't exist
CmdStatusNotFound = "404"
// Command not allowed
// This respond code will be generated if you try to write to a read-only node
CmdStatusNotAllowed = "405"
// Optional feature not supported
// This response code will be generated if you try to access a property that the CSP doesn't support
CmdStatusOptionalFeature = "406"
// This response code will be generated if authentication is required to continue the session
CmdStatusAuthenticationRequired = "407"
// Unsupported type or format
// This response code can result from XML parsing or formatting errors
CmdStatusUnsupportedType = "415"
// Already exists
// This response code occurs if you attempt to add a node that already exists
CmdStatusAlreadyExists = "418"
// Permission Denied
// The requested command failed because the sender doesn't have adequate access control permissions (ACL) on the recipient.
// An "Access denied" errors usually get translated to this response code.
CmdStatusPermissionDenied = "425"
// Command failed. Generic failure.
// The recipient encountered an unexpected condition, which prevented it from fulfilling the request
// This response code will occur when the SyncML DPU can't map the originating error code
CmdStatusCommandFailed = "500"
// Atomic failed
// One of the operations in an Atomic block failed
CmdStatusAtomicFailed = "507"
// Atomic roll back failed
// An Atomic operation failed and the command wasn't rolled back successfully.
CmdStatusAtomicRollbackFailed = "516"
)
// MS-MDM Supported Alerts
// Details on MS-MDM 2.2.7.2: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mdm/72c6ea01-121c-48f9-85da-a26bb12aad51
const (
// SERVER-INITIATED MGMT
// Server-initiated device management session
CmdAlertServerInitiatedManagement = "1200"
// CLIENT-INITIATED MGMT
// Client-initiated device management session
CmdAlertClientInitiatedManagement = "1201"
// NEXT MESSAGE
// Request for the next message of a large object package
CmdAlertNextMessage = "1222"
// SESSION ABORT
// Informs recipient that the sender wishes to abort the DM session
CmdAlertSessionAbort = "1223"
// CLIENT EVENT
// Informs server that an event has occurred on the client
CmdAlertClientEvent = "1224"
// NO END OF DATA
// End of Data for chunked object not received.
CmdAlertNoEndOfData = "1225"
// GENERIC ALERT
// Generic client generated alert with or without a reference to a Management
CmdAlertGeneric = "1226"
)
const (
FleetBitLockerTargetLocURI = "/Vendor/MSFT/BitLocker"
FleetOSUpdateTargetLocURI = "/Vendor/MSFT/Policy/Config/Update"
FleetRemoteWipeTargetLocURI = "/Vendor/MSFT/RemoteWipe"
DiskEncryptionProfileRestrictionErrMsg = "Couldn't add. The configuration profile can't include BitLocker settings."
)
// MinSupportedEnrollmentVersion is the lowest MS-MDE2 discovery RequestVersion Fleet accepts.
//
// The discovery response pins the protocol to EnrollmentVersionV4 ("4.0") and the client
// negotiates down from whatever version it advertised, so Fleet accepts any RequestVersion >= 4.0
// rather than an exact-match allow-list. This keeps enrollment working on newer Windows builds
// that advertise higher versions (e.g. Windows 11 25H2 sends "9.0") without requiring a code
// change for each Windows release.
const MinSupportedEnrollmentVersion = EnrollmentVersionV4
// MS-MDE2 Message constants
const (
// Default MS-MDE2 enrollment protocol version
EnrollmentVersionV4 = "4.0"
// xsi:nil indicates value is not present
DefaultStateXSI = "true"
// Supported authentication types
AuthOnPremise = "OnPremise"
// SOAP Fault codes
SoapFaultRecv = "s:receiver"
// SOAP Fault default error locale
SoapFaultLocale = "en-us"
// HTTP Content Type for SOAP responses
SoapContentType = "application/soap+xml; charset=utf-8"
// HTTP Content Type for SyncML MDM responses
SyncMLContentType = "application/vnd.syncml.dm+xml"
// HTTP Content Type for Webcontainer responses
WebContainerContentType = "text/html; charset=UTF-8"
// Minimal Key Length for SHA1WithRSA encryption
PolicyMinKeyLength = "2048"
// Certificate Validity Period in seconds (365 days)
PolicyCertValidityPeriodInSecs = "31536000"
// Certificate Renewal Period in seconds (180 days)
PolicyCertRenewalPeriodInSecs = "15552000"
// Supported Enroll types gathered from MS-MDE2 Spec Section 2.2.9.3
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mde2/f7553554-b6e1-4a0d-abd6-6a2534503af7
// Supported Enroll Type Device
ReqSecTokenEnrollTypeDevice = "Device"
// Supported Enroll Type Full
ReqSecTokenEnrollTypeFull = "Full"
// Provisioning Doc Certificate Renewal Period (365 days)
WstepCertRenewalPeriodInDays = "365"
// Provisioning Doc Server supports ROBO auto certificate renewal
// TODO: Add renewal support
WstepROBOSupport = "true"
// Provisioning Doc Server retry interval
WstepRenewRetryInterval = "4"
// The PROVIDER-ID paramer specifies the server identifier for a management server used in the current management session
DocProvisioningAppProviderID = "Fleet"
// DMClientPollIntervalLocURI is the DMClient Poll node Fleet writes to tune the Windows MDM poll cadence (the aggressive default vs the
// relaxed steady-state for hosts that can be woken on demand by fleetd).
DMClientPollIntervalLocURI = "./Device/Vendor/MSFT/DMClient/Provider/" + DocProvisioningAppProviderID + "/Poll/IntervalForFirstSetOfRetries"
// The NAME parameter is used in the APPLICATION characteristic to specify a user readable application identity
DocProvisioningAppName = DocProvisioningAppProviderID
// The CONNRETRYFREQ parameter is used in the APPLICATION characteristic to specify a user readable application identity
DocProvisioningAppConnRetryFreq = "6"
// The INITIALBACKOFFTIME parameter is used to specify the initial wait time in milliseconds when the DM client retries for the first time
DocProvisioningAppInitialBackoffTime = "30000"
// The MAXBACKOFFTIME parameter is used to specify the maximum number of milliseconds to sleep after package-sending failure
DocProvisioningAppMaxBackoffTime = "120000"
// The DocProvisioningVersion attributes defines the version of the provisioning document format
DocProvisioningVersion = "1.1"
// It specifies whether the DM client should send out a request pending alert in case the device response to a DM request is too slow.
DmClientCSPEnableOmaDmKeepAliveMessage = "true"
// CSR issuer should be verified during enrollment
EnrollVerifyIssue = true
// Int type used by the DM client configuration
DmClientIntType = "integer"
// Bool type used by the DM client configuration
DmClientBoolType = "boolean"
// Additional Context items present on the RequestSecurityToken token message
ReqSecTokenContextItemUXInitiated = "UXInitiated"
ReqSecTokenContextItemHWDevID = "HWDevID"
ReqSecTokenContextItemLocale = "Locale"
ReqSecTokenContextItemTargetedUserLoggedIn = "TargetedUserLoggedIn"
ReqSecTokenContextItemOSEdition = "OSEdition"
ReqSecTokenContextItemDeviceName = "DeviceName"
ReqSecTokenContextItemDeviceID = "DeviceID"
ReqSecTokenContextItemEnrollmentType = "EnrollmentType"
ReqSecTokenContextItemDeviceType = "DeviceType"
ReqSecTokenContextItemOSVersion = "OSVersion"
ReqSecTokenContextItemApplicationVersion = "ApplicationVersion"
ReqSecTokenContextItemNotInOobe = "NotInOobe"
ReqSecTokenContextItemRequestVersion = "RequestVersion"
// redirect_uri query param expected by TOS endpoint
TOCRedirectURI = "redirect_uri"
// client-request-id query param expected by TOS endpoint
TOCReqID = "client-request-id"
// Alert payload user-driven unenrollment request
AlertUserUnenrollmentRequest = "com.microsoft:mdm.unenrollment.userrequest"
// FleetdWindowsInstallerGUID is the GUID used for fleetd on Windows
FleetdWindowsInstallerGUID = "./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/%7BA427C0AA-E2D5-40DF-ACE8-0D726A6BE096%7D/DownloadInstall"
AuthMD5 = "syncml:auth-md5"
AuthB64Format = "b64"
)
// MS-MDM Message constants
const (
// SyncML Message Content Type
SyncMLMsgContentType = "application/vnd.syncml.dm+xml"
// SyncML Message Meta Namespace
SyncMLMetaNamespace = "syncml:metinf"
// SyncML Cmd Namespace
SyncCmdNamespace = "SYNCML:SYNCML1.2"
// SyncML Message Header Name
SyncMLHdrName = "SyncHdr"
// Supported SyncML version
SyncMLSupportedVersion = "1.2"
// SyncML ver protocol version
SyncMLVerProto = "DM/" + SyncMLSupportedVersion
)
type TestCommand struct {
Verb string
LocURI string
Data string
}
// Wraps the commands in an <Atomic> element for testing purposes
func ForTestWithData(commands []TestCommand) []byte {
var syncMLBuf bytes.Buffer
for _, command := range commands {
syncMLBuf.WriteString(fmt.Sprintf(`
<%s>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
<Data>%s</Data>
</Item>
</%s>`, command.Verb, command.LocURI, command.Data, command.Verb))
}
return fmt.Appendf([]byte{}, "<Atomic>%s</Atomic>", syncMLBuf.Bytes())
}
func ForTestWithDataNonAtomic(commands []TestCommand) []byte {
var syncMLBuf bytes.Buffer
for _, command := range commands {
syncMLBuf.WriteString(fmt.Sprintf(`
<%s>
<Item>
<Target>
<LocURI>%s</LocURI>
</Target>
<Data>%s</Data>
</Item>
</%s>`, command.Verb, command.LocURI, command.Data, command.Verb))
}
return syncMLBuf.Bytes()
}