Find files by permission bits
Search a directory tree for files matching specific permission bits — world-writable files for a security audit, SUID/SGID binaries for a post-CVE sweep, or files with exactly `0644` to verify a deployment.
How to find files by permission bits in each shell
find . -type f -perm -0002`-perm -0002` finds files where the world-writable bit is SET (regardless of other bits). The leading `-` is critical: `-perm 0002` would match files whose mode is EXACTLY `0002` (almost nothing). Three modes: `-perm 644` EXACT match, `-perm -644` AT-LEAST these bits set (most common), `-perm /644` ANY of these bits set (legacy `+644` is deprecated). Always use the symbolic prefix to make intent unambiguous.
find . -type f -perm -0002find . -type f -perm -0002Get-ChildItem -Recurse -File | Where-Object { (Get-Acl $_.FullName).Access | Where-Object { $_.IdentityReference -eq "Everyone" -and ($_.FileSystemRights -band [Security.AccessControl.FileSystemRights]::Write) -and $_.AccessControlType -eq "Allow" } }Windows-only — POSIX bits don't exist; you filter by ACE properties. `FileSystemRights` is a `[Flags]` enum, so `-band` is the bitwise test (NOT `-eq`). `Everyone` is the well-known SID `S-1-1-0` — also check `BUILTIN\Users` (`S-1-5-32-545`) which is broader on default Windows installs. On pwsh-Linux/macOS, fall back to `find -perm`.
icacls C:\path\* /q | findstr /i "Everyone"cmd has no native permission-search verb. `icacls /q` per-file plus `findstr` is the closest. For a recursive sweep: `for /R C:\path %F in (*) do @icacls "%F" 2>nul | findstr /i "Everyone"`. Stub output and slow — for any real audit, use PowerShell or WSL.
Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.
Gotchas & notes
- **The three `-perm` forms — get them backwards and you find nothing**: `-perm 644` matches files whose mode is EXACTLY `0644` (every other bit clear). `-perm -644` matches files where `0644` bits are ALL set (other bits may also be set — `0744`, `0755`, etc. all match). `-perm /644` matches files where AT LEAST ONE of the `0644` bits is set (so `0444` matches because `r--r--r--` shares the read bits; `0200` does NOT match because `0644` has no write-by-other). The mnemonic: no prefix = EXACT, `-` = AT LEAST, `/` = ANY. The deprecated `+644` was renamed to `/644` in GNU find 4.2.21 (2005) — old scripts still use `+`, which now warns.
- **World-writable hunt (security audit)** — find files anyone can modify: `find / -xdev -type f -perm -0002 ! -path "/proc/*" ! -path "/sys/*" ! -path "/tmp/*" ! -path "/var/tmp/*" 2>/dev/null`. `-xdev` keeps `find` on the current filesystem (skips NFS mounts, `/proc`, etc — important for one-pass audits). The excluded paths are the legitimate world-writable spots (sticky-bit `/tmp` is fine — `-perm -1002` catches sticky + ww). Pair with `-print0 | xargs -0 ls -l` to see ownership context. The CIS Benchmark for Linux specifically requires this audit on production hosts.
- **SUID / SGID audit — the standard post-CVE sweep**: `find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -printf "%M %u %g %p\n" 2>/dev/null`. SUID (`-4000`) means "run with file-owner's privileges" — `passwd`, `sudo`, `mount` legitimately need it. SGID (`-2000`) means "run with file-group's privileges" — `wall`, `chage` use it. Anything in `/home`, `/opt`, or `/tmp` with these bits is a red flag (typical privilege-escalation persistence). Compare against a baseline: `diff <(today's output) <(baseline)` flags new SUID binaries since last scan. For containers: `docker run --rm image find / -xdev -perm -4000 2>/dev/null` to audit images before deploy.
- **Combining find with other predicates** — the permission filter is one predicate; mix and match: `find /srv -type f -perm -0040 -not -group www-data` (files group-readable but NOT owned by `www-data` — config-leak audit); `find . -type f -perm -0111 -name "*.txt"` (executable .txt files — almost always a mistake); `find / -xdev -type d -perm -0002 -not -perm -1000` (world-writable dirs WITHOUT sticky bit — `/tmp` should have sticky `-1000`, anywhere else with `-0002` and not `-1000` is a hole). Combine with `-mmin -60` for "world-writable files created in the last hour" (incident-response).
- **Windows: FileSystemRights is a bit-flags enum, not a string**: the `.NET` `FileSystemRights` enum has 14+ values (`Read=131209`, `Write=278`, `ReadAndExecute=131241`, `Modify=197055`, `FullControl=2032127`). To test "is Write granted?" you MUST use `-band`: `if ($rule.FileSystemRights -band [Security.AccessControl.FileSystemRights]::Write) { ... }`. `-eq` will silently fail for ACEs that grant Modify or FullControl (both INCLUDE Write but as a superset bitmask, not equality). Also remember to filter by `$rule.AccessControlType -eq "Allow"` to exclude DENY ACEs and `$rule.IsInherited` to separate explicit vs inherited grants. For the analogue of "world-writable" on Windows: filter by `IdentityReference -in @("Everyone","BUILTIN\Users","NT AUTHORITY\Authenticated Users","BUILTIN\Guests")`.
Related commands
Related tasks
- View ACLs and extended file permissions— Inspect the full access-control list of a file — beyond the POSIX nine-bit `rwxrwxrwx` mode — to see per-user / per-group grants, the ACL mask, default (inherited) ACLs, and the audit (SACL) entries that `ls -l` will not show.
- Change file permissions— Modify read/write/execute permissions on a file or directory.
- Set permissions recursively on a directory tree— Apply a single permission change to a directory and every file underneath — for locking down a freshly-extracted tarball, undoing world-writable mistakes, or normalizing permissions on a copied directory.