Root cause
Authentication & access control introduced
Add one-click allow from deny events; introduce apple team ID sentinel
AI-assisted change: Claude
How AI contributed: Direct introductionPublished Apr 21, 2026 in
craigjbass/clearancekitAll 3 findings in this repository
macOS policy-bypass (CWE-863): ClearanceKit's opfilter Endpoint Security filter treats any process with an EMPTY Team ID but non-empty Signing ID as an Apple platform binary. An unprivileged attacker can ad-hoc sign a binary with an Apple-baseline-allowlisted signing identifier (codesign -f -s - -i 'com.apple.mds') and read any FAA-protected path, because the forged process matches platformBinary/Apple entries in the global allowlist and FAA policy rules. Advisory states the correct signal is es_process_t.is_platform_binary.
Root cause
Add one-click allow from deny events; introduce apple team ID sentinel
AI-assisted change: Claude
Fix
Use is_platform_binary to identify Apple platform processes
AI-assisted fix: Claude Code · Craig Bass
Case facts
@@ -7,6 +7,13 @@ import Foundation +// MARK: - Constants++/// Sentinel team ID used to represent Apple platform binaries.+/// Apple's own binaries carry an empty team ID in the ES audit token;+/// this identifier lets policy rules reference them explicitly.+public let appleTeamID = "apple"+ // MARK: - PolicyDecision public enum PolicyDecision {@@ -99,11 +106,13 @@ public func checkFAAPolicy( } // teamID and signingID are AND: both specified constraints must be satisfied.- let teamOK = rule.allowedTeamIDs.isEmpty || rule.allowedTeamIDs.contains(teamID)+ // Apple platform binaries carry an empty team ID; resolve to appleTeamID for matching.+ let resolvedTeamID = teamID.isEmpty ? appleTeamID : teamID+ let teamOK = rule.allowedTeamIDs.isEmpty || rule.allowedTeamIDs.contains(resolvedTeamID) let signingOK = rule.allowedSigningIDs.isEmpty || rule.allowedSigningIDs.contains(signingID) if (!rule.allowedTeamIDs.isEmpty || !rule.allowedSigningIDs.isEmpty) && teamOK && signingOK { var parts: [String] = []- if !rule.allowedTeamIDs.isEmpty { parts.append("team ID \(teamID)") }+ if !rule.allowedTeamIDs.isEmpty { parts.append("team ID \(resolvedTeamID)") } if !rule.allowedSigningIDs.isEmpty { parts.append("signing ID \(signingID)") } return .allowed(matchedCriterion: parts.joined(separator: " and ")) }@@ -115,11 +124,12 @@ public func checkFAAPolicy( // Ancestor teamID and signingID are also AND across both constraints. for ancestor in ancestors {- let aTeamOK = rule.allowedAncestorTeamIDs.isEmpty || rule.allowedAncestorTeamIDs.contains(ancestor.teamID)+ let resolvedAncestorTeamID = ancestor.teamID.isEmpty ? appleTeamID : ancestor.teamID+ let aTeamOK = rule.allowedAncestorTeamIDs.isEmpty || rule.allowedAncestorTeamIDs.contains(resolvedAncestorTeamID) let aSigningOK = rule.allowedAncestorSigningIDs.isEmpty || rule.allowedAncestorSigningIDs.contains(ancestor.signingID) if (!rule.allowedAncestorTeamIDs.isEmpty || !rule.allowedAncestorSigningIDs.isEmpty) && aTeamOK && aSigningOK { var parts: [String] = []- if !rule.allowedAncestorTeamIDs.isEmpty { parts.append("ancestor team ID \(ancestor.teamID)") }+ if !rule.allowedAncestorTeamIDs.isEmpty { parts.append("ancestor team ID \(resolvedAncestorTeamID)") } if !rule.allowedAncestorSigningIDs.isEmpty { parts.append("ancestor signing ID \(ancestor.signingID)") } parts.append("(\(ancestor.path))") return .allowed(matchedCriterion: parts.joined(separator: " and "))@@ -58,6 +58,58 @@ final class PolicyStore: ObservableObject { save() } + /// Adds the process's team ID and signing ID to the allowedTeamIDs / allowedSigningIDs+ /// of the most specific rule whose prefix matches `path`.+ func allowProcess(teamID: String, signingID: String, inRuleMatching path: String) {+ guard let index = bestMatchingRuleIndex(for: path) else { return }+ let existing = rules[index]+ let effectiveTeamID = teamID.isEmpty ? appleTeamID : teamID+ var newTeamIDs = existing.allowedTeamIDs+ var newSigningIDs = existing.allowedSigningIDs+ if !newTeamIDs.contains(effectiveTeamID) { newTeamIDs.append(effectiveTeamID) }+ if !signingID.isEmpty && !newSigningIDs.contains(signingID) { newSigningIDs.append(signingID) }+ rules[index] = FAARule(+ id: existing.id,+ protectedPathPrefix: existing.protectedPathPrefix,+ allowedProcessPaths: existing.allowedProcessPaths,+ allowedTeamIDs: newTeamIDs,+ allowedSigningIDs: newSigningIDs,+ allowedAncestorProcessPaths: existing.allowedAncestorProcessPaths,+ allowedAncestorTeamIDs: existing.allowedAncestorTeamIDs,+ allowedAncestorSigningIDs: existing.allowedAncestorSigningIDs+ )+ save()+ }++ /// Adds the ancestor's team ID and signing ID to the allowedAncestorTeamIDs /+ /// allowedAncestorSigningIDs of the most specific rule whose prefix matches `path`.+ func allowAncestor(teamID: String, signingID: String, inRuleMatching path: String) {+ guard let index = bestMatchingRuleIndex(for: path) else { return }+ let existing = rules[index]+ let effectiveTeamID = teamID.isEmpty ? appleTeamID : teamID+ var newTeamIDs = existing.allowedAncestorTeamIDs+ var newSigningIDs = existing.allowedAncestorSigningIDs+ if !newTeamIDs.contains(effectiveTeamID) { newTeamIDs.append(effectiveTeamID) }+ if !signingID.isEmpty && !newSigningIDs.contains(signingID) { newSigningIDs.append(signingID) }+ rules[index] = FAARule(+ id: existing.id,+ protectedPathPrefix: existing.protectedPathPrefix,+ allowedProcessPaths: existing.allowedProcessPaths,+ allowedTeamIDs: existing.allowedTeamIDs,+ allowedSigningIDs: existing.allowedSigningIDs,+ allowedAncestorProcessPaths: existing.allowedAncestorProcessPaths,+ allowedAncestorTeamIDs: newTeamIDs,+ allowedAncestorSigningIDs: newSigningIDs+ )+ save()+ }++ private func bestMatchingRuleIndex(for path: String) -> Int? {+ rules.indices+ .filter { path.hasPrefix(rules[$0].protectedPathPrefix) }+ .max(by: { rules[$0].protectedPathPrefix.count < rules[$1].protectedPathPrefix.count })+ }+ private func save() { guard let data = try? JSONEncoder().encode(rules) else { return } try? data.write(to: storageURL, options: .atomic)@@ -92,6 +92,7 @@ struct EventsWindowView: View { struct EventRow: View { let event: FolderOpenEvent+ @State private var allowedItems: Set<String> = [] private var formattedTime: String { let formatter = DateFormatter()@@ -118,66 +119,121 @@ struct EventRow: View { .foregroundColor(.secondary) } - HStack {- Text("PID: \(event.processID)")+ processSection++ if !event.decisionReason.isEmpty {+ Text(event.decisionReason) .font(.caption)- .foregroundColor(.secondary)- if !event.processPath.isEmpty {- Text(event.processPath)- .font(.caption)- .foregroundColor(.secondary)- .lineLimit(1)- }+ .foregroundColor(event.accessAllowed ? .secondary : Color.red.opacity(0.8))+ .fixedSize(horizontal: false, vertical: true) }- if !event.teamID.isEmpty || !event.signingID.isEmpty {++ if !event.ancestors.isEmpty {+ ancestorsSection+ }+ }+ .padding(.vertical, 4)+ .padding(.horizontal, 4)+ .background(+ RoundedRectangle(cornerRadius: 4)+ .fill(event.accessAllowed ? Color.green.opacity(0.05) : Color.red.opacity(0.1))+ )+ }++ @ViewBuilder+ private var processSection: some View {+ HStack(alignment: .top, spacing: 4) {+ VStack(alignment: .leading, spacing: 2) { HStack {- if !event.teamID.isEmpty {- Text("Team: \(event.teamID)")+ Text("PID: \(event.processID)")+ .font(.caption)+ .foregroundColor(.secondary)+ if !event.processPath.isEmpty {+ Text(event.processPath) .font(.caption) .foregroundColor(.secondary)+ .lineLimit(1) }+ }+ HStack {+ Text("Team: \(event.teamID.isEmpty ? "Apple" : event.teamID)")+ .font(.caption)+ .foregroundColor(.secondary) if !event.signingID.isEmpty { Text("Signing: \(event.signingID)") .font(.caption) .foregroundColor(.secondary) } } }-- if !event.decisionReason.isEmpty {- Text(event.decisionReason)- .font(.caption)- .foregroundColor(event.accessAllowed ? .secondary : Color.red.opacity(0.8))- .fixedSize(horizontal: false, vertical: true)+ if !event.accessAllowed {+ Spacer()+ allowButton(itemKey: "process") {+ PolicyStore.shared.allowProcess(+ teamID: event.teamID,+ signingID: event.signingID,+ inRuleMatching: event.path+ )+ } }+ }+ } @@ -41,7 +41,7 @@ func processRecord(from esProcess: UnsafeMutablePointer<es_process_t>) -> Proces } else { rawTeamID = "" }- let teamID = rawTeamID.isEmpty && !signingID.isEmpty ? "apple" : rawTeamID+ let teamID = process.is_platform_binary ? "apple" : rawTeamID let uid = uid_t(process.audit_token.val.1) let gid = gid_t(process.audit_token.val.2)@@ -118,14 +118,15 @@ final class ESJailAdapter { let childKey = ProcessKey(child.audit_token) let signingID = ESInboundAdapter.string(from: child.signing_id) let teamID = ESInboundAdapter.string(from: child.team_id)+ let childIsPlatformBinary = child.is_platform_binary esJailAdapterQueue.async { if let parentRuleID = jailedProcessesLock.withLock({ $0[parentKey] }) { jailedProcessesLock.withLock { $0[childKey] = parentRuleID } return } let rules = rulesLock.withLock { $0 } guard !rules.isEmpty else { return }- let resolvedTeamID = teamID.isEmpty ? appleTeamID : teamID+ let resolvedTeamID = childIsPlatformBinary ? appleTeamID : teamID guard let rule = rules.first(where: { $0.jailedSignature.matches(resolvedTeamID: resolvedTeamID, signingID: signingID) }) else { return } jailedProcessesLock.withLock { $0[childKey] = rule.id } }@@ -137,6 +138,7 @@ final class ESJailAdapter { let parentKey = ProcessKey(message.pointee.process.pointee.parent_audit_token) let signingID = ESInboundAdapter.string(from: target.signing_id) let teamID = ESInboundAdapter.string(from: target.team_id)+ let targetIsPlatformBinary = target.is_platform_binary esJailAdapterQueue.async { let inherited = jailedProcessesLock.withLock { map -> Bool in if let ruleID = map[oldKey] {@@ -153,7 +155,7 @@ final class ESJailAdapter { guard !inherited else { return } let rules = rulesLock.withLock { $0 } guard !rules.isEmpty else { return }- let resolvedTeamID = teamID.isEmpty ? appleTeamID : teamID+ let resolvedTeamID = targetIsPlatformBinary ? appleTeamID : teamID if let rule = rules.first(where: { $0.jailedSignature.matches(resolvedTeamID: resolvedTeamID, signingID: signingID) }) { jailedProcessesLock.withLock { $0[newKey] = rule.id } }@@ -265,7 +265,7 @@ final class ESInboundAdapter { ) let signingID = string(from: process.signing_id) let rawTeamID = string(from: process.team_id)- let teamID = rawTeamID.isEmpty && !signingID.isEmpty ? "apple" : rawTeamID+ let teamID = process.is_platform_binary ? "apple" : rawTeamID return FileAuthEvent( correlationID: correlationID, operation: operation,AI-assisted change 4acba331520094bd4f5de1cd42ad2b3e838d2833a5902e8ae8a07cc9c15f0cd4 · Fix 28f4ae44634ee47df81cd3151d211bfadad33eb2448529ad546c72eed8d05860