1;# $Id$ 2;# 3;# Copyright (c) 1990-2006, Raphael Manfredi 4;# 5;# You may redistribute only under the terms of the Artistic License, 6;# as specified in the README file that comes with the distribution. 7;# You may reuse parts of this distribution only within the terms of 8;# that same Artistic License; a copy of which may be found at the root 9;# of the source tree for mailagent 3.0. 10;# 11;# $Log: secure.pl,v $ 12;# Revision 3.0.1.7 1998/07/28 17:07:05 ram 13;# patch62: now explicitely log when too many symlink levels are found 14;# 15;# Revision 3.0.1.6 1997/02/20 11:46:00 ram 16;# patch55: now honours groupsafe and execsafe configuration variables 17;# 18;# Revision 3.0.1.5 1997/01/07 18:35:52 ram 19;# patch52: now only perform extended exec() checks iff execsafe is ON 20;# 21;# Revision 3.0.1.4 1996/12/24 15:00:39 ram 22;# patch45: extended security checks and created exec_secure() 23;# 24;# Revision 3.0.1.3 1995/03/21 12:57:42 ram 25;# patch35: symbolic directories are now correctly followed 26;# 27;# Revision 3.0.1.2 1994/10/04 17:55:19 ram 28;# patch17: wrong stat command could cause errors when checking symlinks 29;# 30;# Revision 3.0.1.1 1994/09/22 14:38:04 ram 31;# patch12: symbolic directories are now specially handled 32;# 33;# Revision 3.0 1993/11/29 13:49:16 ram 34;# Baseline for mailagent 3.0 netwide release. 35;# 36;# Requires pl/cdir.pl to derive paths when following symbolic links. 37;# 38# A file "secure" if it is owned by the user and not world writable. Some key 39# file within the mailagent have to be kept secure or they might compromise the 40# security of the user account. 41# 42# Additionally, for 'root' users or if the 'secure' parameter in the config 43# file is set to ON, checks are made for group writable files and suspicious 44# directory as well. 45# 46# Return true if the file is secure or missing, false otherwise. 47# Note the extra parameter $exec which is set by exec_secure() only. 48sub file_secure { 49 my ($file, $type, $exec) = @_; 50 return 1 unless -e $file; # Missing file considered secure 51 52 # We're resolving symlinks recursively 53 # NB: Race condition between our checks and the perusal of the file! 54 # --RAM, 2016-09-13 55 56 if (-l $file) { # File is a symbolic link 57 my $target = &symfile_secure($file, $type); 58 unless (defined $target) { 59 # Symbolic link is not secure 60 unless ($exec) { 61 &add_log( 62 "WARNING sensitive $type file $file is an " . 63 "unsecure symbolic link" 64 ) if $loglvl > 5; 65 } 66 return 0; # Unsecure file 67 } 68 $file = $target; 69 if ($exec) { 70 &add_log("NOTICE running $type $file actually runs $target") 71 if $loglvl > 6; 72 } 73 } 74 75 local($ST_MODE) = 2 + $[; # Field st_mode from inode structure 76 unless ($exec || -O _) { # Reuse stat info from -e 77 &add_log("WARNING you do not own $type file $file") if $loglvl > 5; 78 return 0; # Unsecure file 79 } 80 local($st_mode) = (stat(_))[$ST_MODE]; 81 if ($st_mode & $S_IWOTH) { 82 &add_log("WARNING $type file $file is world writable!") if $loglvl > 5; 83 return 0; # Unsecure file 84 } 85 86 # If file is excutable and seg[ug]id, make sure it's not publicly writable. 87 # If writable at all, only the owner should have the rights. That's for 88 # systems which do no reset the set[ug]id bit on write to the file. 89 if (-x _) { 90 if (($st_mode & $S_ISUID) && ($st_mode & ($S_IWGRP|$S_IWOTH))) { 91 &add_log("WARNING setuid $type file $file is writable!") 92 if $loglvl > 5; 93 return 0; 94 } 95 if (($st_mode & $S_ISGID) && ($st_mode & ($S_IWGRP|$S_IWOTH))) { 96 &add_log("WARNING setgid $type file $file is writable!") 97 if $loglvl > 5; 98 return 0; 99 } 100 } 101 102 return 1 unless $cf'secure =~ /on/i || $< == 0; 103 104 # Extra checks for secure mode (or if root user). We make sure the 105 # file is not writable by group and then we conduct the same secure tests 106 # on the directory itself 107 if (($st_mode & $S_IWGRP) && $cf'groupsafe !~ /^off/i) { 108 &add_log("WARNING $type file $file is group writable!") if $loglvl > 5; 109 return 0; # Unsecure file 110 } 111 local($dir); # directory where file is located 112 $dir = '.' unless ($dir) = ($file =~ m|(.*)/.*|); 113 unless ($exec || -O $dir) { 114 &add_log("WARNING you do not own directory of $type file $file") 115 if $loglvl > 5; 116 return 0; # Unsecure directory, therefore unsecure file 117 } 118 $st_mode = (stat(_))[$ST_MODE]; 119 return 0 unless &check_st_mode($dir, 1); 120 121 # If linkdirs is OFF, we do not check further when faced with a symbolic 122 # link to a directory. 123 if (-l $dir && $cf'linkdirs !~ /^off/i && !&symdir_secure($dir, $type)) { 124 &add_log("WARNING directory of $type file $file is an unsecure symlink") 125 if $loglvl > 5; 126 return 0; # Unsecure directory 127 } 128 129 1; # At last! File is secure... 130} 131 132# Is a symbolic link to a directory secure? 133sub symdir_secure { 134 local($dir, $type) = @_; 135 if (&symdir_check($dir, 0)) { 136 &add_log("symbolic directory $dir for $type file is secure") 137 if $loglvl > 11; 138 return 1; 139 } 140 0; # Not secure 141} 142 143# Is a symbolic link to a file secure? 144# Returns the final target if all links up to that file are secure, undef 145# if one of the links is not secure enough. 146sub symfile_secure { 147 local($file, $type) = @_; 148 local($target) = &symfile_check($file, 0); 149 if (defined $target) { 150 &add_log("symbolic file $file for $type file is secure") 151 if $loglvl > 11; 152 } else { 153 &add_log("WARNING symbolic file $file for $type file is unsecure") 154 if $loglvl > 5; 155 } 156 return $target; 157} 158 159# A symbolic directory (that is a symlink pointing to a directory) is secure 160# if and only if: 161# - its target is a symlink that recursively proves to be secure. 162# - the target lies in a non world-writable directory 163# - the final directory at the end of the symlink chain is not world-writable 164# - less than $MAX_LINKS levels of indirection are needed to reach a real dir 165# Unfortunately, we cannot check for group writability here for the parent 166# target directory since the target might lie in a system directory which may 167# have a legitimate need to be read/write for root and wheel, for instance. 168# The routine returns 1 if the file is secure, 0 otherwise. 169sub symdir_check { 170 local($dir, $level) = @_; # Directory, indirection level 171 $MAX_LINKS = 100 unless defined $MAX_LINKS; # May have been overridden 172 if ($level++ > $MAX_LINKS) { 173 &add_log("ERROR more than $MAX_LINKS levels of symlinks to reach $dir") 174 if $loglvl; 175 return 0 176 } 177 local($ndir) = readlink($dir); 178 unless (defined $ndir) { 179 &add_log("SYSERR readlink: $!") if $loglvl; 180 return 0; 181 } 182 $dir =~ s|(.*)/.*|$1|; # Suppress link component (tail) 183 $dir = &cdir($ndir, $dir); # Follow symlink to get its final path target 184 local($still_link) = -l $dir; 185 unless (-d $dir || $still_link) { 186 &add_log("ERROR inconsistency: $dir is a plain file?") if $loglvl; 187 return 0; # Reached a plain file while following links to a dir! 188 } 189 unless (-d "$dir/..") { 190 &add_log("ERROR inconsistency: $dir/.. is not a directory?") if $loglvl; 191 return 0; # Reached a file hooked nowhere in the file system! 192 } 193 # Check parent directory 194 local($ST_MODE) = 2 + $[; # Field st_mode from inode structure 195 $st_mode = (stat(_))[$ST_MODE]; 196 return 0 unless &check_st_mode("$dir/..", 0); 197 # Recurse if still a symbolic link 198 if ($still_link) { 199 return 0 unless &symdir_check($dir, $level); 200 } else { 201 $st_mode = (stat($dir))[$ST_MODE]; 202 return 0 unless &check_st_mode($dir, 1); 203 } 204 1; # Ok, link is secure 205} 206 207# Same as symdir_check, but target is a file! 208sub symfile_check { 209 local($file, $level) = @_; # File, indirection level 210 return undef if $level++ > $MAX_LINKS; 211 local($nfile) = readlink($file); 212 unless (defined $nfile) { 213 &add_log("SYSERR readlink: $!") if $loglvl; 214 return undef; 215 } 216 local($dir) = $file; # Where symlink was held 217 $dir =~ s|(.*)/.*|$1|; # Suppress link component (tail) 218 $file = &cdir($nfile, $dir); # Follow symlink to get its path 219 local($still_link) = -l $file; 220 unless (-f $file || $still_link) { 221 &add_log("ERROR $file does not exist") if !-e _ && $loglvl; 222 &add_log("ERROR $file is not a plain file") if -e _ && $loglvl; 223 return undef; # Reached something that is not a plain file 224 } 225 # Check parent directory 226 ($dir = $file) =~ s|(.*)/.*|$1|; 227 local($ST_MODE) = 2 + $[; # Field st_mode from inode structure 228 $st_mode = (stat($dir))[$ST_MODE]; 229 return undef unless &check_st_mode($dir, 1); 230 return $file unless $still_link; # Ok, link is secure 231 return &symfile_check($file, $level); # Still a symbolic link 232} 233 234# Returns true if mode in $st_mode does not include world or group writable 235# bits, false otherwise. This helps factorizing code used in both &file_secure 236# and &symdir_check. Set $both to true if both world/group checks are desirable, 237# false to get only world checks. 238sub check_st_mode { 239 local($dir, $both) = @_; 240 if ($st_mode & $S_IWOTH) { 241 &add_log("WARNING directory $dir of $type file is world writable!") 242 if $loglvl > 5; 243 return 0; # Unsecure directory 244 } 245 return 1 unless $both; 246 if (($st_mode & $S_IWGRP) && $cf'groupsafe !~ /^off/i) { 247 &add_log("WARNING directory $dir of $type file is group writable!") 248 if $loglvl > 5; 249 return 0; # Unsecure directory 250 } 251 1; 252} 253 254# Make sure the file we are about to execute is secure. If it is a script 255# with the '#!' kernel hook, also check the interpreter! Returns true if the 256# file can be executed "safely". 257sub exec_secure { 258 local($file) = @_; # File to be executed 259 260 unless (-x $file) { 261 &add_log("ERROR lacking execute rights on $file") if $loglvl > 1; 262 return 0; 263 } 264 265 return 1 if $cf'execskip =~ /^on/i; # Assume safe to be exec'ed 266 267 local($cf'secure) = $cf'execsafe; # Use exec settings for file_secure() 268 269 unless (&file_secure($file, 'program', 1)) { 270 &add_log("ERROR cannot execute unsecure $file") if $loglvl > 1; 271 return 0; 272 } 273 274 &add_log("can allow exec() of $file") if $loglvl > 17; 275 276 return 1 unless -T $file; # Safe as far as we can tell, unless script... 277 278 local($head); # Heading line 279 local($interpretor); # Interpretor running the script 280 local($perl) = ''; # Empiric support for perl scripts 281 local(*SCRIPT); 282 283 unless (open(SCRIPT, $file)) { 284 &add_log("SYSERR open: $!") if $loglvl > 1; 285 &add_log("ERROR cannot check script $file") if $loglvl > 1; 286 return 0; 287 } 288 289 $head = <SCRIPT>; 290 291 # Allow empiric support for common perl scripts 292 # This is not bullet-proof, but should guard against common errors. 293 294 if ($head =~ /\bperl\b/) { 295 $perl = <SCRIPT>; 296 if ($perl =~ /\beval\b.*\bexec\s+(\S+)/) { 297 $perl = $1; 298 } else { 299 $perl = ''; # False alarm, can't check further 300 } 301 } 302 303 close SCRIPT; 304 305 ($interpretor) = $head =~ /^#!\s*(\S+)/; 306 $interpretor = '/bin/sh' unless $interpretor; 307 unless (-x $interpretor) { 308 &add_log("ERROR lacking execute rights on $interpretor") if $loglvl > 1; 309 return 0; 310 } 311 312 unless (&file_secure($interpretor, 'interpretor', 1)) { 313 &add_log("ERROR cannot run unsecure interpretor $interpretor") 314 if $loglvl > 1; 315 &add_log("ERROR cannot allow execution of script $file") if $loglvl > 1; 316 return 0; 317 } 318 319 &add_log("can allow $interpretor to run $file") if $loglvl > 17; 320 321 return 1 unless $perl; # Okay, can run the script 322 323 $perl = &locate_program($perl) unless $perl =~ m|/|; 324 unless (-x $perl) { 325 &add_log("ERROR lacking execute rights on $perl") if $loglvl > 1; 326 return 0; 327 } 328 329 unless (&file_secure($perl, 'perl', 1)) { 330 &add_log("ERROR cannot run unsecure perl $perl") 331 if $loglvl > 1; 332 &add_log("ERROR cannot allow execution of perl script $file") 333 if $loglvl > 1; 334 return 0; 335 } 336 337 &add_log("can allow $perl to run $file") if $loglvl > 17; 338 339 return 1; # Okay, perl can run it 340} 341 342