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 re-assumed 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.
...
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).
...
In this noncompliant code example, the file identified by the string filename
is opened, processed, closed, and then reopened for reading.:
Code Block | ||
---|---|---|
| ||
// Identify a file by its path String filename = // initializedInitialized Path file1 = Paths.get(filename); // Open the file for writing try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(file1)))) { // Write to file... } catch (Exception e) { System.out.println("Exception during file access" + e); } // 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 (Exception e) { System.out.println("Exception during file access" + e); } |
There is no guarantee that the file opened for reading is the same file that is 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
.
...
Code Block | ||
---|---|---|
| ||
// Identify a file by its path String filename = //initialization Initialization Path file1 = Paths.get(filename); // Open the file for writing try(BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(file1)))) { // Write to file } catch (Exception e) { System.out.println("Exception during file access" + e); } // ... // 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 (Exception e) { System.out.println("Exception during file access" + e); } |
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. This does not exclude the 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 as the file that was written:
Code Block | ||
---|---|---|
| ||
// Identify a file by its path String filename = // initializedInitialized 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 (Exception e){ System.out.println("Exception during file access" + e); } // 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 (Exception e){ System.out.println("Exception during file access" + e); } |
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 Also, there is a time-of-check-, time-of-use (TOCTOU) race condition occurs between when the time the file's attributes are first read and when the time the file is first opened. Likewise, there is another TOCTOU between 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 which that "uniquely identifies the file" [API 2011], as shown in this compliant solution:
Code Block | ||
---|---|---|
| ||
// Identify a file by its path String filename = // initializedInitialized 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 (Exception e) { System.out.println("Exception during file access" + e); } // 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 (Exception e) { System.out.println("Exception during file access" + e); } |
...
This solution is not perfect. Like the previous compliant solution, it has a TOCTOU race window between when between the time the file's attributes are first read and when the time the file is first opened. Likewise, there is another TOCTOU between 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 re-opening 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.
Code Block | ||
---|---|---|
| ||
// Identify a file by its path String filename = // initializedInitialized 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(); |
...