On Unix-like systems, setting sufficiently strict permissions on a directory allows one to disallow access to its containing files and subdirectories, even if those containing files and subdirectories have more lax permissions.
Or so one would think. This turns out to be untrue on macOS due to magic tricks that we can perform on inodes. Read on to learn how a fundamental security assumption must be thrown overboard.
The access you didn't think you have
On the *nix1 side of life, filesystems are implemented as a bunch of inodes which point at chunks of data. The kernel maps paths to inodes and it is generally considered impossible to open an inode directly. Therefore it follows that you need permission to read and execute parent directories in order to access a child directory.
However this is not true on macOS. There is an overlay filesystem mounted at
/.vol/ which allows you to open any inode on the system directly so long as you have permission for that particular file or dir. What this means is that the protection of the permissions on the parent dirs is meaningless and you can bypass them to access child files or dirs as you like. This is especially important since most users will be part of a common group on multi-user systems. On macOS for instance, that would be the staff group. If they also don't happen to tightly control the permissions on the files in the subdirectories in their home dirs, it will provide a way to read files which people might have reasonably assumed you didn't have access to.
I wrote a quick POC2 for finding inodes on the system that you can read, and which could easily be modified to look for specific file or directory contents; I call it
inode_sleuth: hacking inodes in Rust
The whole program fits into 93 lines excluding empty lines and comments. It's written in Rust because I like Rust and it's amusing to write code that does "bad things" in a language designed around safety. It's not memory optimized, at all, nor is it designed to be hard to notice in the background, but that's because I don't feed skiddies2.
I also don't search the whole inode-space because it takes a while and as a POC, I don't feel like waiting for 100% correctness; that said, one can cover a reasonably large portion of the inode values actually in use by starting from 1 and working up to a multiple of the number of inodes in use (unless someone has any ideas on figuring out the highest inode in use, I find
6 × the number of inodes in use to be the upper limit on inode values on my system, though this would vary with how busy your system is).
You can find my POC below; feel free to try it out yourself, but be aware that it will make your system very slow while doing so. I also created a variant that continues to monitor inode creation and deletion in the background after the initial scan, but after some consideration I don't feel like releasing that version would benefit people's understanding of the issue, and it's not hard to make those changes regardless.