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

简介: 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

相关文章
|
29天前
|
Java
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
68 9
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
68 2
|
9天前
|
Java
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
68 34
|
21天前
|
Java
java 中 IO 流
Java中的IO流是用于处理输入输出操作的机制,主要包括字节流和字符流两大类。字节流以8位字节为单位处理数据,如FileInputStream和FileOutputStream;字符流以16位Unicode字符为单位,如FileReader和FileWriter。这些流提供了读写文件、网络传输等基本功能。
40 9
|
26天前
|
消息中间件 存储 Java
RocketMQ文件刷盘机制深度解析与Java模拟实现
【11月更文挑战第22天】在现代分布式系统中,消息队列(Message Queue, MQ)作为一种重要的中间件,扮演着连接不同服务、实现异步通信和消息解耦的关键角色。Apache RocketMQ作为一款高性能的分布式消息中间件,广泛应用于实时数据流处理、日志流处理等场景。为了保证消息的可靠性,RocketMQ引入了一种称为“刷盘”的机制,将消息从内存写入到磁盘中,确保消息持久化。本文将从底层原理、业务场景、概念、功能点等方面深入解析RocketMQ的文件刷盘机制,并使用Java模拟实现类似的功能。
40 3
|
29天前
|
Java 测试技术 Maven
Maven clean 提示文件 java.io.IOException
在使用Maven进行项目打包时,遇到了`Failed to delete`错误,尝试手动删除目标文件也失败,提示`java.io.IOException`。经过分析,发现问题是由于`sys-info.log`文件被其他进程占用。解决方法是关闭IDEA和相关Java进程,清理隐藏的Java进程后重新尝试Maven clean操作。最终问题得以解决。总结:遇到此类问题时,可以通过任务管理器清理相关进程或重启电脑来解决。
|
1月前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
61 2
|
19天前
|
SQL 安全 Java
Java 异常处理:筑牢程序稳定性的 “安全网”
本文深入探讨Java异常处理,涵盖异常的基础分类、处理机制及最佳实践。从`Error`与`Exception`的区分,到`try-catch-finally`和`throws`的运用,再到自定义异常的设计,全面解析如何有效管理程序中的异常情况,提升代码的健壮性和可维护性。通过实例代码,帮助开发者掌握异常处理技巧,确保程序稳定运行。
32 0
|
7月前
|
存储 Java 数据安全/隐私保护
从零开始学习 Java:简单易懂的入门指南之IO字符流(三十一)
从零开始学习 Java:简单易懂的入门指南之IO字符流(三十一)
|
2月前
|
Java 数据处理 开发者
揭秘Java IO流:字节流与字符流的神秘面纱!
揭秘Java IO流:字节流与字符流的神秘面纱!
39 1