Because the obvious .com wasn't available.
Cat privy is starting as an experiment in reaction to this case reporting a social engineering attack leveraging time pressure & false sense of security to trick people into downloading a malicious payload from a online-meeting.
A Hard Boundary sandbox for Google Chrome on macOS that prevents data exfiltration (reading sensitive files) even if the browser process is fully compromised (Remote Code Execution), while maintaining full hardware acceleration (GPU/Camera/Mic).
Instead of relying on the macOS App Sandbox (which breaks Chrome) or Enterprise Policies (which are soft blocks), we manipulate the Unix Process Identity.
We offer two security modes:
- Selective Block (Default): Chrome runs as you but is explicitly DENIED access to specific folders (Documents, Desktop, .ssh).
- Default Deny: Chrome is denied access to everything in your Home directory by default and is only allowed to see specific "Whitelisted" folders (Downloads, Chrome Sandbox).
In the Default Deny configuration, we use a specific combination of ACLs on your Home directory:
- Deny
list: Prevents Chrome from seeing the names of files and folders in your home directory (e.g.,ls ~fails). - Allow
search: Allows Chrome to "traverse" through your home directory to reach folders it is explicitly allowed to see (like~/Downloads).
Tip
Clean Slate: To safely remove all isolation rules, handling both "ACL Exhaustion" (Memory Error) and "Ghost Inheritance":
for group in tdm-deny-all-chrome tdm-allow-selective-chrome tdm-deny-selective-chrome; do
# Pass 1: Pressure Valve (Pure Removal from Home)
# Solves "Cannot allocate memory" by freeing space first
while idx=$(/bin/ls -le -d ~ | grep "group:$group" | head -n 1 | awk -F: '{print $1}' | xargs) && [ -n "$idx" ]; do
/bin/chmod -a# "$idx" ~
done
# Pass 2: Ghost Flush
# Re-link and remove to force-clear inherited entries on children
/bin/chmod +a "group:$group allow search,file_inherit,directory_inherit" ~ 2>/dev/null
while idx=$(/bin/ls -le -d ~ | grep "group:$group" | head -n 1 | awk -F: '{print $1}' | xargs) && [ -n "$idx" ]; do
/bin/chmod -a# "$idx" ~
done
# Pass 3: Final Sub-file Sweep
/usr/bin/find ~ -maxdepth 4 -acl -print0 2>/dev/null | xargs -0 -n1 bash -c "
while idx=\$(/bin/ls -le -d \"\$1\" | grep \"group:$group\" | grep -v \"inherited\" | head -n 1 | awk -F: '{print \$1}' | xargs) && [ -n \"\$idx\" ]; do
/bin/chmod -a# \"\$idx\" \"\$1\" 2>/dev/null || break
done
" --
doneVerify with:
/usr/bin/find ~ -maxdepth 2 -exec /bin/ls -le -d {} + 2>/dev/null | grep -B 1 "group:tdm-"# Selective Block Group (GID 9999)
sudo dseditgroup -o create -i 9999 tdm-deny-selective-chrome
# Default Deny Groups (GID 9998, 9997)
sudo dseditgroup -o create -i 9998 tdm-deny-all-chrome
sudo dseditgroup -o create -i 9997 tdm-allow-selective-chromeFor Selective Block:
# Documents, Desktop, SSH
for folder in Documents Desktop .ssh; do
target="$HOME/$folder"
rule="group:tdm-deny-selective-chrome deny read,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,file_inherit,directory_inherit"
# 1. Remove ALL existing instances of this rule (Idempotent)
while /bin/chmod -a "$rule" "$target" 2>/dev/null; do :; done
# 2. Apply the rule once
/bin/chmod +a "$rule" "$target"
doneFor Default Deny:
# 1. Clear existing Home ACLs (Ensures Clean Slate for Home)
/bin/chmod -N ~
# 2. Block Chrome from LISTING files in Home, but allow traversing through it
/bin/chmod "+a#" 0 "group:tdm-deny-all-chrome deny list" ~
/bin/chmod "+a#" 0 "group:tdm-deny-all-chrome allow search,readattr" ~
/bin/chmod "+a#" 0 "group:tdm-allow-selective-chrome allow search,readattr" ~
# 3. Block Chrome from READING/WRITING any individual FILE in Home (Inheritable)
/bin/chmod "+a#" 0 "group:tdm-deny-all-chrome deny read,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,only_inherit,file_inherit" ~
# 3b. Seal ALL Existing Top-Level Folders (True Default Deny)
# We must explicitly lock existing folders because inheritance doesn't propagate to them automatically.
for folder in ~/*; do
folder_name=$(basename "$folder")
# Skip Whitelisted Folders (Add others here if needed, e.g. "Public")
if [[ "$folder_name" == "Downloads" || "$folder_name" == "chrome_sandbox" ]]; then
continue
fi
# Skip items we don't own (prevents "Operation not permitted" on system files)
if [[ ! -O "$folder" ]]; then continue; fi
rule="group:tdm-deny-all-chrome deny read,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,file_inherit,directory_inherit"
while /bin/chmod -a "$rule" "$folder" 2>/dev/null; do :; done
/bin/chmod +a "$rule" "$folder"
done
# 4. Whitelist Downloads & Sandbox Profile
mkdir -p ~/chrome_sandbox
for target in ~/chrome_sandbox ~/Downloads; do
# Define rules
allow_sel="group:tdm-allow-selective-chrome allow read,list,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,file_inherit,directory_inherit"
allow_deny="group:tdm-deny-all-chrome allow read,list,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,file_inherit,directory_inherit"
# 1. Loop-remove ALL existing instances
while /bin/chmod -a "$allow_sel" "$target" 2>/dev/null; do :; done
while /bin/chmod -a "$allow_deny" "$target" 2>/dev/null; do :; done
# 2. Apply rules once (at the top, index 0)
/bin/chmod "+a#" 0 "$allow_sel" "$target"
/bin/chmod "+a#" 0 "$allow_deny" "$target"
done
# 4b. Whitelist System Keychain (Enable Passwords/Login)
# 1. Allow traversing ~/Library (Insert at TOP to override Deny)
/bin/chmod "+a#" 0 "group:tdm-deny-all-chrome allow search" ~/Library
# 2. Whitelist Keychains folder
target=~/Library/Keychains
allow_keychain="group:tdm-deny-all-chrome allow read,list,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,file_inherit,directory_inherit"
if [ -d "$target" ]; then
while /bin/chmod -R -a "$allow_keychain" "$target" 2>/dev/null; do :; done
/bin/chmod -R "+a#" 0 "$allow_keychain" "$target"
fi
# 5. Explicitly block .ssh (Security Requirement)
rule_ssh="group:tdm-deny-all-chrome deny read,write,execute,delete,append,readattr,writeattr,readextattr,writeextattr,readsecurity,writesecurity,chown,file_inherit,directory_inherit"
while /bin/chmod -a "$rule_ssh" ~/.ssh 2>/dev/null; do :; done
/bin/chmod +a "$rule_ssh" ~/.sshThe wrapper (cat-privy.c) must be compiled and given setuid root permissions.
# Compile
clang -O3 \
-fstack-protector-all \
-D_FORTIFY_SOURCE=2 \
-Wl,-pie \
-ftrivial-auto-var-init=pattern \
-o cat-privy cat-privy.c
# apply entitlements
codesign --entitlements entitlements.plist -s - --force ./cat-privy
# Set Ownership & Permissions
sudo chown root:wheel cat-privy
sudo /bin/chmod 4755 cat-privy# Selective Mode (Default)
./chrome-privy.sh selective
# Default Deny Mode
./chrome-privy.sh default-denyCheck the terminal output for the dropped identity:
- Selective:
Final UID: 501, GID: 9999 - Default Deny:
Final UID: 501, GID: 9998(with 9997 in supplementary)
The privy-ls tool allows you to simulate the permissions of the Chrome process. Since it modifies process identity, it requires root privileges via sudo.
# Test Selective Block
sudo ./privy-ls 9999 ~/Documents
# Test Default Deny Block
sudo ./privy-ls 9998,9997 ~/Documents
# Test Default Deny Whitelist
sudo ./privy-ls 9998,9997 ~/DownloadsVerify that the ACLs are actually applied to your home directory:
/usr/bin/find ~ -maxdepth 2 -exec /bin/ls -le -d {} + 2>/dev/null | grep -B 1 "group:tdm-"In Chrome, try to upload a file from ~/Documents to a website. The upload will fail (0 bytes or error).
- Why can I still see files in the "Open File" dialog? The dialog is drawn by the macOS System (Trusted), not Chrome. It shows you the files you have access to. However, when you select a file and click "Open", Chrome (Untrusted) receives the file path but cannot read it because the Kernel enforces the GID 9999 block.
- How do I upload files?
Move the file to a "Transfer" folder (like
~/Downloadsor~/Public) that does not have the Deny ACL. This mimics an "Air-Gap" transfer workflow.