Contents
- What is the danger?
- How are these dangers mitigated?
- Appendix: File system races in more detail
- Appendix: File writing APIs that cannot create new files
Creating a file is a pretty basic and conceptually simple task, that many applications do (whether they realise it or not – library code often does this too, at least for temporary files such as caches or for communicating between programs).
So you’d think it’d be trivial to do correctly. Alas, it is not.
☝️ This is a challenging topic, and I’ve done my best to research thoroughly and check everything experimentally. Still, it’s certainly possible I’ve made a mistake or overlooked something. Please let me know of any errors, in the comments at the bottom.
Also, it’s a dense topic, so I’ve tried to highlight (in bold) the most important points. In case of TL;DR. 🙂
What is the danger?
There are two key things to watch out for when creating files:
- Security flaws due to incorrect use of, or badly designed, file system APIs. This is especially a concern for privileged applications (e.g. setuid) or those that ever run with elevated privileges (e.g. via sudo or by admin users). Unintentional reuse of existing files can be (and has been) the cause of major security vulnerabilities. See the appendix for more details.
Another security concern is leaking sensitive information to other programs (or users, on a shared computer). This can easily happen if files are created in places other programs or users can access, such as shared folders. The files may be inadvertently created with inappropriately broad permissions (e.g. world-readable) or the parent folder’s permissions might permit others to change the permissions of the file after the fact even if they don’t own it (e.g. a world-writable folder without the sticky bit set). - Data loss risks due to inadvertent overwrites or modifications of existing files. If you’re not certain you know what file is already at a given path on disk, and that you should be allowed to overwrite it, then you should not. Usually, the user has to give explicit permission (e.g. Save dialogs that explicitly ask the user if they intend to overwrite an existing file).
This risk is greatest for persistent files, e.g. files in your Documents folder. Those are usually where the user stores their most important data.
For temporary files the level of danger is generally lower but not zero. If you’re using a system-designated temporaries folder, then in principle anything in there is unimportant anyway. However, randomly mucking with temporary files can still cause data corruption or loss, depending on how those files are used by applications (including your own). e.g. they might store autosaves of the current document in temporary files, and directly copy / move those files when the user formally saves. Thus, modifying the temporary file might end up modifying the user’s actual save file.
How are these dangers mitigated?
App Sandboxing helps
As annoying & limiting as App Sandboxing can be in other regards, it is one of the best single steps an application author can take to improve file security. For the most part, your application is the only (non-system & non-privileged1) application that can write within its sandbox, and you’ll usually be creating files only within your own sandbox.
Avoid /private/tmp (a.k.a. /tmp)
/tmp
is merely a symlink to /private/tmp
.
/private/tmp
is world-readable: every program on the computer can access its contents. Thankfully, it’s not as bad it first appears – /private/tmp
is special in that it has the “sticky bit” set, which imposes some key restrictions on what users may do to each other’s files (most importantly, that they can’t delete them even though /tmp
is world-writable).
Still, it’s better to use the tmp folder designated via FileManager.default.temporaryDirectory
or URL.temporaryDirectory
. Though the location and security properties of that folder varies:
- If App Sandboxing is in use, it’s an application-specific folder like
/Users/SadPanda/Library/Containers/com.sadpanda.MyApp/Data/tmp/
. No other (unprivileged) application can access that folder. - If App Sandboxing is not in use, it’s a user-specific folder like
/var/folders/v3/8anbad56df64adf3_35gj346jg13v19a/T/
(the exact path varies between user accounts and computers, by design for security). That folder is still insecure – it is accessible to all programs of the same user – but at least you don’t have to worry about other [unprivileged] users.
If used correctly, /private/tmp
has essentially the same properties as the user-specific temporary folders. Using it correctly starts with ensuring files are created with no group or ‘other’ privileges, but the full complexities are beyond the scope of this post.
☝️ /private/tmp
is not accessible when App Sandboxing is enabled.
Consider setting a restrictive umask
When you create a file on any Linux or Unix system, such as macOS, its permissions are set based on a combination of the specific API used and the process-wide umask
. Good APIs will require you to specify the initial privileges of the file, but some do not (e.g. fopen
) and instead use some arbitrary default, such as creating files as readable and writable by anyone (0666). That is bad – usually you don’t want your files readable by any other users.
While you should generally avoid APIs that don’t provide proper control over file permissions, you realistically may not even know if you’re using such APIs because they might be employed by library code you don’t control (including Apple’s).
While it’s possible that the parent folder(s) will protect a given file (by preventing access to their contents by other users), it’s safest to not assume that.
The umask can help mitigate the dangers of bad APIs by specifying which privileges are not to be granted by default2.
The umask defaults to denying write access to groups and other users (0022), which is a start but still not good – read access to private user data is still a concern.
However – beyond the overly permissive default setting – there at two problems with umask:
- It is a process-wide global. Modifications to it apply to all threads in your process, which makes it dangerous to modify. e.g. you might be creating a particularly sensitive file and need the umask to be 0177, so you set it to that, but before you actually get to execute the file creation another thread sets the umask to 0000 because it wants to create an otherwise unrelated file that’s world-writable.
So unfortunately the only safe way to use umask is to set it very early in process launch before any additional threads are created3.
You can try to enforce your own mutual exclusion around umask, e.g. with a global lock, but beware of 3rd party code (including Apple’s) that might modify umask without following your mutual exclusion protocol. Generally umask isn’t modified often, so this arguably is possible to achieve in practice, but proving that there are no missed calls toumask
can be practically impossible. - Lots of existing code, that you might be unwittingly using via libraries or frameworks, assumes the umask remains at its default. Thus, making it more restrictive might break things in ways & places that are difficult to foresee.
In general the more complicated your program, that more of a concern this is. e.g. most command-line programs can modify umask without causing issues, but GUI programs pull in a lot of framework code and functionality, some of which might implicitly rely on certain umask bits. Unfortunately the only way to find out is experimentally.
So umask is not a panacea. Still, setting a restrictive umask improves security if you can get away with it.
Correctly use the right file creation APIs
In short, creation of files needs to (generally) not extend nor replace existing files, and ensure correct initial file permissions.
There are quite a lot of APIs for creating a file in macOS. I’m going to enumerate only the most common ones provided by the system libraries & frameworks.
⚠️
open
Nominally this is the low-level API for opening (existing or new) files, although as you’ll see later it’s not actually the only one.
It has a flags parameter, which is how you tell it what to actually do, between opening existing files, creating new ones, etc. Two of the most important flags are O_CREAT
and O_EXCL
. O_CREAT
tells open
to create the file. If O_EXCL
is specified, open
will fail if a file already exists at the target location. If O_EXCL
is not specified, O_CREAT
is interperted as “create the file if necessary” – meaning, it will actually open the existing file if it exists, and not create a new file.
You should almost never use O_CREAT
without O_EXCL
. If you really do intend to overwrite the existing file, then it’s safer to remove the existing file first (e.g. with unlink
), and then create your new file (with O_EXCL
to ensure no other file appears at the target location in the interim). That way you ensure the new file has your expected location (not a symlink) and attributes (e.g. file permissions).
Note that O_EXCL
will fail if the target is a symlink, so you don’t need to specify O_NOFOLLOW
(although it doesn’t hurt).
open
requires you to specify the new file’s permissions if you use the O_CREAT
option (and respects the umask), which is good as it makes you think about what the permissions should be, and lets you set them to something suitable for each use case. These permissions are only applied to new files, so in a nutshell you cannot rely on them if you don’t use O_EXCL
.
For that reason also, you typically should not use O_TRUNC
. If you don’t need the contents of the existing file, delete the file first. “Reusing it” via O_TRUNC
also reuses its permissions and other attributes, which might not be set correctly for your intentions (e.g. a malicious program might have pre-created the file as world-readable, even though you intend it to be readable only by the current user and have otherwise done the right things such as set the umask to ensure that).
One reason you might validly use O_TRUNC
is if you anticipate there being multiple hard links to the file, and you want to modify the file as seen by the other links too. This is very rare. Be sure that’s necessary before you use O_TRUNC
, and consider putting validations in place to ensure the file you’ve just opened for reuse has the expected permissions and attributes (e.g. via fstat64
and similar APIs that operate on the file descriptor – do not use lstat
or any other path-based APIs).
⚠️ fopen
This is essentially just a wrapper atop open
, with the “w” and “a” flags mapping to O_CREAT
(essentially) and the “x” flag mapping to O_EXCL
. The same rules apply, so you should generally never use “w” without the “x” flag as well, nor “a” without “+”.
fopen
does not let you specify the permissions of the created file, instead defaulting to 0666 (readable & writable by everyone) which is a terrible default. It does respect umask, so by default it will create files as 0644 which is marginally better. But, as discussed previously, it is difficult to guarantee what the umask actually is at any particular point in time. So in general you should prefer open
instead of fopen
.
❌ OutputStream(toFileAtPath:append:)
& friends
This ultimately (when you call the open
method) calls open
with the flags O_WRONLY | O_CREAT
(plus O_TRUNC
if the append argument is false). As such it will always modify an existing file if present.
It also does not let you specify the permissions of the new file, instead defaulting arbitrarily to 0666 (but respecting umask, at least).
It is an unsafe API and should not be used.
⚠️ NSData.write(toFile:options:)
& friends
This family of methods all ultimately call open_dprotected_np
(implementation), a variant of open
specific to Apple platforms which adds Apple-specific functionality regarding file encryption and isolation (see e.g. the protection-related flags within NSData.WritingOptions
). It takes the same flags as the regular open
, and write(toFile:options:)
by default uses O_CREAT | O_TRUNC
. If you use the withoutOverwriting
option, it adds O_EXCL
. So you should usually use withoutOverwriting
.
If you use the atomic
flag, it creates the file using a private function _NSCreateTemporaryFile_Protected
which obtains a file path using mktemp
⚠️ and calls open_dprotected_np
with the flags O_CREAT | O_EXCL | O_RDWR
4. Once it has created & written to that temporary file, it uses rename
to move it into place. rename
just silently deletes any existing file at the destination path. So it’s safe against race attacks, but susceptible to data loss bugs from unintentionally overwriting existing files. As such, use atomic
only with caution.
⚠️ If you add withoutOverwriting
on top of atomic
, the call crashes your program! It throws an Objective-C exception – NSInvalidArgumentException
. Swift does not support Objective-C exceptions (it’s fundamentally unsafe to pass Objective-C exceptions up to Swift functions) so you have to use an Objective-C helper function as an intermediary.
This is a particularly unfortunate limitation – even aside from the crashiness – because using both options together is highly desirable and it should in principle work – the implementation can simply use renamex_np
instead with the flag RENAME_EXCL
.
FB13568491.
It also does not let you specify the permissions of the new file, instead defaulting arbitrarily to 0666 (but respecting umask, at least). For files containing sensitive data (such as private user data), this API should generally not be used.
❌ NSString.write(toFile:atomically:encoding:)
& friends
These are essentially just wrappers over NSData.write(toFile:options:)
& friends, where the atomically
argument maps to the atomic
option. They provide no way to use the withoutOverwriting
option, so they should not be used in most cases.
Use randomised names for transient files
Whenever the file name doesn’t actually matter – i.e. it’s not chosen by the user and isn’t pre-defined by some system requirement – it’s best to use a randomised name. This serves two purposes:
- If your code has any bugs that allow it to erroneously overwrite existing files, using random names at least makes it a lot less likely that you’ll trigger those bugs, by greatly reducing the odds of using the same name twice.
- It makes it harder (if not impossible) for attackers to predict the file names, and therefore to attack them.
There are many ways to obtain a randomised name, but it’s wise to use one of the canonical methods detailed below.
☝️ If you do care about the file name, but not its location, you can use these APIs to create a randomly-named temporary folder, and then create your file within there.
This can be handy for e.g. preparing a file URL to be dragged from your app, where you want the file to have a proper name but don’t care (per se) where it lives.
⚠️ mktemp
mktemp
mktemp
is infamously a source of security vulnerabilities, because it doesn’t actually create the file (or folder) but merely returns a path. The caller is responsible for securely creating the file (or folder). This can be done safely – by following the guidance earlier in this post, in particular around the O_EXCL
open
flag – but it’s easy to screw up.
Generally it’s preferable to use mkstemp
/ mkdtemp
& friends. Unfortunately, if you want to use higher-level file APIs on Apple’s platforms that’s a problem because most of those APIs don’t support initialisation from a file descriptor, only a path (or equivalently, URL). So you may find that you still need to use mktemp
. If so, be very careful about how you actually create the files & folders.
✅ mkstemp
/ mkdtemp
& friends
These replacements for mktemp
actually create the file / folder (respectively), in a safe way.
mkstemp
creates the file and returns the corresponding open file descriptor, instead of merely returning a path and leaving it to the caller to get the creation step right. It will never overwrite (nor open) an existing file. It also sets the file’s initial permissions to 0600 (i.e. read-writable but only by the current user), which is a pretty safe default.
mkdtemp
still returns a path (not a file descriptor), like mktemp
, but it ensures the folder was actually created (and not previously existent) with the permissions set to 0700 (i.e. usable only by the current user).
Neither make any guarantees regarding security – or lack thereof – due to parent folder permissions. The caller still needs to ensure necessary security protections for those (whether by choosing a suitable system-provided folder, or manually checking permissions and symlinks in the path).
is a good starting point.URL.temporaryDirectory
Using these from Swift is a little awkward because they mutate their primary argument (the path template), but here’s an example. Once you have the file descriptor (in the mkstemp
case) you can wrap it in e.g. a FileHandle
and work with it at a slightly higher level.
⚠️
FileManager.default.url(for:in:appropriateFor:create:)
In its special mode where ‘for’ is .itemReplacementDirectory
and ‘in’ is .userDomainMask
, this behaves like mkdtemp
; it creates a randomly-named folder and returns the URL to it (note that it completely ignores the ‘create’ argument in this case – there is no way to have it not create the temporary folder).
This API seems to presume the use of App Sandboxing to mitigate its problems – and when App Sandboxing is enabled, it’s the best way to obtain a temporary file or folder path, though only if you use a suitable value for the ‘appropriateFor’ parameter, such as
5. When App Sandboxing is enabled, that will result in a path inside the sandbox, which is the most secure place an unprivileged application can use.URL.temporaryDirectory
However, if instead a URL is provided which points to a different volume, it returns a path to a user-specific temporary folder on that volume, e.g. /Volumes/Example/.TemporaryItems/folders.501/TemporaryItems/
. While that does exclude (unprivileged) other users, it’s still a big step down from a location inside the app’s sandbox.
Appendix: File system races in more detail
Generally-speaking, the file system is a shared resource. Multiple programs can access it simultaneously with no coordination required6 between them. That opens the door for races – where the state of the world changes in-between file system operations that a program might mistakenly assume are atomic.
In general, any single call to a low-level file system API – e.g. open
– is atomic. Most such APIs correspond to a single syscall into the kernel, and the operation inside the kernel is wrapped inside a lock (conceptually if not also literally).
Conversely, any operation that takes multiple calls to a file system API is never atomic.
A textbook security vulnerability arises when you do something like:
- Make up some random file name (e.g. with
mktemp
). - Check that it doesn’t exist (one syscall), and see that it doesn’t.
- Write to that file (a separate syscall).
A malicious program could inject its own file in-between steps two and three – or even more dangerously, a symlink – and cause your program to overwrite something (many file APIs will automatically follow symlinks and open existing files, if not used correctly as detailed in this post).
The classic concern in this regard is with privileged programs that have the ability to overwrite sensitive files, e.g. /etc/passwd
. Tricking them into doing so can cause major damage to the system (e.g. nobody can login anymore!) in the best case, and in the worst case – where the attacker can also influence the contents of the file, or those contents are conveniently just what the attacker wants – they might be able to implement a more subtle attack that doesn’t merely break the system but instead e.g. changes the root password, giving them superuser access to the computer.
Even for unprivileged applications, it can still be a concern. e.g. they might be tricked into writing a bunch of private user data into a shared location from where the attacker can exfiltrate it.
Appendix: File writing APIs that cannot create new files
These APIs are nominally irrelevant since they can’t be used to create new files, but it can be useful to know that fact, for use in converse scenarios where you do not want to create a file.
FileHandle(forWritingAtPath:)
…ultimately calls [[NSConcreteFileHandle alloc] initWithPath:… flags:0x1 createMode:0 error:nil]
, which turns the path string into a URL using -[NSURL fileURLWithPath:]
and calls -[NSConcreteFileHandle initWithURL:flags:createMode:error:]
, which calls _NSOpenFileDescriptor
to do the actual file system calls. That calls open
with only the flag O_WRONLY
; it does not pass O_CREAT
nor O_EXCL
. So FileHandle
cannot create new files (which I find a bit unintuitive, as nothing in the name really suggests that limitation).
NSData(contentsOfFile:options:)
& friends
…ultimately call open
with no flags (meaning they can only read existing files, not even modify them). So again, cannot create new files. Which is perhaps implied and obvious from the name, but it’s good to be certain.
- In principle system programs & privileged (e.g. root & admin) programs should be defended against too, but it’s often impractically difficult to do so, and beyond the scope of this post to try to explain how.
Root is of course the most impractical to defend against – even though the root user is not a traditional “God” user on macOS, Apple’s nerfing of root is designed to protect Apple’s programs, not yours. Root (and admin users) can ultimately still access any files your application(s) create and there’s nothing you can do about it (other than potentially through orthogonal protections, such as encryption). ↩︎ - It is of course possible for libraries to override the umask, but that would be particularly foul of them and none that I’ve surveyed do that, thankfully. ↩︎
- This can be hard to guarantee, in a non-trivial program. You can use an assertion or precondition on the return value of
pthread_is_threaded_np
to help ensure you’re modifying umask before additional threads are created. ↩︎ - It’s not apparent to me why it needs read access to the file as well – that appears to be a bug. ↩︎
- I’m not sure what happens if the App Sandbox container is not on the boot volume. ↩︎
- There are various mechanism for voluntary coordination, such as
NSFileCoordinator
andflock
, but programs are not required to use them (and malicious programs happily won’t). ↩︎