SQL Injection in TCC: and why it (probably) wasn't a security risk (this time)

Prologue

This article is a disclosure of a bug that was patched in macOS Sequoia 15, released today. Despite the unpatched bug not posing a security risk, I would still recommend upgrading. Sequoia comes with many other security patches that are indeed security-critical. Also, the bug described in this article was not patched as part of the releases of macOS Sonoma 14.7 and macOS Ventura 13.7 that were also released today.

Additionally, everything in this article is my own work, unless otherwise cited. While I would like to thank the team at Apple Product Security for their prompt response and help with this bug (as well as their public recognition), this article is written entirely independent of their input. I did not solicit their input for this article, and they were not even made aware of it prior to publication. Anyway, with that out of the way, onto the article!

Introduction

Prior to macOS Sequoia 15, there existed an SQL injection vulnerability in TCC. If you're not a security researcher that probably didn't make sense to you. If you are one, especially if you know what TCC is (or just if you know what SQL injection is), this statement is probably very concerning to you. However, in this article, I will show how the actual vulnerability did not pose a security risk. Whether this was due to sheer luck or intelligently-written code is something I'll leave up to the reader. But first, what is TCC?

Some Definitions

TCC

TCC is the permissions system built into macOS (as well as iOS). If you have ever seen a prompt from an app on macOS asking for permission to access your camera, documents, or other resources, that prompt was probably actually coming from TCC (specifically the TCC daemon: tccd). The TCC daemon is a separate system process that manages all the permissions given to apps on your device (although it prefers to use the term "service" internally instead of "permission").

TCC stands for Transparency, Consent, and Control. Probably. That's how many security researchers refer to it. However, I have never seen Apple themselves confirm this. Even in their Security Bounty program documentation, they refer to it as TCC. They seem to have never used "transparency, consent and control" to refer to the system nominally (at least, in any official capacity). They have, however, used the phrase.

In the help article Controlling app access to files in macOS, Apple states (emphasis mine):

Apple believes that users should have full transparency, consent, and control over what apps are doing with their data.

This is likely where the phrase originated. For brevity, I will continue to refer to the system as TCC throughout this article.

SQL

If you already know what SQL and SQL injection is, you can skip to this part. SQL, or Structured Query Language is a domain-specific language for managing data. A domain-specific language is a programming language that is designed to work within a specific subject area (i.e. a specific domain). SQL is widely popular and used across many database-management systems (or DBMS's).

Syntax

SQL syntax is mostly made up of verb-based statements, along with other keywords, with an SQL query being a collection of SQL statements. It is often surprisingly human-readable. For example, if this article were stored in a database, the query to get it would likely be something like this (note that a semicolon indicates the end of a statement):

      
SELECT * FROM articles WHERE id='1';
      
    

An SQL database is a collection of tables with individual rows, with each table having one or more columns. The above query "selects" all (*) columns in the table named articles from rows where the column named id has a value of 1. There's a lot more to SQL, but this general overview should hopefully be enough to understand the rest of this article.

SQL Injection

SQL has a problem that's fundamental to its status as a domain-specific language: without proper safeguards, it's prone to SQL injection. Specifically, given that SQL is domain-specific, it must integrate with more general-purpose languages when developing an application. SQL can't build a UI, but a developer might want to use it to control aspects of that interface. To do so, they must integrate SQL with the language used to build the interface. And this is where problems can occur.

To take the above article example, let's say this article was hosted on a URL where one could change the ID number used in the query from the URL:

      
https://www.example.com/article?id=[x]
      
    

You've probably seen this pattern before. While it may seem harmless at first, opening up the query to user input like this could allow end users to execute their own queries on the underlying database. If all the developer did was write code to copy the value of the id URL parameter directly into the SQL query, it would put too much control in the hands of users of the site.

URL parameters are not limited to numbers. If the developer used the simple copy method mentioned above, a user accessing this URL

      
https://www.example.com/article?id=1'
      
    

would result in this query:

      
SELECT * FROM articles WHERE id='1'';
      
    

As there is a mismatched number of quotes, this would actually be a syntax error in SQL. However, if a user were to instead write additional SQL statements in a way that would still be syntactically correct when copied into the original query, those SQL statements would potentially be executed on the database.

This leads to the general rule to never blindly trust user input when developing applications. This rule applies even beyond SQL. Regardless, the current solution in SQL to avoid SQL injection is a concept known as prepared statements. Prepared statements are SQL statements that are specially made so that any user-provided parts are not read as anything other than data.

How TCC uses SQL

TCC uses a library called SQLite to manage its databases. Put simply, SQLite allows for a single file to contain an entire SQL database. This is in contrast to how most other SQL systems work, where the data itself is held in a much more complex format. Using files as databases greatly simplifies the process of data management.

TCC uses at least three different databases, stored at the following locations:

The third one is specific to each user (i.e. each user has their own TCC.db). The TCC.db databases contain information about the permissions granted or denied to each app on a system-wide or per-user basis (respectively). You might think it's dangerous to have such a file out in the open in the user's files, but thankfully it's very protected. The directories containing these files aren't even readable under most circumstances. There are ways around these restrictions, but they often require certain TCC permissions, basically creating a catch-22.

The REG.db database contains a list of known TCC.db files. This appears to be a response to complex attacks where a hacker plants a fake TCC.db file and attempts to get TCC to use it. If a TCC.db database is not listed in REG.db, TCC will refuse to use it and will likely even delete it. Note that I am skimming over details here that aren't entirely important, but this is the basic gist. Regardless, it was the REG.db database (or, more specifically, a query into that database) that was vulnerable to SQL injection.

The Vulnerability

Frameworks

macOS provides many collections of code in the form of frameworks. These frameworks can be linked into existing code and provide additional functionality. In addition to the public frameworks located in the /System/Library/Frameworks/ directory, there are many private frameworks located in the /System/Library/PrivateFrameworks/ directory. While these frameworks are not meant to be used directly, there is nothing in the development toolchain that stops them from being used.

The private framework located at /System/Library/PrivateFrameworks/TCC.framework includes many functions for interacting with the TCC daemon. Behind the scenes, these functions send XPC messages to tccd. XPC is a public API, so these functions (and the framework itself) aren't necessarily needed, but it definitely doesn't hurt. Anyway, it was one of these functions that was vulnerable to SQL injection.

The Vulnerable Function

While I won't name it directly here, the function is easily findable in the private TCC framework. What this function did, specifically, was take a user-provided string and see if it matches the path of any known databases in the REG.db database. This was the query the handler code in tccd used:

      
SELECT * FROM registry WHERE abs_path = '%s';
      
    

The %s is replaced with the user-provided string through the use of stringWithFormat:. This allowed any user to input additional SQL code into the query. This would have been potentially devastating. While the query was connected to the least security-critical database in TCC, SQLite does include an ATTACH DATABASE statement that could have attached an additional database (perhaps one of the TCC.db ones) and allowed queries to be executed on it. However, the way SQLite was used in this case made it impossible to execute anything more than the initial SELECT statement.

Why This Wasn't a Security Risk

SQLite provides two methods of executing SQL statements:

The vulnerable function in this case used the latter method. You might think that, because it's a prepared statement, it's immune to SQL injection. However, I glossed over some details earlier. In order for user-provided values to never be treated as code, they must be bound to the prepared statement with the sqlite3_bind_*() family of functions. They cannot simply be put directly into the statement as SQLite would not be able to know which part of the query is user-provided. This bind step (which is also often used in other SQL systems) was missed in this case.

While this did allow for SQL injection, given that a sqlite3_prepare*() function was used, only the SELECT statement is ever executable. These functions were meant to work with only single statements, so any additional statements after the first semicolon are ignored. Thus, all a malicious actor could do is modify the SELECT statement. Given that SELECT statements are generally non-mutating (they don't change data), and that REG.db database is the least security-critical database in TCC, I concluded that this did not pose a security risk to users. Apple seems to have agreed, as they thanked me in their security bulletin for macOS Sequoia 15 under the "Additional recognition" section and not with the lists of specific vulnerabilities.

Conclusion

Summary

In short, this vulnerability was an SQL injection vulnerability…

That's two points in its favor and one against. Two out of three isn't that bad of a score. It's not a great score either, though.

Was This Luck?

This brings us back to the question I posed at the beginning of this article. Was it luck or intelligently-written code that made this not a security risk? While this might be a false dichotomy (and at the risk of tipping the scale a bit), it is my personal belief is that the lack of a specific security risk here did include a not-insignificant amount of luck.

SQLite does include some functions that can be included as part of SELECT statements. While most of these functions are trivial, it's theoretical there could be some combination of them that could lead to a dangerous SELECT statement. However, I was unable to find such a combination in my research. SQLite, for all intents and purposes, appears to be a very secure library. There seems to be only a handful of recent vulnerabilities, many of them not even in the core library itself. However, it's still not good that such injection was possible in TCC.

Additionally, if the sqlite3_prepare*() family of functions did not operate on single statements at a time, this would have allowed for multiple statements to be injected. If that were possible, it could have potentially lead to a full compromise of TCC. The fact that the sqlite3_prepare*() functions operate on single statements is an implicit guarantee, not an explicit one. While it is a very understandable way the functions would work, it's rarely a good idea to operate based on implicit guarantees.

How Did Apple Fix It?

Apple now use the following query:

        
  SELECT * FROM registry WHERE abs_path = ?;
        
      

They also removed the use of stringWithFormat: to replace the %s in the old query, and instead used a call to sqlite3_bind_text() to bind the user-provided string to the ? in the new query. This new method uses the prepared statements feature of SQLite properly and avoids any potential SQL injection.

Epilogue

Thank you to everyone who took the time to read my first article. It really means a lot to me and I hope to write many more in the future. To keep up to date with the happenings here, watch this space.