March 28, 2018·4 min read·engineeringrubysecurityfilesystem

Ruby temporary-name prefixes could escape their directory

CVE-2018-6914 lets externally supplied temporary-name prefixes alter the creation path. Ruby's correction removes path separators from name components while preserving the separate parent-directory argument.

A temporary filename prefix should help identify a temporary object without deciding which directory contains it. Ruby's tmpdir library allowed that distinction to fail. A prefix containing relative directory components could change the path where the library attempted to create its supposedly temporary object.

The March 28 advisory for CVE-2018-6914 identifies unintended directory creation in tmpdir and unintended file creation in tempfile. Tempfile is affected because it uses tmpdir internally. The vulnerable input is the prefix supplied by an application, with successful creation also depending on the process's privileges and filesystem permissions.

This is a narrower condition than saying every temporary-file operation can write anywhere. An attacker needs influence over the relevant input. The Ruby process must also be allowed to create the target object. Those conditions determine the authority exposed by the path-handling defect.

I find Ruby's concise resource-management APIs useful, especially when a block clearly defines an object's lifetime. That convenience should not make a naming argument carry directory-selection authority. The correction preserves the distinction between the temporary object's name and its parent directory.

The first argument names the temporary object

The Ruby 2.5.1 tmpdir implementation documents the interface precisely. Dir.mktmpdir accepts a prefix, or a pair containing a prefix and suffix, as its first argument. A separate optional argument specifies the directory under which the temporary directory is created.

Those are different decisions. A prefix can make a temporary directory recognizable as belonging to a particular operation. A parent-directory argument chooses the location in which creation occurs. An application can reasonably wish to control the second while accepting descriptive text for the first.

The advisory says prefixes could contain relative directory specifiers such as ../. Interpreting those characters as path structure gives a naming component the ability to traverse toward another directory. The generated uniqueness portion of the name does not remove the meaning of the traversal components.

Randomness and containment therefore solve different problems. A generated component can help avoid collisions with existing names. It does not establish that the complete path remains beneath an intended parent. The implementation needs to control how the prefix participates in the path.

This also explains tempfile's exposure. The vulnerable naming mechanism is shared. An application can reach it while requesting a temporary file even if its own code never calls Dir.mktmpdir directly. Reviewing only direct tmpdir calls would miss that dependency.

The corrected implementation removes separators

The tagged implementation converts the prefix to a string and removes both the platform's primary file separator and its alternate separator. It applies the same operation to an optional suffix. The resulting name components no longer contain those path separators when the temporary name is assembled.

After preparing the components, the helper builds a name containing the date, process identifier and random component. It joins that name with the selected temporary directory. If creation reports that the path already exists, it can retry with another generated name.

The separator removal belongs before the join. It changes which parts of the supplied input can act as path syntax. A component containing dots without a separator cannot express the same sequence of parent-directory path elements described in the advisory.

This is a library-level correction to interpretation. An application does not need to predict every hostile spelling to benefit from the updated name handling. The library can enforce that a name component remains a name component in the operation it provides.

The parent-directory parameter still exists. The fix does not turn an untrusted parent directory into a trusted one. Application code choosing that argument still chooses where creation takes place. The correction narrows the meaning of prefix and suffix arguments.

Permissions limit what the process can create

The advisory explicitly connects impact to inappropriate target permissions or excessive process privileges. That qualification matters because path traversal does not grant a process new operating-system permissions. It can redirect an operation into a location the process can already access.

A service with broad write access may therefore expose more possible destinations than a process confined to a smaller writable area. The defect is the path-selection error, while the process's existing authority determines where the redirected creation can succeed.

The documented mktmpdir behavior creates its directory with permission 0700. That controls access to the newly created object. It does not establish that the object was created in the intended location. Restrictive object permissions and correct path containment are independent requirements.

My preference is to keep those properties separate in code review. Seeing a private mode on a newly created directory is useful evidence about access. It is not evidence that external text could not influence its parent path. CVE-2018-6914 concerns that second property.

Block cleanup addresses lifetime

The block form of mktmpdir yields the directory path and removes the directory and its contents when the block finishes. This example uses a fixed application-controlled prefix.

require "tmpdir"

Dir.mktmpdir("report-") do |directory|
  puts directory
end

The tagged source also checks the parent directory before block cleanup, rejecting a world-writable parent without the sticky bit. That is another safety condition in the lifecycle. It does not replace the prefix correction because creation and cleanup are different operations.

Without a block, the method returns the path and leaves removal to the caller. The interface therefore has explicit lifetime behavior alongside its naming behavior. An application choosing the non-block form takes on the cleanup responsibility documented by the method.

The Ruby 2.5.1 release announcement lists CVE-2018-6914 among its security corrections. The advisory identifies Ruby 2.5.0 and earlier in that series as affected, alongside older maintained series and the first Ruby 2.6 preview. For the older branches, the advisory names Ruby 2.3.6 and earlier and Ruby 2.4.3 and earlier. It also includes Ruby 2.2.9 and earlier in the affected range. Updating the relevant interpreter supplies the corrected library behavior. The practical distinction remains that temporary-name components describe the object, while the separate parent argument controls its location.