Secure coding with Ruby from the perspective of a system engineer (1)

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.

Common pitfalls

Path sanitizing

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 or Rails.root Rails applications.

[1] 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:

[2] 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 /etc/passwd

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 ././././ which File::join does not handle, and finally returns the absolute path of the concatenated path in the file system.

[3] 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.

[4] pry(main)> File.expand_path('/etc/passwd', ROOT)
=> "/etc/passwd"

[5] 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 method like 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.

[1] pry(main)> File.symlink('/etc/passwd', 'linkfile')
=> 0
[2] pry(main)>'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.

[1] pry(main)> File.realpath('linkfile')
=> "/etc/passwd"


If you simply want to deny the usage of symbolic links to avoid any unexpected occasions. Using File#lstat to check the file stats is my recommendation, if it is a symbolic link, exit loudly.

[1] pry(main)>'linkfile').lstat.symlink?
=> true

Be noticed File#stat can’t recognize a link because it follows it to the directed file.

[1] pry(main)> File.stat('linkfile').symlink?
=> false


## File path glob
## Command Injections
## Neglected dangerous functions

# How to find these bugs from existing code