Skip to content

Commit ce2a438

Browse files
[DOC] Enhanced RDoc for FileUtils (#78)
Treats: ::rm ::rm_f ::rm_r ::rm_rf ::remove_entry_secure
1 parent d6d7e53 commit ce2a438

File tree

1 file changed

+142
-62
lines changed

1 file changed

+142
-62
lines changed

lib/fileutils.rb

Lines changed: 142 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,57 @@
101101
# files/directories. This equates to passing the <tt>:noop</tt> and
102102
# <tt>:verbose</tt> flags to methods in FileUtils.
103103
#
104+
# == Avoiding the TOCTTOU Vulnerability
105+
#
106+
# For certain methods that recursively remove entries,
107+
# there is a potential vulnerability called the
108+
# {Time-of-check to time-of-use}[https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use],
109+
# or TOCTTOU, vulnerability that can exist when:
110+
#
111+
# - An ancestor directory of the entry at the target path is world writable;
112+
# such directories include <tt>/tmp</tt>.
113+
# - The directory tree at the target path includes:
114+
#
115+
# - A world-writable descendant directory.
116+
# - A symbolic link.
117+
#
118+
# To avoid that vulnerability, you can use this method to remove entries:
119+
#
120+
# - FileUtils.remove_entry_secure: removes recursively
121+
# if the target path points to a directory.
122+
#
123+
# Also available are these methods,
124+
# each of which calls \FileUtils.remove_entry_secure:
125+
#
126+
# - FileUtils.rm_r with keyword argument <tt>secure: true</tt>.
127+
# - FileUtils.rm_rf with keyword argument <tt>secure: true</tt>.
128+
#
129+
# Finally, this method for moving entries calls \FileUtils.remove_entry_secure
130+
# if the source and destination are on different devices
131+
# (which means that the "move" is really a copy and remove):
132+
#
133+
# - FileUtils.mv with keyword argument <tt>secure: true</tt>.
134+
#
135+
# \Method \FileUtils.remove_entry_secure removes securely
136+
# by applying a special pre-process:
137+
#
138+
# - If the target path points to a directory, this method uses
139+
# {chown(2)}[https://man7.org/linux/man-pages/man2/chown.2.html]
140+
# and {chmod(2)}[https://man7.org/linux/man-pages/man2/chmod.2.html]
141+
# in removing directories.
142+
# - The owner of the target directory should be either the current process
143+
# or the super user (root).
144+
#
145+
# WARNING: You must ensure that *ALL* parent directories cannot be
146+
# moved by other untrusted users. For example, parent directories
147+
# should not be owned by untrusted users, and should not be world
148+
# writable except when the sticky bit is set.
149+
#
150+
# For details of this security vulnerability, see Perl cases:
151+
#
152+
# - {CVE-2005-0448}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448].
153+
# - {CVE-2004-0452}[https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452].
154+
#
104155
module FileUtils
105156
VERSION = "1.6.0"
106157

@@ -197,7 +248,7 @@ def remove_trailing_slash(dir) #:nodoc:
197248
#
198249
# Creates directories at the paths in the given +list+
199250
# (an array of strings or a single string);
200-
# returns +list+.
251+
# returns +list+ if it is an array, <tt>[list]</tt> otherwise.
201252
#
202253
# With no keyword arguments, creates a directory at each +path+ in +list+
203254
# by calling: <tt>Dir.mkdir(path, mode)</tt>;
@@ -239,7 +290,7 @@ def mkdir(list, mode: nil, noop: nil, verbose: nil)
239290
# Creates directories at the paths in the given +list+
240291
# (an array of strings or a single string),
241292
# also creating ancestor directories as needed;
242-
# returns +list+.
293+
# returns +list+ if it is an array, <tt>[list]</tt> otherwise.
243294
#
244295
# With no keyword arguments, creates a directory at each +path+ in +list+,
245296
# along with any needed ancestor directories,
@@ -311,7 +362,7 @@ def fu_mkdir(path, mode) #:nodoc:
311362
#
312363
# Removes directories at the paths in the given +list+
313364
# (an array of strings or a single string);
314-
# returns +list+.
365+
# returns +list+, if it is an array, <tt>[list]</tt> otherwise.
315366
#
316367
# With no keyword arguments, removes the directory at each +path+ in +list+,
317368
# by calling: <tt>Dir.rmdir(path)</tt>;
@@ -865,6 +916,10 @@ def copy_stream(src, dest)
865916
# If +src+ and +dest+ are on different devices,
866917
# first copies, then removes +src+.
867918
#
919+
# May cause a local vulnerability if not called with keyword argument
920+
# <tt>secure: true</tt>;
921+
# see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
922+
#
868923
# If +src+ is the path to a single file or directory and +dest+ does not exist,
869924
# moves +src+ to +dest+:
870925
#
@@ -898,13 +953,14 @@ def copy_stream(src, dest)
898953
# | `-- src.txt
899954
# `-- src1.txt
900955
#
901-
# - <tt>force: true</tt> - attempts to force the move;
902-
# if the move includes removing +src+
956+
# Keyword arguments:
957+
#
958+
# - <tt>force: true</tt> - if the move includes removing +src+
903959
# (that is, if +src+ and +dest+ are on different devices),
904960
# ignores raised exceptions of StandardError and its descendants.
905961
# - <tt>noop: true</tt> - does not move files.
906-
# - <tt>secure: true</tt> - removes +src+ securely
907-
# by calling FileUtils.remove_entry_secure.
962+
# - <tt>secure: true</tt> - removes +src+ securely;
963+
# see details at FileUtils.remove_entry_secure.
908964
# - <tt>verbose: true</tt> - prints an equivalent command:
909965
#
910966
# FileUtils.mv('src0', 'dest0', noop: true, verbose: true)
@@ -949,13 +1005,29 @@ def mv(src, dest, force: nil, noop: nil, verbose: nil, secure: nil)
9491005
alias move mv
9501006
module_function :move
9511007

1008+
# Removes entries at the paths in the given +list+
1009+
# (an array of strings or a single string);
1010+
# returns +list+, if it is an array, <tt>[list]</tt> otherwise.
1011+
#
1012+
# With no keyword arguments, removes files at the paths given in +list+:
1013+
#
1014+
# FileUtils.touch(['src0.txt', 'src0.dat'])
1015+
# FileUtils.rm(['src0.dat', 'src0.txt']) # => ["src0.dat", "src0.txt"]
1016+
#
1017+
# Keyword arguments:
1018+
#
1019+
# - <tt>force: true</tt> - ignores raised exceptions of StandardError
1020+
# and its descendants.
1021+
# - <tt>noop: true</tt> - does not remove files; returns +nil+.
1022+
# - <tt>verbose: true</tt> - prints an equivalent command:
1023+
#
1024+
# FileUtils.rm(['src0.dat', 'src0.txt'], noop: true, verbose: true)
1025+
#
1026+
# Output:
9521027
#
953-
# Remove file(s) specified in +list+. This method cannot remove directories.
954-
# All StandardErrors are ignored when the :force option is set.
1028+
# rm src0.dat src0.txt
9551029
#
956-
# FileUtils.rm %w( junk.txt dust.txt )
957-
# FileUtils.rm Dir.glob('*.so')
958-
# FileUtils.rm 'NotExistFile', force: true # never raises exception
1030+
# FileUtils.remove is an alias for FileUtils.rm.
9591031
#
9601032
def rm(list, force: nil, noop: nil, verbose: nil)
9611033
list = fu_list(list)
@@ -971,10 +1043,13 @@ def rm(list, force: nil, noop: nil, verbose: nil)
9711043
alias remove rm
9721044
module_function :remove
9731045

1046+
# Equivalent to:
9741047
#
975-
# Equivalent to
1048+
# FileUtils.rm(list, force: true, **kwargs)
9761049
#
977-
# FileUtils.rm(list, force: true)
1050+
# See FileUtils.rm for keyword arguments.
1051+
#
1052+
# FileUtils.safe_unlink is an alias for FileUtils.rm_f.
9781053
#
9791054
def rm_f(list, noop: nil, verbose: nil)
9801055
rm list, force: true, noop: noop, verbose: verbose
@@ -984,24 +1059,50 @@ def rm_f(list, noop: nil, verbose: nil)
9841059
alias safe_unlink rm_f
9851060
module_function :safe_unlink
9861061

1062+
# Removes entries at the paths in the given +list+
1063+
# (an array of strings or a single string);
1064+
# returns +list+, if it is an array, <tt>[list]</tt> otherwise.
9871065
#
988-
# remove files +list+[0] +list+[1]... If +list+[n] is a directory,
989-
# removes its all contents recursively. This method ignores
990-
# StandardError when :force option is set.
1066+
# May cause a local vulnerability if not called with keyword argument
1067+
# <tt>secure: true</tt>;
1068+
# see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
9911069
#
992-
# FileUtils.rm_r Dir.glob('/tmp/*')
993-
# FileUtils.rm_r 'some_dir', force: true
1070+
# For each file path, removes the file at that path:
9941071
#
995-
# WARNING: This method causes local vulnerability
996-
# if one of parent directories or removing directory tree are world
997-
# writable (including /tmp, whose permission is 1777), and the current
998-
# process has strong privilege such as Unix super user (root), and the
999-
# system has symbolic link. For secure removing, read the documentation
1000-
# of remove_entry_secure carefully, and set :secure option to true.
1001-
# Default is <tt>secure: false</tt>.
1072+
# FileUtils.touch(['src0.txt', 'src0.dat'])
1073+
# FileUtils.rm_r(['src0.dat', 'src0.txt'])
1074+
# File.exist?('src0.txt') # => false
1075+
# File.exist?('src0.dat') # => false
10021076
#
1003-
# NOTE: This method calls remove_entry_secure if :secure option is set.
1004-
# See also remove_entry_secure.
1077+
# For each directory path, recursively removes files and directories:
1078+
#
1079+
# system('tree --charset=ascii src1')
1080+
# src1
1081+
# |-- dir0
1082+
# | |-- src0.txt
1083+
# | `-- src1.txt
1084+
# `-- dir1
1085+
# |-- src2.txt
1086+
# `-- src3.txt
1087+
# FileUtils.rm_r('src1')
1088+
# File.exist?('src1') # => false
1089+
#
1090+
# Keyword arguments:
1091+
#
1092+
# - <tt>force: true</tt> - ignores raised exceptions of StandardError
1093+
# and its descendants.
1094+
# - <tt>noop: true</tt> - does not remove entries; returns +nil+.
1095+
# - <tt>secure: true</tt> - removes +src+ securely;
1096+
# see details at FileUtils.remove_entry_secure.
1097+
# - <tt>verbose: true</tt> - prints an equivalent command:
1098+
#
1099+
# FileUtils.rm_r(['src0.dat', 'src0.txt'], noop: true, verbose: true)
1100+
# FileUtils.rm_r('src1', noop: true, verbose: true)
1101+
#
1102+
# Output:
1103+
#
1104+
# rm -r src0.dat src0.txt
1105+
# rm -r src1
10051106
#
10061107
def rm_r(list, force: nil, noop: nil, verbose: nil, secure: nil)
10071108
list = fu_list(list)
@@ -1017,13 +1118,17 @@ def rm_r(list, force: nil, noop: nil, verbose: nil, secure: nil)
10171118
end
10181119
module_function :rm_r
10191120

1121+
# Equivalent to:
1122+
#
1123+
# FileUtils.rm_r(list, force: true, **kwargs)
10201124
#
1021-
# Equivalent to
1125+
# May cause a local vulnerability if not called with keyword argument
1126+
# <tt>secure: true</tt>;
1127+
# see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
10221128
#
1023-
# FileUtils.rm_r(list, force: true)
1129+
# See FileUtils.rm_r for keyword arguments.
10241130
#
1025-
# WARNING: This method causes local vulnerability.
1026-
# Read the documentation of rm_r first.
1131+
# FileUtils.rmtree is an alias for FileUtils.rm_rf.
10271132
#
10281133
def rm_rf(list, noop: nil, verbose: nil, secure: nil)
10291134
rm_r list, force: true, noop: noop, verbose: verbose, secure: secure
@@ -1033,37 +1138,12 @@ def rm_rf(list, noop: nil, verbose: nil, secure: nil)
10331138
alias rmtree rm_rf
10341139
module_function :rmtree
10351140

1141+
# Securely removes the entry given by +path+,
1142+
# which should be the entry for a regular file, a symbolic link,
1143+
# or a directory.
10361144
#
1037-
# This method removes a file system entry +path+. +path+ shall be a
1038-
# regular file, a directory, or something. If +path+ is a directory,
1039-
# remove it recursively. This method is required to avoid TOCTTOU
1040-
# (time-of-check-to-time-of-use) local security vulnerability of rm_r.
1041-
# #rm_r causes security hole when:
1042-
#
1043-
# * Parent directory is world writable (including /tmp).
1044-
# * Removing directory tree includes world writable directory.
1045-
# * The system has symbolic link.
1046-
#
1047-
# To avoid this security hole, this method applies special preprocess.
1048-
# If +path+ is a directory, this method chown(2) and chmod(2) all
1049-
# removing directories. This requires the current process is the
1050-
# owner of the removing whole directory tree, or is the super user (root).
1051-
#
1052-
# WARNING: You must ensure that *ALL* parent directories cannot be
1053-
# moved by other untrusted users. For example, parent directories
1054-
# should not be owned by untrusted users, and should not be world
1055-
# writable except when the sticky bit set.
1056-
#
1057-
# WARNING: Only the owner of the removing directory tree, or Unix super
1058-
# user (root) should invoke this method. Otherwise this method does not
1059-
# work.
1060-
#
1061-
# For details of this security vulnerability, see Perl's case:
1062-
#
1063-
# * https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2005-0448
1064-
# * https://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2004-0452
1065-
#
1066-
# For fileutils.rb, this vulnerability is reported in [ruby-dev:26100].
1145+
# Avoids a local vulnerability that can exist in certain circumstances;
1146+
# see {Avoiding the TOCTTOU Vulnerability}[rdoc-ref:FileUtils@Avoiding+the+TOCTTOU+Vulnerability].
10671147
#
10681148
def remove_entry_secure(path, force = false)
10691149
unless fu_have_symlink?

0 commit comments

Comments
 (0)