Many file-related security vulnerabilities result from a program accessing an unintended file object because . One frequent cause is that file names are only loosely-bound to underlying file objects. File names provide no information are uninformative regarding the nature of the file object itself. Furthermore, the binding of a file name to a file object is reassumed every re-evaluated each time the file name is used in an operation. This re-evaluation introduces a 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.
...
File identification is less of an issue if for applications that 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).
...
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) { // 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 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 can 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) { // 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 the Java API lacks any guarantee that the method isSameFile()
really actually checks that whether the files are the same file. The Java 7 API for isSameFile()
says:
...
That is, isSameFile()
may simply check that the paths to the two files are the same. The possibility that If the file at that path has had been replaced by a different file between the two open operations is not excluded, this would remain undetected.
Compliant Solution (Multiple Attributes)
This compliant solution checks the creation and last-modified times of the files to ensure increase the likelihood that the file opened for reading is the same file that was written:
...
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 Because the file is never closed, no the race condition is possiblecannot occur.
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(); } |
...
This noncompliant code example tries to ensure that the file it opens has 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) { // handle error } } |
...
This compliant solution uses the FileChannel.size()
method to obtain the file size. Since Because this method is applied only 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) { // handle error } } |
Applicability
Many Attackers frequently exploit file-related vulnerabilities are exploited to cause a program programs to access an unintended file. Proper file identification is necessary to prevent exploitation.
...