Many file-related security vulnerabilities result from a program accessing an unintended file object. One frequent cause is that file names are only loosely - bound to underlying file objects. File names are uninformative regarding the nature of the file object itself. Furthermore, the binding of a file name to a file object is re-evaluated reevaluated each time the file name is used in an operation. This re-evaluation reevaluation introduces a time-of-check, time-of-use (TOCTOU) race condition with the file system. Objects of type java.io.File
and of type java.nio.file.Path
are bound to underlying file objects by the operating system.
...
Fortunately, files can often be identified by other attributes in addition to the file name, for 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.
...
Code Block | ||
---|---|---|
| ||
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) { // handleHandle 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) { // handleHandle error } } |
Because the binding between the file name and the underlying file object is reevaluated when the BufferedReader
is created, this code cannot guarantee that the file opened for reading is the same file that was previously opened for writing. An attacker could replace the original file (for example, with a symbolic link) between the first call to close()
and the subsequent creation of the BufferedReader
.
...
Code Block | ||
---|---|---|
| ||
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) { // handleHandle error } // ... // Reopen the file for reading Path file2 = Paths.get(filename); if (!Files.isSameFile(file1, file2)) { System.out.println("File tampered with"); // handleHandle 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) { // handleHandle error } } |
Unfortunately, the Java API lacks any guarantee that the method isSameFile()
actually checks whether the files are the same file. The Java 7 API for isSameFile()
[API 2011] says:
If both Path objects are equal then this method returns true without checking if the file exists.
...
Code Block | ||
---|---|---|
| ||
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) { // handleHandle 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"); // handleHandle 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) { // handleHandle 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.
...
Code Block | ||
---|---|---|
| ||
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) { // handleHandle 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) { // handleHandle 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.
...
Code Block | ||
---|---|---|
| ||
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 contains exactly 1024 bytes.
Code Block | ||||
---|---|---|---|---|
| ||||
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) { // handleHandle 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. Because this method is applied to the file only after it has been opened, this solution eliminates the race window.
Code Block | ||||
---|---|---|---|---|
| ||||
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) { // handleHandle error } } |
Applicability
Attackers frequently exploit file-related vulnerabilities to cause programs to access an unintended file. Proper file identification is necessary to prevent exploitation.
...
[API 2011] | Class java.io.File |
...