20

I am root. I would like to know whether a non-root user has write access to some files - thousands of them. How to do it efficiently while avoiding process creation?

  • Please show us what you actually do! – F. Hauri May 15 '15 at 14:39
  • Same kind of question as unix.stackexchange.com/a/88591 – Stéphane Chazelas May 15 '15 at 15:05
  • Assuming you don't care about race conditions, why not just call access(2) with an appropriately set real-UID (e.g. via setresuid(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 '15 at 17:47
  • 1
    @Kevin, shells' [ -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 '15 at 21:32
  • @StéphaneChazelas - you can use chgrp in any shell. – mikeserv May 18 '15 at 10:06
2

Perhaps like this:

#! /bin/bash

writable()
{
    local uid="$1"
    local gids="$2"
    local ids
    local perms

    ids=($( stat -L -c '%u %g %a' -- "$3" ))
    perms="0${ids[2]}"

    if [[ ${ids[0]} -eq $uid ]]; then
        return $(( ( perms & 0200 ) == 0 ))
    elif [[ $gids =~ (^|[[:space:]])"${ids[1]}"($|[[:space:]]) ]]; then
        return $(( ( perms & 020 ) == 0 ))
    else
        return $(( ( perms & 2 ) == 0 ))
    fi
}

user=foo
uid="$( id -u "$user" )"
gids="$( id -G "$user" )"

while IFS= read -r f; do
    writable "$uid" "$gids" "$f" && printf '%s writable\n' "$f"
done

The above runs a single external program for each file, namely stat(1).

Note: This assumes bash(1), and the Linux incarnation of stat(1).

Note 2: Please read comments from Stéphane Chazelas below for past, present, future, and potential dangers and limitations of this approach.

  • That could say a file is writable even though the user has no access to the directory where it is. – Stéphane Chazelas May 16 '15 at 8:22
  • That assumes file names don't contain newline character and file paths passed on stdin don't start with -. You could modify it to accept a NUL delimited list instead with read -d '' – Stéphane Chazelas May 16 '15 at 8:23
  • Note that there's no such thing as a Linux stat. Linux is the kernel found in some GNU and non-GNU systems. While there are commands (like from util-linux) that are specifically written for Linux, the stat you're referring to is not, it's a GNU command which has been ported to most systems, not only Linux. Also note that you had a stat command on Linux long before GNU stat was written (the stat zsh builtin). – Stéphane Chazelas May 16 '15 at 8:26
  • 2
    @Stéphane Chazelas: Note that there's no such thing as a Linux stat. - I believe I wrote "the Linux incarnation of stat(1)". I'm referring to a stat(1) that accepts the -c <format> syntax, as opposed to, say, BSD syntax -f <format>. I also believe it was pretty clear I wasn't referring to stat(2). I'm sure a wiki page about the history of the common commands would be quite interesting though. – lcd047 May 16 '15 at 8:35
  • 1
    @Stéphane Chazelas: That could say a file is writable even though the user has no access to the directory where it is. - True, and probably a reasonable limitation. That assumes file names don't contain newline character - True, and probably a reasonable limitation. and file paths passed on stdin don't start with - - Edited, thank you. – lcd047 May 16 '15 at 8:40
17

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:

  1. search access to / (no need for read)
  2. search access to /foo (no need for read)
  3. 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:

  1. $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.
  2. Same thing if he owns /a/b but doesn't have search access to it.
  3. $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.
  4. 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.
  5. 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.
  6. 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.
  • What is a search access ? I have never heard of it in the traditional permissions : read, write, execute. – bela83 May 15 '15 at 19:35
  • 2
    @bela83, execute permission on a directory (you don't execute directories) translates to search. That's the ability to access files in it. You can list a directory's content if you have read permission, but you can't do anything with the files in it unless you also have search (x bit) permission on the directory. You can also have search permissions but not read, meaning files in there are hidden to you, but if you know their name, you can access them. A typical example is the php session file directory (something like /var/lib/php). – Stéphane Chazelas May 15 '15 at 19:53
2

You can combine options with the find command, so it will find out the files with specified mode and owner. For instance:

$ find / \( -group staff -o -group users \) -and -perm -g+w

The above command will list all entries which belong to the group "staff" or "users" and have write permission for that group.

You should also check for entries which are owned by your user, and files wich are world writable, so:

$ find / \( -user yourusername -or \
             \(  \( -group staff -o -group users \) -and -perm -g+w \
             \) -or \
            -perm -o+w \
         \)

However, this command won't match entries with extended ACL. So you may su then find out all writable entries:

# su - yourusername
$ find / -writable
  • That would say that a file with r-xrwxrwx yourusername:anygroup or r-xr-xrwx anyone:staff is writeable. – Stéphane Chazelas May 15 '15 at 16:18
  • It would also report as writeable files that are in directories yourusername doesn't have access to. – Stéphane Chazelas May 15 '15 at 16:21
1

The approach depends upon what you are really testing.

  1. Do you want to ensure write access is possible?
  2. Do you want to ensure lack of write access?

This is because there are so many ways to arrive at 2) and Stéphane's answer covers these well (immutable is one to remember), and recall that there are physical means as well, such as unmounting the drive or making it read-only at a hardware level (floppy tabs). I am guessing your thousands of files are in differing directories and you are wanting a report or you are checking against a master list. (Another abuse of Puppet just waiting to happen).

You likely want Stéphane's perl tree-traversal and to "join" the output with a list if required (su will also catch missing execute on parent directories?). If surrogacy is a performance issue are you doing this for a "large number" of users ? Or is it an online query ? If this is a permanent ongoing requirement it may be time to consider a third party product.

0

You can do...

find / ! -type d -exec tee -a {} + </dev/null

...for a list of all files to which the user cannot write as written to stderr in the form...

"tee: cannot access %s\n", <pathname>" 

...or similar.

See the comments below for notes on the issues this approach might have, and the explanation below for why it might work. More sanely, though, you should probably only find regular files like:

find / -type f -exec tee -a {} + </dev/null

In short, tee will print errors when it attempts to open() a file with either of the two flags...

O_WRONLY

Open for writing only.

O_RDWR

Open for reading and writing. The result is undefined if this flag is applied to a FIFO.

...and encounters...

[EACCES]

Search permission is denied on a component of the path prefix, or the file exists and the permissions specified by oflag are denied, or the file does not exist and write permission is denied for the parent directory of the file to be created, or O_TRUNC is specified and write permission is denied.

...as specified here:

The tee utility shall copy standard input to standard output, making a copy in zero or more files. The tee utility shall not buffer output.

If the -a option is not specified, output files shall be written (see File Read, Write, and Creation)...

...POSIX.1-2008 requires functionality equivalent to using O_APPEND...

Because it has to check the same way test -w does...

-w pathname

True if pathname resolves to an existing directory entry for a file for which permission to write to the file will be granted, as defined in File Read, Write, and Creation. False if pathname cannot be resolved, or if pathname resolves to an existing directory entry for a file for which permission to write to the file will not be granted.

They both check for EACCESS.

  • You're likely to run into a limit of number of concurrently open files with that approach (unless the number of files is low). Beware of side effects with devices and named pipes. You'll get a different error message for sockets. – Stéphane Chazelas May 17 '15 at 21:20
  • @StéphaneChazelas - All tue - I think it might be also true that tee will hang unless explicitly interrupted once per run. It was the closest thing I could think of to [ -w... though - it's effects should be close in that it guarantees the user can OAPPEND the file. Much easier than either option would be pax with -o format options and/or -t for checking against EACCESS - but every time I suggest pax people seem to shrug it off. And, anyway, the only pax I've found that meets the std there is AST's - in which case you might as well use their ls. – mikeserv May 17 '15 at 22:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.