java安全编码指南之:文件IO操做

简介

对于文件的IO操做应该是咱们常常会使用到的,由于文件的复杂性,咱们在使用File操做的时候也有不少须要注意的地方,下面我一块儿来看看吧。java

建立文件的时候指定合适的权限

不论是在windows仍是linux,文件都有权限控制的概念,咱们能够设置文件的owner,还有文件的permission,若是文件权限没有控制好的话,恶意用户就有可能对咱们的文件进行恶意操做。linux

因此咱们在文件建立的时候就须要考虑到权限的问题。git

很遗憾的是,java并非以文件操做见长的,因此在JDK1.6以前,java的IO操做是很是弱的,基本的文件操做类,好比FileOutputStream和FileWriter并无权限的选项。github

Writer out = new FileWriter("file");

那么怎么处理呢?windows

在JDK1.6以前,咱们须要借助于一些本地方法来实现权限的修改功能。数组

在JDK1.6以后,java引入了NIO,能够经过NIO的一些特性来控制文件的权限功能。安全

咱们看一下Files工具类的createFile方法:网络

public static Path createFile(Path path, FileAttribute<?>... attrs)
        throws IOException
    {
        newByteChannel(path, DEFAULT_CREATE_OPTIONS, attrs).close();
        return path;
    }

其中FileAttribute就是文件的属性,咱们看一下怎么指定文件的权限:ide

public void createFileWithPermission() throws IOException {
        Set<PosixFilePermission> perms =
                PosixFilePermissions.fromString("rw-------");
        FileAttribute<Set<PosixFilePermission>> 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必定要是正常退出的,若是是非正常退出,文件不会被删除。

咱们看下面的例子:

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选项,能够保证文件在关闭以后就被删除:

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暴露出去,不然可能会修改底层的数组。

public CharBuffer getBuffer(){
         char[] dataArray = new char[10];
         return CharBuffer.wrap(dataArray);
    }

上面的例子暴露了CharBuffer,实际上也暴露了底层的char数组。

有两种方式对其进行改进:

public CharBuffer getBuffer1(){
        char[] dataArray = new char[10];
        return CharBuffer.wrap(dataArray).asReadOnlyBuffer();
    }

第一种方式就是将CharBuffer转换成为只读的。

第二种方式就是建立一个新的Buffer,切断Buffer和数组的联系:

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:

getOutputStream()
getInputStream()
getErrorStream()

若是是使用parent process的IO,那么在有些系统上面,这些buffer空间比较小,若是出现大量输入输出操做的话,就有可能被阻塞,甚至是死锁。

怎么办呢?咱们要作的就是将Process产生的IO进行处理,以防止Buffer的阻塞。

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的边界。

咱们考虑这样的一个问题:

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结束。

因此咱们须要先作返回值的判断,而后再进行转换:

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.

FileReader in;
char data;
while ((data = (char) in.read()) != -1) {
  // ...
}

write() 方法不要超出范围

在OutputStream中有一个很奇怪的方法,就是write,咱们看下write方法的定义:

public abstract void write(int b) throws IOException;

write接收一个int参数,可是实际上写入的是一个byte。

由于int和byte的范围不同,因此传入的int将会被截取最后的8位来转换成一个byte。

因此咱们在使用的时候必定要判断写入的范围:

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方法:

public int read(byte b[]) throws IOException

public int read(byte b[], int off, int len) throws IOException

若是咱们使用了这两种方法,那么必定要注意读取到的byte数组是否被填满,考虑下面的一个例子:

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填充满,那么咱们将会获得一个没有填充满的数组,那么咱们使用起来实际上是有问题的。

怎么正确的使用呢?

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的转换。

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() 直接进行小端到大端的转换。

public  int reverse(int i) {
        return Integer.reverseBytes(i);
    }

本文的代码:

learn-java-base-9-to-20/tree/master/security

本文已收录于 http://www.flydean.com/java-security-code-line-file-io/

最通俗的解读,最深入的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注个人公众号:「程序那些事」,懂技术,更懂你!

相关文章
相关标签/搜索