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