Guard disk_cleanup's --dirs, and stop --help hiding it
--dirs accepted any path and fed it to `find -delete` running as root,
with no confirmation: `--clean --dirs /home` removed every file in /home
past the age threshold, and `--dirs /` did it system-wide.
Now refuses protected directories, comparing the readlink -f resolved
path so a symlink or /tmp/../home cannot smuggle one through, and
requires absolute paths. Anything outside the /tmp,/var/tmp defaults
also needs an interactive confirmation -- or --yes, so unattended use
stays possible; without a tty and without --yes it refuses rather than
hanging in cron.
usage() sliced fixed line numbers (head -22 | tail -n +18) and had
already outgrown them, truncating --help mid-list at --age. So --dirs,
the one option that could destroy a system, was the one option --help
never mentioned. Replaced with a sed range that tracks the comment block
wherever it moves.
The first version of the protected-path check let "/" through: it
compared against "${p%/}", which turns the "/" entry into an empty
string that matches nothing. Caught by testing the guard against the
paths it exists to stop, rather than assuming it worked.
This commit is contained in:
+73
-1
@@ -21,6 +21,8 @@
|
|||||||
# --dry-run Show what would be cleaned without actually removing files
|
# --dry-run Show what would be cleaned without actually removing files
|
||||||
# --age DAYS Set age threshold for temp file cleanup (default: 7)
|
# --age DAYS Set age threshold for temp file cleanup (default: 7)
|
||||||
# --dirs DIR1,DIR2 Specify custom directories to clean (default: /tmp,/var/tmp)
|
# --dirs DIR1,DIR2 Specify custom directories to clean (default: /tmp,/var/tmp)
|
||||||
|
# Protected system directories are refused.
|
||||||
|
# --yes Skip the confirmation prompt for non-default --dirs
|
||||||
# --help Display this help message
|
# --help Display this help message
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
@@ -37,13 +39,18 @@ AGE_THRESHOLD=7
|
|||||||
DIRS_TO_CLEAN=("/tmp" "/var/tmp")
|
DIRS_TO_CLEAN=("/tmp" "/var/tmp")
|
||||||
DRY_RUN=false
|
DRY_RUN=false
|
||||||
CLEAN_MODE=false
|
CLEAN_MODE=false
|
||||||
|
ASSUME_YES=false
|
||||||
LOG_FILE="/var/log/disk_cleanup.log"
|
LOG_FILE="/var/log/disk_cleanup.log"
|
||||||
|
|
||||||
# ===== UTILITY FUNCTIONS =====
|
# ===== UTILITY FUNCTIONS =====
|
||||||
|
|
||||||
# Print usage information
|
# Print usage information
|
||||||
usage() {
|
usage() {
|
||||||
head -n 22 "$0" | tail -n +18
|
# Print the comment block between the "# Usage:" line and the blank
|
||||||
|
# comment that ends it. Previously this sliced fixed line numbers
|
||||||
|
# (head -22 | tail -n +18), which silently truncated the output at
|
||||||
|
# --age as soon as the block grew -- hiding --dirs entirely.
|
||||||
|
sed -n '/^# Usage:/,/^#$/p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Log messages to file and stdout
|
# Log messages to file and stdout
|
||||||
@@ -83,6 +90,66 @@ check_root() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Refuse to run -delete against paths that are not plausibly scratch
|
||||||
|
# space. --dirs previously accepted anything: `--clean --dirs /home`
|
||||||
|
# removed every file in /home older than the age threshold, and
|
||||||
|
# `--dirs /` did it system-wide, as root, with no confirmation.
|
||||||
|
validate_clean_dirs() {
|
||||||
|
local dir real
|
||||||
|
local protected=(
|
||||||
|
/ /bin /boot /dev /etc /home /lib /lib32 /lib64 /opt /proc /root
|
||||||
|
/run /sbin /srv /sys /usr /var
|
||||||
|
)
|
||||||
|
|
||||||
|
for dir in "${DIRS_TO_CLEAN[@]}"; do
|
||||||
|
[ -n "$dir" ] || error_exit "Empty directory in --dirs list." 1
|
||||||
|
|
||||||
|
case "$dir" in
|
||||||
|
/*) ;;
|
||||||
|
*) error_exit "--dirs requires absolute paths; got '$dir'." 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Compare the resolved path, so symlinks and /tmp/../home cannot
|
||||||
|
# smuggle a protected directory past the check.
|
||||||
|
real="$(readlink -f -- "$dir" 2>/dev/null || echo "$dir")"
|
||||||
|
# Normalise to a trailing-slash-free form, except "/" itself, which
|
||||||
|
# must stay "/" -- stripping it yields "" and matches nothing.
|
||||||
|
[ "$real" = "/" ] || real="${real%/}"
|
||||||
|
[ -n "$real" ] || real=/
|
||||||
|
|
||||||
|
for p in "${protected[@]}"; do
|
||||||
|
if [ "$real" = "$p" ]; then
|
||||||
|
error_exit "Refusing to clean '$dir' (resolves to '$real'): protected directory." 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Anything outside the built-in defaults gets an explicit confirmation,
|
||||||
|
# because that is where the destructive surprises live.
|
||||||
|
confirm_nondefault_dirs() {
|
||||||
|
local dir nondefault=()
|
||||||
|
for dir in "${DIRS_TO_CLEAN[@]}"; do
|
||||||
|
case "$dir" in
|
||||||
|
/tmp|/var/tmp) ;;
|
||||||
|
*) nondefault+=("$dir") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ ${#nondefault[@]} -eq 0 ] && return 0
|
||||||
|
[ "$DRY_RUN" = true ] && return 0
|
||||||
|
[ "$ASSUME_YES" = true ] && return 0
|
||||||
|
|
||||||
|
if [ ! -t 0 ]; then
|
||||||
|
error_exit "Non-default --dirs (${nondefault[*]}) need confirmation; pass --yes to run unattended." 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "About to delete files older than $AGE_THRESHOLD days from:"
|
||||||
|
printf ' %s\n' "${nondefault[@]}"
|
||||||
|
read -r -p "Proceed? (yes/NO): " reply
|
||||||
|
[ "$reply" = "yes" ] || error_exit "Cancelled." 0
|
||||||
|
}
|
||||||
|
|
||||||
# ===== DISPLAY FUNCTIONS =====
|
# ===== DISPLAY FUNCTIONS =====
|
||||||
|
|
||||||
# Show disk usage overview
|
# Show disk usage overview
|
||||||
@@ -193,6 +260,9 @@ parse_arguments() {
|
|||||||
--dry-run)
|
--dry-run)
|
||||||
DRY_RUN=true
|
DRY_RUN=true
|
||||||
;;
|
;;
|
||||||
|
--yes)
|
||||||
|
ASSUME_YES=true
|
||||||
|
;;
|
||||||
--age)
|
--age)
|
||||||
if [ -z "$2" ] || ! [[ "$2" =~ ^[0-9]+$ ]]; then
|
if [ -z "$2" ] || ! [[ "$2" =~ ^[0-9]+$ ]]; then
|
||||||
error_exit "Invalid age value. Must be a number." 1
|
error_exit "Invalid age value. Must be a number." 1
|
||||||
@@ -232,6 +302,8 @@ main() {
|
|||||||
else
|
else
|
||||||
# Cleanup mode
|
# Cleanup mode
|
||||||
check_root
|
check_root
|
||||||
|
validate_clean_dirs
|
||||||
|
confirm_nondefault_dirs
|
||||||
|
|
||||||
if [ "$DRY_RUN" = true ]; then
|
if [ "$DRY_RUN" = true ]; then
|
||||||
log "DRY RUN MODE - No files will be deleted"
|
log "DRY RUN MODE - No files will be deleted"
|
||||||
|
|||||||
Reference in New Issue
Block a user