fix(classic): normalize config-profile payloads for the Classic API's non-spec XML handling (PI-827) - #310
Merged
Conversation
… non-spec XML handling (PI-827) The Classic API does not treat <payloads> content per XML 1.0: it entity-decodes CDATA content once (spec: zero) and text content twice (spec: once). Any profile whose plist contains an entity — which means any value with & or < in it — failed POST/PUT with an opaque HTTP 409 "Unable to update the database", including Jamf's own GET/backup XML piped back into apply. Fix (all in the classic generator templates): - normalizeClassicProfilePayloadsForSend funnels every input form into the one wire format the server stores correctly: recover the true plist (text-form input is entity-decoded once), guard "]]>", escape every "&" once, wrap in CDATA. Applied as the final body transform in create/update/apply for both config-profile resources — after UUID injection, which needs the true-form plist. - injectClassicFileFields now converts binary plists to XML and rewrites embedded CDATA sections as escaped character data (textually — howett.net/plist drops trailing whitespace in CDATA text, so no parse/re-serialise round trip). - replaceClassicProfilePayload entity-guards "]]>" as a backstop. - profileconvert.NormalizeXML added for the binary-plist conversion. Wire-verified against Jamf Pro 11.x through the platform gateway: all five reserved characters in every legal representation (raw, named entity, decimal and hex refs), literal entity text, embedded CDATA sections, and GET→apply round trips — byte-exact storage through create, update, and apply. The server's only remaining transforms are its own canonicalization (entities for & < >, literals for " ', key sorting, edge-whitespace trim), which no client can influence. Full analysis: docs/solutions/conventions/ classic-profile-payload-ampersand-escaping-2026-07-30.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
neilmartin83
enabled auto-merge
July 30, 2026 11:53
neilmartin83
disabled auto-merge
July 30, 2026 12:26
neilmartin83
marked this pull request as draft
July 30, 2026 12:26
…rect the PI-827 model Follow-up to the initial escaping fix, driven by live probes across two tenants that refined the server model: - The 409 on raw content is a validation pass (fires for any payload type when one decode yields a bare & or <), so escape-once stays the wire form. Storage is per-payload-type: MCX custom settings and notification settings fragments are decoded once (escape stores them byte-exact); every other payload type — TCC, direct loginwindow, all mobile device payloads — is stored verbatim, keeping an extra entity layer. Values with & or < in those types cannot round-trip via any client. A raw-first/retry-on-409 design was implemented and reverted in this branch once probes showed raw never succeeds for entity-bearing profiles. - verifyClassicProfileStored: after every successful profile write, GET the stored payload and semantically compare against the submitted plist (profileconvert.DiffPayloadValues — masks server-rewritten Payload* metadata, tolerates edge-whitespace trim). Server-side corruption now surfaces as a stderr warning naming the exact paths instead of passing silently. - CMS-signed mobileconfigs are detected and their inner plist extracted (smallstep/pkcs7) with a note — the signature cannot survive this API. Binary plists are converted to XML. Live-verified: MCX reserved-character matrix byte-exact with zero warnings (osx create/update/apply, backup round-trip); mobile matrix and signed Login Window profiles create with deterministic warnings naming the affected values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
neilmartin83
marked this pull request as ready for review
July 30, 2026 12:47
This was referenced Jul 30, 2026
grahampugh
approved these changes
Jul 30, 2026
neilmartin83
enabled auto-merge
July 30, 2026 13:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
pro classic-macos-config-profiles apply --mobileconfig-file X.mobileconfigfailed with an opaque HTTP 409 "Unable to update the database" whenever the mobileconfig contained any XML entity in a string value. Since plists must encode&and<, any profile with&or<in a value could not be uploaded — Setup Manager profiles, Santa CEL rules, CIS content, URLs with query strings. Backup round-trips (GET XML piped back intoapply) failed identically. Signed and binary mobileconfigs produced malformed requests.The server model (every clause wire-verified against two Jamf Pro 11.x tenants)
<payloads>content once and 409s when the result contains a bare&or<. Spec-correct raw CDATA is therefore rejected for any plist containing&/<— escape-once is the only submittable wire form for such profiles. (A raw-first/retry-on-409 design was implemented and reverted in this branch's history once probes proved this.)com.apple.ManagedClient.preferencescustom settings including dict keys,com.apple.notificationsettings) are entity-decoded once — the escape stores them byte-exact. Every other payload type (TCC, directcom.apple.loginwindow, all mobile device payloads) is stored verbatim, keeping an extra entity layer — values with&/<there cannot be stored faithfully by any client, matching PI-827's history that Jamf's own exports can't be re-uploaded intact.Fix (generator templates +
internal/profileconvert)normalizeClassicProfilePayloadsForSend— final body transform before every profile send: recover the true plist (text-form/backup input decoded once), guard]]>, escape every&once, wrap in a single CDATA section.verifyClassicProfileStored— after every successful write, GET the stored payload and semantically compare against the submitted plist (profileconvert.DiffPayloadValues: parse-based, masks server-rewrittenPayload*metadata, tolerates the server's edge-whitespace trim). Server-side corruption surfaces as a stderr warning naming the exact dotted paths (e.g.PayloadContent[3].LoginwindowText) instead of passing silently.injectClassicFileFields): CMS-signed mobileconfigs auto-extract their inner plist (smallstep/pkcs7) with a note — the signature can't survive this API anyway; binary plists convert to XML; embedded CDATA sections are rewritten textually (stripCDATASections— not a plist re-serialise, which drops trailing whitespace in CDATA text).Verification
Live on both tenants: all five reserved characters × every legal representation (raw/named entity/decimal/hex refs), literal entity text, entity-bearing dict keys, embedded CDATA sections, CEL expressions, signed profiles, binary plists, GET→apply round-trips. MCX-family content stores byte-exact with zero warnings through create/update/apply; the server-unfixable combinations (mobile + typed macOS payloads with
&/<in values) create with deterministic warnings naming the affected values. On-device verification of decoded values via System Settings.Full analysis:
docs/solutions/conventions/classic-profile-payload-ampersand-escaping-2026-07-30.md🤖 Generated with Claude Code