写点什么

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

发布于: 2020 年 10 月 27 日
java安全编码指南之:文件IO操作

简介

对于文件的 IO 操作应该是我们经常会使用到的,因为文件的复杂性,我们在使用 File 操作的时候也有很多需要注意的地方,下面我一起来看看吧。


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

不管是在 windows 还是 linux,文件都有权限控制的概念,我们可以设置文件的 owner,还有文件的 permission,如果文件权限没有控制好的话,恶意用户就有可能对我们的文件进行恶意操作。


所以我们在文件创建的时候就需要考虑到权限的问题。


很遗憾的是,java 并不是以文件操作见长的,所以在 JDK1.6 之前,java 的 IO 操作是非常弱的,基本的文件操作类,比如 FileOutputStream 和 FileWriter 并没有权限的选项。


Writer out = new FileWriter("file");
复制代码


那么怎么处理呢?


在 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 就是文件的属性,我们看一下怎么指定文件的权限:


    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/

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

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


发布于: 2020 年 10 月 27 日阅读数: 414
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
java安全编码指南之:文件IO操作