TL;DR
find / ! -type l -print0 |
sudo -u "$user" perl -Mfiletest=access -l -0ne 'print if -w'
You need to ask the system if the user has write permission. The only reliable way is to switch the effective uid, effective gid and supplementation gids to that of the user and use the access(W_OK)
system call (even that has some limitations on some systems/configurations).
And bear in mind that not having write permission to a file does not necessarily guarantee that you can't modify the content of the file at that path.
The longer story
Let's consider what it takes for instance for a $user to have write access to /foo/file.txt
(assuming none of /foo
and /foo/file.txt
are symlinks)?
He needs:
- search access to
/
(no need for read
)
- search access to
/foo
(no need for read
)
- write access to
/foo/file.txt
You can see already that approaches (like @lcd047's or @apaul's) that check only the permission of file.txt
won't work because they could say file.txt
is writable even if the user doesn't have search permission to /
or /foo
.
And an approach like:
sudo -u "$user" find / -writeble
Won't work either because it won't report the files in directories the user doesn't have read access (as find
running as $user
can't list their content) even if he can write to them.
If we forget about ACLs, read-only file systems, FS flags (like immutable), other security measures (apparmor, SELinux, which can even distinguish between different types of writing) and only focus on traditional permission and ownership attributes, to get a given (search or write) permission, that's already quite complicated and hard to express with find
.
You need:
- if the file is owned by you, you need that permission for the owner (or have uid 0)
- if the file is not owned by you, but the group is one of yours, then you need that permission for the group (or have uid 0).
- if it's not owned by you, and not in any of your groups, then the other permissions apply (unless your uid is 0).
In find
syntax, here as an example with a user of uid 1 and gids 1 and 2, that would be:
find / -type d \
\( \
-user 1 \( -perm -u=x -o -prune \) -o \
\( -group 1 -o -group 2 \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) -o -type l -o \
-user 1 \( ! -perm -u=w -o -print \) -o \
\( -group 1 -o -group 2 \) \( ! -perm -g=w -o -print \) -o \
! -perm -o=w -o -print
That one prunes the directories that user doesn't have search right for and for other types of files (symlinks excluded as they're not relevant), checks for write access.
If you also want to consider write access to directories:
find / -type d \
\( \
-user 1 \( -perm -u=x -o -prune \) -o \
\( -group 1 -o -group 2 \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user 1 \( ! -perm -u=w -o -print \) -o \
\( -group 1 -o -group 2 \) \( ! -perm -g=w -o -print \) -o \
! -perm -o=w -o -print
Or for an arbitrary $user
and its group membership retrieved from the user database:
groups=$(id -G "$user" | sed 's/ / -o -group /g'); IFS=" "
find / -type d \
\( \
-user "$user" \( -perm -u=x -o -prune \) -o \
\( -group $groups \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user "$user" \( ! -perm -u=w -o -print \) -o \
\( -group $groups \) \( ! -perm -g=w -o -print \) -o \
! -perm -o=w -o -print
(that's 3 processes in total: id
, sed
and find
)
The best here would be to descend the tree as root and check the permissions as the user for each file.
find / ! -type l -exec sudo -u "$user" sh -c '
for file do
[ -w "$file" ] && printf "%s\n" "$file"
done' sh {} +
(that's one find
process plus one sudo
and sh
process every few thousand files, [
and printf
are usually built in the shell).
Or with perl
:
find / ! -type l -print0 |
sudo -u "$user" perl -Mfiletest=access -l -0ne 'print if -w'
(3 processes in total: find
, sudo
and perl
).
Or with zsh
:
files=(/**/*(D^@))
USERNAME=$user
for f ($files) {
[ -w $f ] && print -r -- $f
}
(0 process in total, but stores the whole file list in memory)
Those solutions rely on the access(2)
system call. That is instead of reproducing the algorithm the system uses to check for access permission, we're asking the system to do that check with the same algorithm (which takes into account permissions, ACLs, immutable flags, read-only file systems...) it would use would you try to open the file for writing, so is the closest you're going to get to a reliable solution.
To test the solutions given here, with the various combinations of user, group and permissions, you could do:
perl -e '
for $u (1,2) {
for $g (1,2,3) {
$d1="u${u}g$g"; mkdir$d1;
for $m (0..511) {
$d2=$d1.sprintf"/%03o",$m; mkdir $d2; chown $u, $g, $d2; chmod $m,$d2;
for $uu (1,2) {
for $gg (1,2,3) {
$d3="$d2/u${uu}g$gg"; mkdir $d3;
for $mm (0..511) {
$f=$d3.sprintf"/%03o",$mm;
open F, ">","$f"; close F;
chown $uu, $gg, $f; chmod $mm, $f
}
}
}
}
}
}'
Varying user between 1 and 2 and group betweem 1, 2, and 3 and limiting ourselves to the lower 9 bits of the permissions as that's already 9458694 files created. That for directories and then again for files.
That creates all possible combinations of u<x>g<y>/<mode1>/u<z>g<w>/<mode2>
. The user with uid 1 and gids 1 and 2 would have write access to u2g1/010/u2g3/777
but not u1g2/677/u1g1/777
for instance.
Now, all those solutions try to identify the paths of files that the user may open for writing, that's different from the paths where the user may be able to modify the content. To answer that more generic question, there are several things to take into account:
- $user may not have write access to
/a/b/file
but if he owns file
(and has search access to /a/b
, and the file system is not read-only, and the file doesn't have the immutable flag, and he's got shell access to the system), then he would be able to change the permissions of the file
and grant himself access.
- Same thing if he owns
/a/b
but doesn't have search access to it.
- $user may not have access to
/a/b/file
because he doesn't have search access to /a
or /a/b
, but that file may have a hard link at /b/c/file
for instance, in which case he may be able to modify the content of /a/b/file
by opening it via its /b/c/file
path.
- Same thing with bind-mounts. He may not have search access to
/a
, but /a/b
may be bind-mounted in /c
, so he could open file
for writing via its /c/file
other path.
- He may not have write permissions to
/a/b/file
, but if he has write access to /a/b
he can remove or rename file
in there and replace it with his own version. He would change the content of the file at /a/b/file
even if that would be a different file.
- Same thing if he's got write access to
/a
(he could rename /a/b
to /a/c
, create a new /a/b
directory and a new file
in it.
To find the paths that $user
would be able to modify. To address 1 or 2, we can't rely on the access(2)
system call anymore. We could adjust our find -perm
approach to assume search access to directories, or write access to files as soon as you're the owner:
groups=$(id -G "$user" | sed 's/ / -o -group /g'); IFS=" "
find / -type d \
\( \
-user "$user" -o \
\( -group $groups \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user "$user" -print -o \
\( -group $groups \) \( ! -perm -g=w -o -print \) -o \
! -perm -o=w -o -print
We could address 3 and 4, by recording the device and inode numbers or all the files $user has write permission to and report all the file paths that have those dev+inode numbers. This time, we can use the more reliable access(2)
-based approaches:
Something like:
find / ! -type l -print0 |
sudo -u "$user" perl -Mfiletest=access -0lne 'print 0+-w,$_' |
perl -l -0ne '
($w,$p) = /(.)(.*)/;
($dev,$ino) = stat$p or next;
$writable{"$dev,$ino"} = 1 if $w;
push @{$p{"$dev,$ino"}}, $p;
END {
for $i (keys %writable) {
for $p (@{$p{$i}}) {
print $p;
}
}
}'
5 and 6 are at first glance complicated by the t
bit of the permissions. When applied on directories, that's the restricted deletion bit which prevents users (others than the owner of the directory) from removing or renaming the files they don't own (even though they have write access to the directory).
For instance, if we go back to our earlier example, if you have write access to /a
, then you should be able to rename /a/b
to /a/c
, and then recreate a /a/b
directory and a new file
in there. But if the t
bit is set on /a
and you don't own /a
, then you can only do it if you own /a/b
. That gives:
- If you own a directory, as per 1, you can grant yourself write access, and the t bit doesn't apply (and you could remove it anyway), so you can delete/rename/recreate any file or dirs in there, so all file paths under there are yours to rewrite with any content.
- If you don't own it but have write access, then:
- Either the
t
bit is not set, and you're in the same case as above (all file paths are yours).
- or it's set and then you can't modify the files you don't own or don't have write access to, so for our purpose of finding the file paths you can modify, that's the same as not having write permission at all.
So we can address all of 1, 2, 5 and 6 with:
find / -type d \
\( \
-user "$user" -prune -exec find {} + -o \
\( -group $groups \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user "$user" \( -type d -o -print \) -o \
\( -group $groups \) \( ! -perm -g=w -o \
-type d ! -perm -1000 -exec find {} + -o -print \) -o \
! -perm -o=w -o \
-type d ! -perm -1000 -exec find {} + -o \
-print
That and the solution for 3 and 4 are independent, you can merge their output to get a complete list:
{
find / ! -type l -print0 |
sudo -u "$user" perl -Mfiletest=access -0lne 'print 0+-w,$_' |
perl -0lne '
($w,$p) = /(.)(.*)/;
($dev,$ino) = stat$p or next;
$writable{"$dev,$ino"} = 1 if $w;
push @{$p{"$dev,$ino"}}, $p;
END {
for $i (keys %writable) {
for $p (@{$p{$i}}) {
print $p;
}
}
}'
find / -type d \
\( \
-user "$user" -prune -exec sh -c 'exec find "$@" -print0' sh {} + -o \
\( -group $groups \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user "$user" \( -type d -o -print0 \) -o \
\( -group $groups \) \( ! -perm -g=w -o \
-type d ! -perm -1000 -exec sh -c 'exec find "$@" -print0' sh {} + -o -print0 \) -o \
! -perm -o=w -o \
-type d ! -perm -1000 -exec sh -c 'exec find "$@" -print0' sh {} + -o \
-print0
} | perl -l -0ne 'print unless $seen{$_}++'
As should be clear if you've read everything thus far, part of it at least only deals with permissions and ownership, not the other features that may grant or restrict write access (read-only FS, ACLs, immutable flag, other security features...). And as we process it in several stages, some of that information may be wrong if the files/directories are being created/deleted/renamed or their permissions/ownership modified while that script is running, like on a busy file server with millions of files.
Portability notes
All that code is standard (POSIX, Unix for t
bit) except:
-print0
is a GNU extension now also supported by a few other implementations. With find
implementations that lack support for it, you can use -exec printf '%s\0' {} +
instead, and replace -exec sh -c 'exec find "$@" -print0' sh {} +
with -exec sh -c 'exec find "$@" -exec printf "%s\0" {\} +' sh {} +
.
perl
is not a POSIX-specified command but is widely available. You need perl-5.6.0
or above for -Mfiletest=access
.
zsh
is not a POSIX-specified command. That zsh
code above should work with zsh-3 (1995) and above.
sudo
is not a POSIX-specified command. The code should work with any version as long as the system configuration allows running perl
as the given user.
access(2)
with an appropriately set real-UID (e.g. viasetresuid(2)
or the portable equivalent)? I mean, I'd be hard-pressed to do that from bash, but I'm sure Perl/Python can handle it. – Kevin May 15 at 17:47[ -w
do generally use access(2) or equivalent. You also need to set the gids in addition to uid (as su or sudo do). bash doesn't have builtin support for that but zsh does. – Stéphane Chazelas May 15 at 21:32chgrp
in any shell. – mikeserv 14 hours ago