Why I’m writing this
Since approximately half an year ago, I began to do some bug hunting stuff. During the past several months, I learned a lot and found some vulnerabilities in some popular software.
Due to my own background (several years of experience for being a system engineer), I payed more attention on problems caused by the OS-related issues. Like path sanitizing, improperly handled symbolic links and unintended system command invocations.
So I’m going to enumerate some common pitfalls which are often neglected by developers and hard to discover by code analysis tools.
Ruby has a bunch of File path related functions, but none of them is sufficient for sanitizing a user inputted path to a developer-expecting-safe path in the file system.
When dealing with file paths, developers usually want that all the files are
located inside a base directory, which could be a developer specified constant
Rails.root Rails applications.
 pry(main)> ROOT='/home/nyangawa' => "/home/nyangawa"
join is often used to concatenate paths without taking care of the trailing
slashes. But the user should be careful about the following conditions:
 pry(main)> File.join(ROOT, '../../etc/passwd') => "/home/nyangawa/../../etc/passwd"
Here the path string still begins with
/home/nyangawa while the actual file
it directs to is actually
So, before joining any user input with your
ROOT, be sure to sanitize the string
first and check the pattern
expand_path is also a method which I noticed to be used by many developers for
path concatenation as this method can “expand” the path and get rid of some
meaningless patterns like
File::join does not handle, and
finally returns the absolute path of the concatenated path in the file system.
 pry(main)> File.expand_path('dir/./././file', ROOT) => "/home/nyangawa/dir/file"
However, even if the input is checked and ensured that no
.. included. This
function can still be used to bring some surprises.
 pry(main)> File.expand_path('/etc/passwd', ROOT) => "/etc/passwd"  pry(main)> File.expand_path('~sys', ROOT) => "/dev"
Reading the document of
expand_path can help to understand the behavior.
So the general recommendation is, no matter what function you use to concatenate
user input to your base path, comparing the result to the base path with string
start_with? would be a good idea.
But is that enough?
The answer is probably no due to the existence of symbolic links. A symbolic links could be used to escape out of the base directory without showing any differences on its path with any other normal files.
 pry(main)> File.symlink('/etc/passwd', 'linkfile') => 0  pry(main)> File.read('linkfile') => "root:x:0:0:root:/root:/bin/bash\ndaemon..."
Therefore, before reading any file which might be controllable or partially controllable by external users, we should check the file first. And there are a couple of useful methods for this purpose.
File::realpath is a good example, it follows a symbolic link to its real
destination and returns the absolute path so that it could be checked against
our expected base directory.
 pry(main)> File.realpath('linkfile') => "/etc/passwd"
If you simply want to deny the usage of symbolic links to avoid any unexpected
File#lstat to check the file stats is my recommendation, if
it is a symbolic link, exit loudly.
 pry(main)> File.new('linkfile').lstat.symlink? => true
File#stat can’t recognize a link because it follows it to the
 pry(main)> File.stat('linkfile').symlink? => false
## File path glob ## Command Injections ## Neglected dangerous functions # How to find these bugs from existing code