#!/usr/bin/perl -T # checkperms: ensure as best we can that a given file can only be # modified by the given user (or the superuser, naturally). This # means checking file ownership and permissions all the way back to # the root directory. Pass the file by its absolute path. # example invocation: # checkperms dkg /home/dkg/.monkeysphere/authorized_user_ids # return values: zero if we believe the file and path can only be # modified by the user. non-zero otherwise. # see StrictModes in sshd_config(5) (and its implementation in # OpenSSH's secure_filename() in auth.c) for the initial # inspiration/rationale for this code. # Author: # Daniel Kahn Gillmor # Started on: 2009-07-31 11:10:16-0400 # License: GPL v3 or later use strict; use Cwd qw(realpath); # found in debian in perl-base use File::stat; # found in debian in perl-modules use User::pwent; # found in debian in perl-modules use Fcntl qw(:mode); # for S_IS* functions (in perl-base) use File::Basename; # for dirname (in perl-modules) my $username = shift; my $path = shift; defined($username) or die "You must pass a username and an absolute path.\n"; defined($path) or die "You must pass a username and an absolute path.\n"; my $pw = getpwnam($username) or die "no such user $username\n"; $path =~ m#^/# or die "path was not absolute (did not start with /)\n"; sub debug { if ($ENV{LOG_LEVEL} eq 'DEBUG') { # FIXME: prefix with $ENV{LOG_PREFIX} printf STDERR @_; } } ## return undef if permissions are OK. otherwise return an error string sub permissions_ok { my $user = shift; my $path = shift; # if we can't even stat the path, the permissions are not ok: my $stat = lstat($path) or return "cannot stat '$path'"; while (S_ISLNK($stat->mode)) { my $newpath = realpath($path) or return "cannot trace symlink '$path'"; debug("tracing link %s to %s\n", $path, $newpath); $path = $newpath; $stat = lstat($path) or return "cannot stat '$path'"; } debug("checking '%s'\n", $path); if (($stat->uid != $user->uid) && ($stat->uid != 0)) { return sprintf("improper ownership on '%s': owner ID %d is neither %s (ID %d) nor the superuser", $path, $stat->uid, $user->name, $user->uid); } if (S_IWGRP & $stat->mode) { return sprintf("improper group writability on '%s'", $path); } if (S_IWGRP & $stat->mode) { return sprintf("improper group writability on '%s'", $path); } if (S_IWOTH & $stat->mode) { return sprintf("improper other writability on '%s'", $path); } # see the rationalization in secure_filename() in auth.c in the # OpenSSH sources for an explanation of this bailout (see also # monkeysphere #675): if ($path eq $user->dir) { debug("stopping at the %s's home directory '%s'\n", $user->name, $path); return undef; } my $nextlevel = dirname($path); if ($path eq $nextlevel) { # we bottom out at the root (/ in UNIX) return undef; } return permissions_ok($user, $nextlevel); } my $err = permissions_ok($pw, $path); if (defined($err)) { printf(STDERR "%s%s\n", $ENV{LOG_PREFIX}, $err); exit(1); } else { exit(0); }