66# Instantly move matched globs to staging, delete async in background.
77# Perfect for millions of files (node_modules, build artifacts, etc).
88#
9+ # SECURITY: This script includes protections against:
10+ # - Directory traversal attacks (.. rejection, symlink resolution)
11+ # - Critical system path deletion (/, /bin, /usr, /etc, etc)
12+ # - Staging folder escape (only deletes within $HOME/.rmAsync)
13+ # - Race conditions (atomic operations, path validation)
14+ # - Privilege escalation (uid/gid checks, file permissions)
15+ #
916# Usage:
1017# rmAsync <pattern> [<pattern2> ...]
1118# rmAsync status
1623# - Each execution gets unique staging folder (UUID + timestamp)
1724# - All matches staged atomically, then deleted as single folder
1825#
19- # Behavior:
20- # 1. Expand all glob patterns to file list
21- # 2. Create unique staging folder: $HOME/.rmAsync/<uuid>_<timestamp>
22- # 3. Move all matched files into staging folder (instant on same filesystem)
23- # 4. Return immediately (original paths now available)
24- # 5. Delete staging folder in background
25- # 6. Auto-cleanup orphans if no other rm processes active
26- #
27- # Performance:
28- # - Staging: O(n) where n = file count (fast due to inode moves)
29- # - Main process: returns immediately after staging
30- # - Deletion: happens in background, non-blocking
31- #
3226# ==============================================================================
3327
3428set -o pipefail
3529
30+ # Strict umask prevents world-readable staging dirs
31+ umask 0077
32+
3633# Configuration
3734readonly RM_ASYNC_DIR=" ${HOME} /.rmAsync"
3835readonly RM_ASYNC_PID_FILE=" ${RM_ASYNC_DIR} /.pids"
36+ readonly RM_ASYNC_LOCK=" ${RM_ASYNC_DIR} /.lock"
37+
38+ # Security: Verify we're running as the user who owns HOME
39+ readonly EXPECTED_UID=$( stat -f%u " $HOME " 2> /dev/null || stat -c%u " $HOME " 2> /dev/null)
40+ if [[ $( id -u) != " $EXPECTED_UID " ]]; then
41+ echo " Error: HOME ownership mismatch. Possible privilege escalation attempt."
42+ exit 1
43+ fi
3944
4045# Colors for output
4146readonly RED=' \033[0;31m'
@@ -78,11 +83,31 @@ _generate_unique_id() {
7883}
7984
8085_init_staging_dir () {
86+ # Ensure staging dir doesn't exist as symlink (symlink attack prevention)
87+ if [[ -L " $RM_ASYNC_DIR " ]]; then
88+ _log_error " Staging directory is a symlink (potential attack): $RM_ASYNC_DIR "
89+ return 1
90+ fi
91+
8192 mkdir -p " $RM_ASYNC_DIR " || {
8293 _log_error " Failed to create staging directory: $RM_ASYNC_DIR "
8394 return 1
8495 }
85- chmod 700 " $RM_ASYNC_DIR " 2> /dev/null
96+
97+ # Verify directory ownership (must be current user)
98+ local dir_owner
99+ dir_owner=$( stat -f%u " $RM_ASYNC_DIR " 2> /dev/null || stat -c%u " $RM_ASYNC_DIR " 2> /dev/null)
100+
101+ if [[ " $dir_owner " != " $( id -u) " ]]; then
102+ _log_error " Staging directory owned by different user: $RM_ASYNC_DIR "
103+ return 1
104+ fi
105+
106+ # Set strict permissions (700 = rwx------)
107+ chmod 700 " $RM_ASYNC_DIR " 2> /dev/null || {
108+ _log_error " Failed to set permissions on staging directory"
109+ return 1
110+ }
86111}
87112
88113_validate_critical_paths () {
@@ -94,6 +119,13 @@ _validate_critical_paths() {
94119 return 1
95120 fi
96121
122+ # SECURITY: Reject if path is a symlink (symlink attack prevention)
123+ # We want to validate the link itself, not its target
124+ if [[ -L " $path " ]]; then
125+ _log_error " Path is a symlink (potential attack): $path "
126+ return 1
127+ fi
128+
97129 # Resolve to absolute path to prevent tricks
98130 local abs_path
99131 abs_path=$( cd " $( dirname " $path " ) " 2> /dev/null && pwd) || {
@@ -120,6 +152,12 @@ _validate_critical_paths() {
120152 return 1
121153 fi
122154
155+ # Safety: reject if path contains null bytes (shell injection attempt)
156+ if [[ " $abs_path " == * $' \0' * ]]; then
157+ _log_error " Invalid path (contains null bytes): $path "
158+ return 1
159+ fi
160+
123161 return 0
124162}
125163
0 commit comments