1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
#!/bin/bash
# This script is not provided with the assumption that it is absolutely secure - instead, its primary function is to give users the convenience of using their configuration for their editor while editing root-owned files
# Certain situations where security issues might arise, such as when copying a non-world-readable file to /tmp, are disallowed for this reason, but I can make no promises about the security of using this software
# If, on the other hand, you have a suggestion to improve the security of the script, please contact me at magnus@iastate.edu or on matrix @magnustesshu:matrix.org and tell me how to improve it
# Since I am lazy and am using this to write small scripts under /usr/local/bin sometimes, I also added a feature for myself to install with permissions already set if you are writing a new file and start it with a shebang.
EDITOR=${EDITOR:-vi}
fileperms=""
anyreadperm=""
ERR() {
echo -e "doasedit: $1";
[[ -v doasediting_file ]] && [[ -f "$doasediting_file" ]] && echo "(Leaving behind file $doasediting_file)"
exit 1
}
# Set shell options to fail if any variable expansion or final expression in a pipeline fails
set -eu
# Single argument was passed
[[ $# != "1" ]] && ERR 'usage:\n\tdoasedit /file/owned/by/root\n\tdoasedit /new/file\t(starting a new file with "#!" will make it executable)'
# Ensure this script is run as a user who has doas priviledges, editing a regular or nonexistent file
[[ "$(id -u)" == "0" ]] && ERR "Do not run doasedit as root!"
# To allow doasedit, you must be allowed to run `doas doasedit`
doas -C /etc/doas.conf doasedit \"$1\" >/dev/null || ERR "You are not a doer"
[[ -d "$1" ]] && ERR "'$1' is a directory" # File is a directory
# Ensure the file is in a directory we can read
[[ -r "`dirname $1`" ]] || ERR "'$1' is a directory you do not have permission to read"
# Ensure that the filepath is in a directory, as the above does not guarantee the directory is not actually a file
[[ -d "`dirname $1`" ]] || ERR "'`dirname $1`' is a file, not a directory"
# Now that we know the file is in a real directory, we can readlink it to create a temporary file
real_path=$(readlink -f "$1")
# While there are collisions with some dumb filenames, no harm can come from it and we inform the user why even if they do have a `/usr.bin` regular file for example
doasediting_file="/tmp/doasedit${real_path//\//.}"
# Ensure the file we create to edit does not already exist
[ -f "$doasediting_file" ] && echo -e "File '$1' is already being edited by user '$(stat -c %U $doasediting_file)'\n(the file $doasediting_file exists)" && exit 1
# In the positive case, the file exists, and we check that anyone can read it and that it is owned by root. In the negative case, the file does not exist
if [[ -e "$1" ]] ; then
[[ -r "$1" ]] || ERR "file '$1' is world-readable but is not readable by your user, what are you doing?"
fileperms=$(stat -L -c "%a" "$1")
anyreadperm=${fileperms:3:1} # With sticky bit
[[ ${#fileperms} == "3" ]] && anyreadperm=${fileperms:2:1} # without sticky bit
[[ $anyreadperm -ge "4" ]] || ERR "file '$1' is not world-readable (since security is hard, this is a sacrifice doasedit makes for ease of implementation)"
[[ "$(stat -L -c "%u" "$1")" == "0" ]] || ERR "Can only edit root-owned files with this script, as copying the file back will make root the owner"
# Preserves file permissions, amazingly
cp "$1" "$doasediting_file" || ERR "Cannot copy to $doasediting_file"
else
# Default permissions are 644
rm -f "$doasediting_file" || ERR "Cannot remove $doasediting_file"
touch "$doasediting_file" || ERR "Cannot touch $doasediting_file"
fi
$EDITOR "$doasediting_file" || ERR "The editor failed to exit successfully"
# If the original file existed, don't update it if we did not make any modification to the temporary copy
[[ -e "$1" ]] && cmp -s "$doasediting_file" "$1" && rm "$doasediting_file" && exit 0
# If the original file did not exist, print a warning
[[ -e "$1" ]] || echo "doasedit: File '$1' will be created, if this is not desireable press ctrl+C"
# Ensure if we edit '/etc/doas.conf' that we didn't create a syntax error or other mistake
[[ "/etc/doas.conf" == "$1" ]] && {
echo "$ doas -C "$doasediting_file" doasedit \"$1\"" &&
doas -C "$doasediting_file" doasedit \"$1\" || {
# There probably is a syntax error. Move the file so we can re-run doasedit to edit it again
mv -f "$doasediting_file" "$doasediting_file".backup
ERR "Replacing '/etc/doas.conf' would mean you no longer have permissions to edit it any further.\nIf you do not know what you're doing, you had a syntax error, so see the above output and try again."
}
}
doas cp "$doasediting_file" "$1" || ERR "You put your password in wrong. Manually run 'doas cp "$doasediting_file" $1'"
# Remove the temporary file now that it has been copied successfully back into place. We do this here rather than with `trap` because if an error occurs, we don't necessarily want to overwrite the user's changes to that file
rm "$doasediting_file"
# Change permissions if the file was empty and starts with a shebang
[[ $fileperms != "" ]] || [[ `head -c 2 "$1"` != "#!" ]] || { echo "doasedit: automatically running \`doas chmod +x '$1'\`" && doas chmod +x "$1" ; } || ERR "Cannot change permissions of '$1'"
|