1. 程式人生 > >java安全編碼指南之:檔案IO操作

java安全編碼指南之:檔案IO操作

[toc] # 簡介 對於檔案的IO操作應該是我們經常會使用到的,因為檔案的複雜性,我們在使用File操作的時候也有很多需要注意的地方,下面我一起來看看吧。 # 建立檔案的時候指定合適的許可權 不管是在windows還是linux,檔案都有許可權控制的概念,我們可以設定檔案的owner,還有檔案的permission,如果檔案許可權沒有控制好的話,惡意使用者就有可能對我們的檔案進行惡意操作。 所以我們在檔案建立的時候就需要考慮到許可權的問題。 很遺憾的是,java並不是以檔案操作見長的,所以在JDK1.6之前,java的IO操作是非常弱的,基本的檔案操作類,比如FileOutputStream和FileWriter並沒有許可權的選項。 ~~~java Writer out = new FileWriter("file"); ~~~ 那麼怎麼處理呢? 在JDK1.6之前,我們需要藉助於一些本地方法來實現許可權的修改功能。 在JDK1.6之後,java引入了NIO,可以通過NIO的一些特性來控制檔案的許可權功能。 我們看一下Files工具類的createFile方法: ~~~java public static Path createFile(Path path, FileAttribute... attrs) throws IOException { newByteChannel(path, DEFAULT_CREATE_OPTIONS, attrs).close(); return path; } ~~~ 其中FileAttribute就是檔案的屬性,我們看一下怎麼指定檔案的許可權: ~~~java public void createFileWithPermission() throws IOException { Set perms = PosixFilePermissions.fromString("rw-------"); FileAttribute> attr = PosixFilePermissions.asFileAttribute(perms); Path file = new File("/tmp/www.flydean.com").toPath(); Files.createFile(file,attr); } ~~~ # 注意檢查檔案操作的返回值 java中很多檔案操作是有返回值的,比如file.delete(),我們需要根據返回值來判斷檔案操作是否完成,所以不要忽略了返回值。 # 刪除使用過後的臨時檔案 如果我們使用到不需要永久儲存的檔案時,就可以很方便的使用File的createTempFile來建立臨時檔案。臨時檔案的名字是隨機生成的,我們希望在臨時檔案使用完畢之後將其刪除。 怎麼刪除呢?File提供了一個deleteOnExit方法,這個方法會在JVM退出的時候將檔案刪除。 > 注意,這裡的JVM一定要是正常退出的,如果是非正常退出,檔案不會被刪除。 我們看下面的例子: ~~~java public void wrongDelete() throws IOException { File f = File.createTempFile("tmpfile",".tmp"); FileOutputStream fop = null; try { fop = new FileOutputStream(f); String str = "Data"; fop.write(str.getBytes()); fop.flush(); } finally { // 因為Stream沒有被關閉,所以檔案在windows平臺上面不會被刪除 f.deleteOnExit(); // 在JVM退出的時候刪除臨時檔案 if (fop != null) { try { fop.close(); } catch (IOException x) { // Handle error } } } } ~~~ 上面的例子中,我們建立了一個臨時檔案,並且在finally中呼叫了deleteOnExit方法,但是因為在呼叫該方法的時候,Stream並沒有關閉,所以在windows平臺上會出現檔案沒有被刪除的情況。 怎麼解決呢? NIO提供了一個DELETE_ON_CLOSE選項,可以保證檔案在關閉之後就被刪除: ~~~java public void correctDelete() throws IOException { Path tempFile = null; tempFile = Files.createTempFile("tmpfile", ".tmp"); try (BufferedWriter writer = Files.newBufferedWriter(tempFile, Charset.forName("UTF8"), StandardOpenOption.DELETE_ON_CLOSE)) { // Write to file } } ~~~ 上面的例子中,我們在writer的建立過程中加入了StandardOpenOption.DELETE_ON_CLOSE,那麼檔案將會在writer關閉之後被刪除。 # 釋放不再被使用的資源 如果資源不再被使用了,我們需要記得關閉他們,否則就會造成資源的洩露。 但是很多時候我們可能會忘記關閉,那麼該怎麼辦呢?JDK7中引入了try-with-resources機制,只要把實現了Closeable介面的資源放在try語句中就會自動被關閉,很方便。 # 注意Buffer的安全性 NIO中提供了很多非常有用的Buffer類,比如IntBuffer, CharBuffer 和 ByteBuffer等,這些Buffer實際上是對底層的陣列的封裝,雖然建立了新的Buffer物件,但是這個Buffer是和底層的陣列相關聯的,所以不要輕易的將Buffer暴露出去,否則可能會修改底層的陣列。 ~~~java public CharBuffer getBuffer(){ char[] dataArray = new char[10]; return CharBuffer.wrap(dataArray); } ~~~ 上面的例子暴露了CharBuffer,實際上也暴露了底層的char陣列。 有兩種方式對其進行改進: ~~~java public CharBuffer getBuffer1(){ char[] dataArray = new char[10]; return CharBuffer.wrap(dataArray).asReadOnlyBuffer(); } ~~~ 第一種方式就是將CharBuffer轉換成為只讀的。 第二種方式就是建立一個新的Buffer,切斷Buffer和陣列的聯絡: ~~~java public CharBuffer getBuffer2(){ char[] dataArray = new char[10]; CharBuffer cb = CharBuffer.allocate(dataArray.length); cb.put(dataArray); return cb; } ~~~ # 注意 Process 的標準輸入輸出 java中可以通過Runtime.exec()來執行native的命令,而Runtime.exec()是有返回值的,它的返回值是一個Process物件,用來控制和獲取native程式的執行資訊。 預設情況下,創建出來的Process是沒有自己的I/O stream的,這就意味著Process使用的是父process的I/O(stdin, stdout, stderr),Process提供了下面的三種方法來獲取I/O: ~~~java getOutputStream() getInputStream() getErrorStream() ~~~ 如果是使用parent process的IO,那麼在有些系統上面,這些buffer空間比較小,如果出現大量輸入輸出操作的話,就有可能被阻塞,甚至是死鎖。 怎麼辦呢?我們要做的就是將Process產生的IO進行處理,以防止Buffer的阻塞。 ~~~java public class StreamProcesser implements Runnable{ private final InputStream is; private final PrintStream os; StreamProcesser(InputStream is, PrintStream os){ this.is=is; this.os=os; } @Override public void run() { try { int c; while ((c = is.read()) != -1) os.print((char) c); } catch (IOException x) { // Handle error } } public static void main(String[] args) throws IOException, InterruptedException { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("vscode"); Thread errorGobbler = new Thread(new StreamProcesser(proc.getErrorStream(), System.err)); Thread outputGobbler = new Thread(new StreamProcesser(proc.getInputStream(), System.out)); errorGobbler.start(); outputGobbler.start(); int exitVal = proc.waitFor(); errorGobbler.join(); outputGobbler.join(); } } ~~~ 上面的例子中,我們建立了一個StreamProcesser來處理Process的Error和Input。 # InputStream.read() 和 Reader.read() InputStream和Reader都有一個read()方法,這兩個方法的不同之處就是InputStream read的是Byte,而Reader read的是char。 雖然Byte的範圍是-128到127,但是InputStream.read()會將讀取到的Byte轉換成0-255(0x00-0xff)範圍的int。 Char的範圍是0x0000-0xffff,Reader.read()將會返回同樣範圍的int值:0x0000-0xffff。 如果返回值是-1,表示的是Stream結束了。這裡-1的int表示是:0xffffffff。 我們在使用的過程中,需要對讀取的返回值進行判斷,以用來區分Stream的邊界。 我們考慮這樣的一個問題: ~~~java FileInputStream in; byte data; while ((data = (byte) in.read()) != -1) { } ~~~ 上面我們將InputStream的read結果先進行byte的轉換,然後再判斷是否等於-1。會有什麼問題呢? 如果Byte本身的值是0xff,本身是一個-1,但是InputStream在讀取之後,將其轉換成為0-255範圍的int,那麼轉換之後的int值是:0x000000FF, 再次進行byte轉換,將會擷取最後的Oxff, Oxff == -1,最終導致錯誤的判斷Stream結束。 所以我們需要先做返回值的判斷,然後再進行轉換: ~~~java FileInputStream in; int inbuff; byte data; while ((inbuff = in.read()) != -1) { data = (byte) inbuff; // ... } ~~~ > 拓展閱讀: > > 這段程式碼的輸出結果是多少呢? (int)(char)(byte)-1 > > 首先-1轉換成為byte:-1是0xffffffff,轉換成為byte直接擷取最後幾位,得到0xff,也就是-1. > > 然後byte轉換成為char:0xff byte是有符號的,轉換成為2個位元組的char需要進行符號位擴充套件,變成0xffff,但是char是無符號的,對應的十進位制是65535。 > > 最後char轉換成為int,因為char是無符號的,所以擴充套件成為0x0000ffff,對應的十進位制數是65535. 同樣的下面的例子中,如果提前使用char對int進行轉換,因為char的範圍是無符號的,所以永遠不可能等於-1. ~~~java FileReader in; char data; while ((data = (char) in.read()) != -1) { // ... } ~~~ # write() 方法不要超出範圍 在OutputStream中有一個很奇怪的方法,就是write,我們看下write方法的定義: ~~~java public abstract void write(int b) throws IOException; ~~~ write接收一個int引數,但是實際上寫入的是一個byte。 因為int和byte的範圍不一樣,所以傳入的int將會被擷取最後的8位來轉換成一個byte。 所以我們在使用的時候一定要判斷寫入的範圍: ~~~java public void writeInt(int value){ int intValue = Integer.valueOf(value); if (intValue < 0 || intValue > 255) { throw new ArithmeticException("Value超出範圍"); } System.out.write(value); System.out.flush(); } ~~~ 或者有些Stream操作是可以直接writeInt的,我們可以直接呼叫。 # 注意帶陣列的read的使用 InputStream有兩種帶陣列的read方法: ~~~java public int read(byte b[]) throws IOException ~~~ 和 ~~~java public int read(byte b[], int off, int len) throws IOException ~~~ 如果我們使用了這兩種方法,那麼一定要注意讀取到的byte陣列是否被填滿,考慮下面的一個例子: ~~~java public String wrongRead(InputStream in) throws IOException { byte[] data = new byte[1024]; if (in.read(data) == -1) { throw new EOFException(); } return new String(data, "UTF-8"); } ~~~ 如果InputStream的資料並沒有1024,或者說因為網路的原因並沒有將1024填充滿,那麼我們將會得到一個沒有填充滿的陣列,那麼我們使用起來其實是有問題的。 怎麼正確的使用呢? ~~~java public String readArray(InputStream in) throws IOException { int offset = 0; int bytesRead = 0; byte[] data = new byte[1024]; while ((bytesRead = in.read(data, offset, data.length - offset)) != -1) { offset += bytesRead; if (offset >= data.length) { break; } } String str = new String(data, 0, offset, "UTF-8"); return str; } ~~~ 我們需要記錄實際讀取的byte數目,通過記載偏移量,我們得到了最終實際讀取的結果。 或者我們可以使用DataInputStream的readFully方法,保證讀取完整的byte陣列。 # little-endian和big-endian的問題 java中的資料預設是以big-endian的方式來儲存的,DataInputStream中的readByte(), readShort(), readInt(), readLong(), readFloat(), 和 readDouble()預設也是以big-endian來讀取資料的,如果在和其他的以little-endian進行互動的過程中,就可能出現問題。 我們需要的是將little-endian轉換成為big-endian。 怎麼轉換呢? 比如,我們想要讀取一個int,可以首先使用read方法讀取4個位元組,然後再對讀取的4個位元組做little-endian到big-endian的轉換。 ~~~java public void method1(InputStream inputStream) throws IOException { try(DataInputStream dis = new DataInputStream(inputStream)) { byte[] buffer = new byte[4]; int bytesRead = dis.read(buffer); // Bytes are read into buffer if (bytesRead != 4) { throw new IOException("Unexpected End of Stream"); } int serialNumber = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt(); } } ~~~ 上面的例子中,我們使用了ByteBuffer提供的wrap和order方法來對Byte陣列進行轉換。 當然我們也可以自己手動進行轉換。 還有一個最簡單的方法,就是呼叫JDK1.5之後的reverseBytes() 直接進行小端到大端的轉換。 ~~~java public int reverse(int i) { return Integer.reverseBytes(i); } ~~~ 本文的程式碼: [learn-java-base-9-to-20/tree/master/security](https://github.com/ddean2009/learn-java-base-9-to-20/tree/master/security) > 本文已收錄於 [http://www.flydean.com/java-security-code-line-file-io/ ](http://www.flydean.com/java-security-code-line-file-io/ ) > > 最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現! > > 歡迎關注我的公眾號:「程式那些事」,懂技術,更