You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 32 Next »

Many file-related security vulnerabilities result from a program accessing an unintended file object because file names are only loosely bound to underlying file objects. File names provide no information regarding the nature of the file object itself. Furthermore, the binding of a file name to a file object is reassumed every time the file name is used in an operation. Objects of type java.io.File and of type java.nio.file.Path are bound to underlying file objects by the operating system.

The java.io.File constructors and the java.io.File methods renameTo() and delete() rely solely on file names for file identification. The same holds for the java.nio.file.Path.get() methods for creating Path objects and the move and delete methods of java.nio.file.Files. Use all of these methods with caution.

Fortunately, files can often be identified by other attributes in addition to the file name, for example, by comparing file creation time or modification times. Information about a file that has been created and closed can be stored and then used to validate the identity of the file when it is reopened.

Comparing multiple attributes of the file increases the likelihood that the reopened file is the same file that was previously operated on.

File identification is less of an issue if applications maintain their files in secure directories where they can be accessed only by the owner of the file and (possibly) by a system administrator (see FIO00-J. Do not operate on files in shared directories).

Noncompliant Code Example

In this noncompliant code example, the file identified by the string filename is opened, processed, closed, and then reopened for reading:

public void processFile_nce(String filename){
  // Identify a file by its path
  Path file1 = Paths.get(filename);
 
 
  // Open the file for writing
  try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(file1)))) {
      // Write to file...
  } catch (IOException e) {
    // handle error
  } 
  
  // Close the file
  
  /*
   * A race condition here allows for an attacker to switch
   * out the file for another
   */

  // Reopen the file for reading
  Path file2 = Paths.get(filename);
    
    
  try(BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(file2)))){
      String line;
      while ((line = br.readLine()) != null) {
          System.out.println(line);
      }
  } catch (IOException e) {
    // handle error
  } 
}

There is no guarantee that the file opened for reading is the same file that was opened for writing. An attacker can replace the original file (for example, with a symbolic link) between the first call to close() and the subsequent creation of the BufferedReader.

Noncompliant Code Example (isSameFile())

In this noncompliant code example, the programmer attempts to ensure that the file opened for reading is the same as the file previously opened for writing by calling the method isSameFile():

public void sameFile_nce(String filename){
  // Identify a file by its path
  Path file1 = Paths.get(filename);
  
  // Open the file for writing
  try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(file1)))) {
    // Write to file
  } catch (IOException e) {
    // handle error
  } 
  
  // ...
  // Reopen the file for reading
  Path file2 = Paths.get(filename);
  if (!Files.isSameFile(file1, file2)) {
    System.out.println("File tampered with");
    // handle error
  }
  
  try(BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(file2)))) { 
    String line;
    while ((line = br.readLine()) != null) {
      System.out.println(line);
    }
  } catch (IOException e) {
    // handle error
  } 
}

Unfortunately, there is no guarantee that the method isSameFile() really checks that the files are the same file. The Java 7 API for isSameFile() says:

If both Path objects are equal then this method returns true without checking if the file exists.

That is, isSameFile() may simply check that the paths to the two files are the same. The possibility that the file at that path has been replaced by a different file between the two open operations is not excluded.

Compliant Solution (Multiple Attributes)

This compliant solution checks the creation and last-modified times of the files to ensure that the file opened for reading is the same file that was written:

public void sameFile_cs(String filename) throws IOException{
  // Identify a file by its path
  Path file1 = Paths.get(filename);
  BasicFileAttributes attr1 = Files.readAttributes(file1, BasicFileAttributes.class);
  FileTime creation1 = attr1.creationTime();
  FileTime modified1 = attr1.lastModifiedTime();

  // Open the file for writing
  try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(file1)))) {
    // Write to file...
  } catch (IOException e) {
    // handle error
  } 
  
  // Reopen the file for reading
  Path file2 = Paths.get(filename);
  BasicFileAttributes attr2 = Files.readAttributes(file2, BasicFileAttributes.class);
  FileTime creation2 = attr2.creationTime();
  FileTime modified2 = attr2.lastModifiedTime();
  if ( (!creation1.equals(creation2)) || (!modified1.equals(modified2)) ) {
    System.out.println("File tampered with");
    // handle error
  }
  
  try(BufferedReader br = new BufferedReader( new InputStreamReader(Files.newInputStream(file2)))){
    String line;
    while ((line = br.readLine()) != null) {
      System.out.println(line);
    }
  } catch (IOException e) {
    // handle error
  } 
}

Although this solution is reasonably secure, a determined attacker could create a symbolic link with the same creation and last-modified times as the original file. Also, a time-of-check, time-of-use (TOCTOU) race condition occurs between the time the file's attributes are first read and the time the file is first opened. Likewise, a second TOCTOU condition occurs the second time the attributes are read and the file is reopened.

Compliant Solution (POSIX fileKey Attribute)

In environments that support the fileKey attribute, a more reliable approach is to check that the fileKey attributes of the two files are the same. The fileKey attribute is an object that "uniquely identifies the file" [API 2011], as shown in this compliant solution:

public void filekey_cs(String filename) throws IOException{
  // Identify a file by its path
  Path file1 = Paths.get(filename);
  BasicFileAttributes attr1 = Files.readAttributes(file1, BasicFileAttributes.class);
  Object key1 = attr1.fileKey();
  // Open the file for writing
  try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(file1)))) {
    // Write to file
  } catch (IOException e) {
    // handle error
  } 
  
  // Reopen the file for reading
  Path file2 = Paths.get(filename);
  BasicFileAttributes attr2 = Files.readAttributes(file2, BasicFileAttributes.class);
  Object key2 = attr2.fileKey();

  if ( !key1.equals(key2) ) {
    System.out.println("File tampered with");
    // handle error
  }

  try(BufferedReader br = new BufferedReader(new InputStreamReader(Files.newInputStream(file2)))) {
    String line;
    while ((line = br.readLine()) != null) {
      System.out.println(line);
    }
  } catch (IOException e) {
    // handle error
  } 
} 

This approach will not work on all platforms. For example, on an Intel Core i5-2400 machine running Windows 7 Enterprise, all fileKey attributes are null.

This solution is not perfect. Like the previous compliant solution, it has a TOCTOU race window between the time the file's attributes are first read and the time the file is first opened. A second TOCTOU condition occurs the second time the attributes are read and the file is reopened.

Compliant Solution (RandomAccessFile)

A better approach is to avoid reopening a file. The following compliant solution demonstrates use of a RandomAccessFile, which can be opened for both reading and writing. Since the file is never closed, no race condition is possible.

public void randomAccess_cs(String filename) throws IOException{
  // Identify a file by its path
  RandomAccessFile file = new RandomAccessFile( filename, "rw");

  // Write to file...

  // Go back to beginning and read contents
  file.seek(0);
  try {
    while (true) {
      String s = file.readUTF();
      System.out.print(s);
    }
  } catch (EOFException x) {
    // Ignore, this breaks out of while loop
  }
  br.close();
}

Noncompliant Code Example (file size)

This noncompliant code example tries to ensure that the file it opens has exactly 1024 bytes.

static long goodSize = 1024;

public void doSomethingWithFile(String filename) {
  long size = new File( filename).length();
  if (size != goodSize) {
    System.out.println("File is wrong size!");
    return;
  }

  try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream( filename)))) {
    // ... work with file
  } catch (IOException e) {
    // handle error
  } 
}

This code is subject to a (TOCTOU) race condition between when the file size is learned and when the file is opened. If an attacker replaces a 1024-byte file with another file during this race window, they can cause this program to open any file, defeating the check.

Compliant Solution (file size)

This compliant solution uses the FileChannel.size() method to obtain the file size. Since this method is applied only to the file after it has been opened, this solution eliminates the race window.

static long goodSize = 1024;

public void doSomethingWithFile(String filename) {
  try (FileInputStream in = new FileInputStream( filename);
    BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
    long size = in.getChannel().size();
    if (size != goodSize) {
      System.out.println("File is wrong size!");
      return;
    }

    String line;
    while ((line = br.readLine()) != null) {
      System.out.println(line);
    }
  } catch (IOException e) {
    // handle error
  } 
}

Applicability

Many file-related vulnerabilities are exploited to cause a program to access an unintended file. Proper file identification is necessary to prevent exploitation.

Bibliography

 


  • No labels