您的位置 首页 java

Java安全编码军规

规则1:对外部输入必须进行严格校验

【说明】

软件最为普遍的缺陷就是对来自客户端或者外部环境的数据没有进行正确的合法性校验。这种缺陷可以导致几乎所有的程序弱点,例如Dos、命令注入、SQL注入、数据破坏、文件系统攻击等。这些不可信数据可能来自:

l 用户输入

l 外部调用的参数

l 进程间的通信数据

l 网络连接(甚至是一个安全的连接)

l 上层应用(业务)输入

l 普通用户可修改的配置文件

l 外部输入的文件

l 环境变量

针对外部输入的校验,必须是做在服务端,校验的方式可以有白名单、黑名单两种方式。推荐使用白名单的方式,黑名单的方式取决于特殊字符排查是否彻底,同时随着时间的演进,还可能增加新的特殊字符,因此使用白名单校验是更加简单、安全的。

规则2-8中详细阐述了几种外部输入校验的场景。

规则2:禁止使用外部数据拼接SQL语句

【说明】

SQL注入 是指原始SQL查询被恶意动态更改成一个与程序预期完全不同的查询。执行更改后的查询可能会导致信息泄露或者数据被篡改。防止 SQL 注入的方式主要分为两类:

l 使用参数化查询

l 对不可信数据进行校验

参数化查询是一种简单有效的防止SQL注入的查询方式,应该被优先考虑使用。

【错误代码】

Statement stmt = null ;

ResultSet rs = null ;

try

{

String userName = ctx.getAuthenticatedUserName(); //this is a constant

String sqlString = “SELECT * FROM t_item WHERE owner='” + userName + “‘ AND itemName='” + request.getParameter( “itemName” ) + “‘” ;

stmt = connection.createStatement();

rs = stmt.executeQuery(sqlString);

// … result set handling

}

catch (SQLException se)

{

// … logging and error handling

}

上例中查询字符串常量与用户输入将拼接成动态SQL查询命令。仅当itemName不包含单引号时,这条查询语句的行为才会和预期是一致的。如果一个攻击者提交的userName为wiley,同时itemName为:

name’ OR ‘a’ = ‘a

那么这个查询将变成:

SELECT * FROM t_item WHERE owner = ‘wiley’ AND itemname = ‘name’ OR ‘a’=’a’;

这里的OR ‘a’=’a’条件会导致整个WHERE子句的值总为真。所以这个查询便等价于如下非常简单的查询:

SELECT * FROM t_item

此查询使得攻击者能够绕过原有的条件限制,返回items表中所有储存的条目,而不管它们的所有者是谁(原本应该只返回属于当前已认证用户的条目)

【正确代码】

PreparedStatement stmt = null

ResultSet rs = null

try

{

String userName = ctx.getAuthenticatedUserName(); //this is a constant

String itemName = request.getParameter( “itemName” );

// …Ensure that the length of userName and itemName is legitimate

// …

String sqlString = “SELECT * FROM t_item WHERE owner=? AND itemName=?” ;

stmt = connection.prepareStatement(sqlString);

stmt.setString(1, userName);

stmt.setString(2, itemName);

rs = stmt.executeQuery();

// … result set handling

}

catch (SQLException se)

{

// … logging and error handling

}

参数化查询在SQL语句中使用占位符表示需在运行时确定的参数值,使得SQL查询的语义逻辑预先被定义,实际的查询参数值则在程序运行时再确定。参数化查询使得数据库能够区分SQL语句中语义逻辑和数据参数,以确保用户输入无法改变预期的SQL查询语义逻辑。在C#中,可以使用SqlCommand.Parameters.AddWithValue进行参数化查询。正确示例中,如果攻击者的itemName输入为name’ OR ‘a’ = ‘a,参数化查询将会查找itemName匹配name’ OR ‘a’ = ‘a字符串的条目,而不是返回整个表中的所有条目。

如果采用黑名单方式校验,请参考SQL特殊字符:

数据库

特殊字符

描述

转义序列

Oracle

%

百分比:任何包括0或更多字符的字符串

/% escape ‘/’

_

下划线:任意单个字符

/_ escape ‘/’

/

斜线:转义字符

// escape ‘/’

单引号

MySQL

单引号

\’

双引号

\”

\

反斜杠

\\

%

百分比:任何包括0或更多字符的字符串

\%

_

下划线:任意单个字符

\_

DB2

单引号

;

冒号

.

SQL Server

单引号

[

左中括号:转义字符

[[]

_

下划线:任意单个字符

[_]

%

百分比:任何包括0或更多字符的字符串

[%]

^

插入符号:不包括以下字符

[^]

规则3:禁止使用外部数据拼接 SHELL 命令

【说明】

java 中,Runtime.exec()经常被用来调用一个新的进程,如果在Runtime.exec()中使用命令行shell(例如, cmd.exe /bin/sh )来调用一个程序,如果还需要拼接外部输入的参数,那么则可能产生命令注入。

【错误代码】

class DirList

{

public static void main(String[] args)

{

if (args. length == 0)

{

System. out .println( “No arguments” );

System. exit (1);

}

try

{

Runtime rt = Runtime. getRuntime ();

Process proc = rt.exec( “/bin/sh ls” + args[0]);

// …

}

catch (Exception e)

{

// Handle errors

}

}

}

攻击者可以通过以下命令来利用这个漏洞程序:

java DirList “dummy ; touch attack_file; ”

实际将会执行两个命令:

ls dummy

touch attack_file

这样会创建出来一个恶意文件attack_file。

【正确代码】

在拼接shell命令之前进行特殊字符校验,如果含有特殊字符则不允许执行。

SHELL命令特殊字符:

分类

符号

功能描述

管道

|

连结上个指令的标准输出,作为下个指令的标准输入。

内联命令

;

连续指令符号

&

单一个& 符号,且放在完整指令列的最后端,即表示将该指令列放入后台中工作。

逻辑操作符

$

变量替换(Variable Substitution)的代表符号。

&&

代表与(and)逻辑的符号。

||

代表或(or)逻辑的符号。

重定向操作

>

将命令输出写入到目标文件中。

>>

将命令输出附加到目标文件中。

<

将目标文件的内容发送到命令当中。

表达式

${}

变量的正规表达式。

反引号

‘ ‘

返回当前执行命令的结果。

倒斜线

\

在交互模式下的 escape 字元,有几个作用;放在指令前,有取消 aliases的作用;放在特殊符号前,则该特殊符号的作用消失;放在指令的最末端,表示指令连接下一行。

引号

“”

双引号 括住的内容将被视为单一字符串。但是对$,\,‘和”不起作用。

‘’

被单引号括住的内容,将被视为单一字符串。

括号

()

用括号将一串连续指令括起来。

[]

常出现在流程控制中,扮演括住判断式的作用。

双分号

;;

case语句中担任结束符。

文件目录

~

账户的home目录。

.

一个dot代表当前目录,两个dot代码上层目录。

文件名扩展

?

在文件名扩展(Filename expansion)上扮演的角色是匹配一个任意的字元,但不包含 null 字元。

*

在文件名扩展( File name expansion)上,她用来代表任何字元,包含 null 字元。

规则4:禁止使用外部数据拼接XML

【说明】

使用未经校验数据来构造XML会导致XML注入漏洞。如果用户被允许输入结构化的XML片段,则他可以在XML的数据域中注入XML标签来改写目标XML文档的结构和内容,XML解析器会对注入的标签进行识别和解释,引起注入问题。需要注意的是,XML schema或者 DTD 校验仅能确保XML的格式是有效的,但是攻击者可以在不打破原有XML格式的情况下,对XML的内容进行篡改。如果输入中确实含有XML中的特殊字符,需要转义处理。

XML特殊字符转义:

特殊字符

转义字符

<

<

>

>

&

&

【错误代码1】

private void createXMLStream(BufferedOutputStream outStream, User user) throws IOException

{

String xmlString;

xmlString = “<user><role>operator</role><id>” + user.getUserId()

+ “</id><description>” + user.getDescription() + “</description></user>” ;

outStream.write(xmlString.getBytes());

outStream.flush();

}

恶意用户可能会使用下面的字符串作为用户ID:

“joe</id><role>administrator</role><id>joe”

并使用如下正常的输入作为描述字段:

I want to be an Administrator

最终,整个XML字符串将变成如下形式:

<user>

<role> operator </role>

<id> joe </id>

<role> administrator </role>

<id> joe </id>

<description> I want to be an administrator </description>

</user>

由于SAX解析器(org.xml.sax and javax.xml.parsers. SAX Parser)在解释XML文档时会将第二个role域的值覆盖前一个role域的值,因此会导致此用户角色由操作员提升为了管理员。

【错误代码2】:

private void createXMLStream(BufferedOutputStream outStream, User user)

throws IOException

{

String xmlString;

xmlString = “<user><id>” + user.getUserId()

+ “</id><role>operator</role><description>”

+ user.getDescription() + “</description></user>” ;

StreamSource xmlStream = new StreamSource( new StringReader(xmlString));

// Build a validating SAX parser using the schema

Schema Factory sf = SchemaFactory. newInstance (XMLConstants. W3C_XML_SCHEMA_NS_URI );

StreamSource ss = new StreamSource( new File( “schema.xsd” ));

try

{

Schema schema = sf.newSchema(ss);

Validator validator = schema.newValidator();

validator.validate(xmlStream);

}

catch (SAXException x)

{

throw new IOException( “Invalid userId” , x);

}

// the XML is valid, proceed

outStream.write(xmlString.getBytes());

outStream.flush();

}

如下是schema.xsd文件中的schema定义:

< xs:schema xmlns:xs = “#34; >

< xs:element name = “user” >

< xs:complexType >

< xs:sequence >

< xs:element name = “id” type = “xs:string” />

< xs:element name = “role” type = “xs:string” />

< xs:element name = “description” type = “xs:string” />

</ xs:sequence >

</ xs:complexType >

</ xs:element >

</ xs:schema >

某个恶意用户可能会使用下面的字符串作为用户ID:

“joe</id><role>Administrator</role><!—”

并使用如下字符串作为描述字段:

“–><description> I want to be an administrator

最终,整个XML字符串将变成如下形式:

<user>

<id> joe </id>

<role> Administrator </role> <!–</id> <role>operator</role> <description> –>

<description> I want to be an administrator </description>

</user>

这种情况即通过了XML schema或者DTD校验,但是实际报文仍然对XML的内容进行篡改。

【正确代码】:

public static void buidlXML(FileWriter writer, User user) throws IOException

{

Document userDoc = DocumentHelper. createDocument ();

Element userElem = userDoc.addElement( “user” );

Element idElem = userElem.addElement( “id” );

idElem.setText(user.getUserId());

Element roleElem = userElem.addElement( “role” );

roleElem.setText( “operator” );

Element descrElem = userElem.addElement( “description” );

descrElem.setText(user.getDescription());

XMLWriter output = null ;

try

{

OutputFormat format = OutputFormat. createPrettyPrint ();

format.setEncoding( “UTF-8” );

output = new XMLWriter(writer, format);

output.write(userDoc);

output.flush();

}

finally

{

try

{

output.close();

}

catch (Exception e)

{

// handle exception

}

}

}

正确示例中使用dom4j来构建XML,dom4j是一个定义良好、开源的XML工具库。Dom4j将会对文本数据域进行XML编码,从而使得XML的原始结构和格式免受破坏。

这个例子中,攻击者如果输入如下字符串作为用户ID:

“joe</id><role>Administrator</role><!—”

以及使用如下字符串作为描述字段:

“–><description> I want to be an administrator

则最终会生成如下格式的XML:

<user>

<id> joe < /id >< role > Administrator < /role >< !— </id>

<role> operator </role>

<description> >< description > I want to be an administrator </description>

</user>

可以看到,“<”与“>”经过XML编码后分别被替换成了“<”与“>”,导致攻击者未能将其角色类型从操作员提升到管理员。

规则5:禁止使用外部输入作为格式化字符串

【说明】

当转换参数与对应的格式符不匹配时,标准类库会抛出异常,这种方式可以减少被恶意攻击的机会。但如果代码中使用来源不可信的数据来构造格式化字符串时,仍有可能被攻击者利用,造成系统信息泄露或者拒绝服务。因此,不能直接将来自不可信源的字符串用于构造格式化字符串。

【错误代码】

class Format

{

static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY, 23);

public static void main(String[] args )

{

System.out.printf (args[0]

+ ” did not match! HINT: It was issued on %1$terd of some month”, c);

}

}

【正确代码】

class Format

{

static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY, 23);

public static void main(String[] args)

{

System.out.printf(” %s did not match! ”

+ ” HINT: It was issued on %2$terd of some month”, args[0] , c);

}

}

规则6:禁止使用外部输入的路径或文件名,路径校验之前必须进行标准化

【说明】

当文件路径来自非信任域时,需要先将文件路径规范化再做校验。路径在验证时会有很多干扰因素,如相对路径与绝对路径,如文件的符号链接、硬链接、快捷路径、别名等。

所以在验证路径时需要对路径进行标准化,使得路径表达唯一化、无歧义。

如果没有作标准化处理,攻击者就有机会:

(1)构造一个跨越目录限制的文件路径,例如“../../../etc/passwd”或“../../../boot.ini”

(2)构造指向系统关键文件的链接文件,例如 symlink (“/etc/shadow”,”/tmp/log”)

通过上述两种方式之一可以实现读取或修改系统重要数据文件,威胁系统安全。

因此在校验之前,需要通过标准化转化为绝对路径

【错误代码】

public static void main(String[] args)

{

File f = new File(System.getProperty(“user.home”)

+ System.getProperty(“file.separator”) + args[0]);

String absPath = f.getAbsolutePath(); // File.getAbsolutePath()返回文件的绝对路径,但是它不会解析文件链接,也不会消除等价错误。

if (!isInSecureDir(Paths.get(absPath)))

{

// Refer to Rule 3.5 for the details of isInSecureDir()

throw new IllegalArgumentException();

}

if (!validate(absPath))

{

// Validation

throw new IllegalArgumentException();

}

/* … */

}

【正确代码】

public static void main(String[] args) throws IOException

{

File f = new File(System.getProperty(“user.home”)

+ System.getProperty(“file.separator”) + args[0]);

String canonicalPath = f.getCanonicalPath();

if (!isInSecureDir(Paths.get(absPath)))

{

// Refer to Rule 3.5 for the details of isInSecureDir()

throw new IllegalArgumentException();

}

if (!validate(absPath))

{

// Validation

throw new IllegalArgumentException();

}

/* … */

}

规则7:安全地从ZipInputStream提取文件

【说明】

从java.util.zip.ZipInputStream中解压文件时需要小心谨慎。有两个特别的问题需要避免:一个是解压出的标准化路径文件在解压目标目录之外,另一个是解压的文件消耗过多的系统资源。前一种情况,攻击者可以从zip文件中往用户可访问的任何目录写入任意的数据。后一种情况,在与输入数据所使用资源相比严重不成比例时,就可能产生拒绝服务。由于Zip算法有极高的压缩率,即使在解压如ZIP、GIF、gzip编码HTTP的小文件时,也可能会导致过度的资源消耗,导致zip炸弹(zip bomb)。

Zip算法有非常高的压缩比。例如,一个由字符a和字符b交替出现的行构成的文件,压缩比可以达到200:1。使用针对目标压缩算法的输入数据,或者使用更多的输入数据(不针对目标压缩算法的),或者使用其他的压缩方法,甚至可以达到更高的压缩比。

任何被提取条目的目标路径不在程序预期目录之内时(必须先对文件名进行标准化,参照规则6),要么拒绝将其提取出来,要么将其提取到一个安全的位置。Zip文件中任何被提取条目,若解压之后的文件大小超过一定的限制时,必须拒绝将其解压。具体大小限制由平台的处理性能来决定。

【错误代码】

static final int BUFFER = 512;

// …

public final void unzip(String fileName) throws java.io.IOException

{

FileInputStream fis = new FileInputStream(fileName);

ZipInputStream zis = new ZipInputStream( new BufferedInputStream(fis));

ZipEntry entry;

while ((entry = zis.getNextEntry()) != null )

{

System. out .println( “Extracting: ” + entry);

int count;

byte data[] = new byte [ BUFFER ];

// Write the files to the disk

FileOutputStream fos = new FileOutputStream(entry.getName());

BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER );

while ((count = zis.read(data, 0, BUFFER )) != -1)

{

dest.write(data, 0, count);

}

dest.flush();

dest.close();

zis.closeEntry();

}

zis.close();

}

错误示例中,未对解压的文件名做验证,直接将文件名传递给FileOutputStream构造器。它也未检查解压文件的资源消耗情况,允许程序运行到操作完成或者本地资源被耗尽。

【错误代码2】

public static final int BUFFER = 512;

public static final int TOOBIG = 0x6400000; // 100MB

// …

public final void unzip( String filename) throws java.io.IOException

{

FileInputStream fis = new FileInputStream(filename);

ZipInputStream zis = new ZipInputStream( new BufferedInputStream(fis));

ZipEntry entry;

try

{

while ((entry = zis.getNextEntry()) != null )

{

System. out .println( “Extracting: ” + entry);

int count;

byte data[] = new byte [ BUFFER ];

// Write the files to the disk, but only if the file is not insanely big

if (entry.getSize() > TOOBIG )

{

throw new IllegalStateException(

“File to be unzipped is huge.” );

}

if (entry.getSize() == -1)

{

throw new IllegalStateException(

“File to be unzipped might be huge.” );

}

FileOutputStream fos = new FileOutputStream(entry.getName());

BufferedOutputStream dest = new BufferedOutputStream(fos,

BUFFER );

while ((count = zis.read(data, 0, BUFFER )) != -1)

{

dest.write(data, 0, count);

}

dest.flush();

dest.close();

zis.closeEntry();

}

}

finally

{

zis.close();

}

}

错误示例在解压条目之前调用ZipEntry.getSize()方法判断其大小,以试图解决之前的问题。但不幸的是,恶意攻击者可以伪造ZIP文件中用来描述解压条目大小的字段,因此,getSize()方法的返回值是不可靠的,本地资源实际仍可能被过度消耗。

【正确代码】

static final int BUFFER = 512;

static final int TOOBIG = 0x6400000; // max size of unzipped data, 100MB

static final int TOOMANY = 1024; // max number of files

// …

private String sanitzeFileName(String entryName, String intendedDir) throws IOException

{

File f = new File(intendedDir, entryName);

String canonicalPath = f.getCanonicalPath();

File iD = new File(intendedDir);

String canonicalID = iD.getCanonicalPath();

if (canonicalPath.startsWith(canonicalID))

{

return canonicalPath;

}

else

{

throw new IllegalStateException(

“File is outside extraction target directory.” );

}

}

// …

public final void unzip(String fileName) throws java.io.IOException

{

FileInputStream fis = new FileInputStream(fileName);

ZipInputStream zis = new ZipInputStream( new BufferedInputStream(fis));

ZipEntry entry;

int entries = 0;

int total = 0;

byte [] data = new byte [ BUFFER ];

try

{

while ((entry = zis.getNextEntry()) != null )

{

System. out .println( “Extracting: ” + entry);

int count;

// Write the files to the disk, but ensure that the entryName is valid,

// and that the file is not insanely big

String name = sanitzeFileName(entry.getName(), “.” );

FileOutputStream fos = new FileOutputStream(name);

BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER );

while (total + BUFFER <= TOOBIG && (count = zis.read(data, 0, BUFFER )) != -1)

{

dest.write(data, 0, count);

total += count;

}

dest.flush();

dest.close();

zis.closeEntry();

entries++;

if (entries > TOOMANY )

{

throw new IllegalStateException( “Too many files to unzip.” );

}

if (total > TOOBIG )

{

throw new IllegalStateException(

“File being unzipped is too big.” );

}

}

}

finally

{

zis.close();

}

}

正确示例中,代码会在解压每个条目之前对其文件名进行校验。如果某个条目校验失败,整个解压过程都将会被终止。实际上也可以忽略跳过这个条目,继续后面的解压过程,甚至也可以将这个条目解压到某个安全位置。除了校验文件名,while循环中的代码会检查从zip存档文件中解压出来的每个文件条目的大小。如果一个文件条目太大,此例中是100MB,则会抛出异常。最后,代码会计算从存档文件中解压出来的文件条目总数,如果超过1024个,则会抛出异常。

规则8:禁止使用外部输入作为控制循环、递归的条件,防止出现死循环或者无限递归

【说明】

如果循环或者递归的条件是直接使用了外部输入,那么恶意攻击者就可能通过恶意构造输入,导致程序进入死循环或者深度递归,从而导致资源耗尽引起异常,或者是DOS攻击。

对于内部类似定时器类的死循环线程不做限制。

规则9:新建文件必须指定合理的文件权限,临时文件使用完毕必须删除

【说明】

系统文件的权限应当遵循最小化的原则,权限设置的不合理,则可能被攻击者利用。例如文件权限如果过大,如果存在了敏感数据,则可能泄露这些数据。临时文件如果不及时清理,则可能在环境中造成堆积病情导致磁盘空间占满,攻击者可能利用此漏洞造成程序功能异常,引起DOS攻击。

权限设置如下:

文件类型

设置值

用户主目录

750(rwxr-x—)

程序文件(含脚本文件、库文件等)

550(r-xr-x—)

程序文件目录

550(r-xr-x—)

配置文件

640(rw-r—–)

配置文件目录

750(rwxr-x—)

日志文件(记录完毕或者已经归档)

440(r–r—–)

日志文件(正在记录)

640(rw-r—–)

日志文件目录

750(rwxr-x—)

Debug文件

640(rw-r—–)

Debug文件目录

750(rwxr-x—)

临时文件目录

750(rwxr-x—)

维护升级文件目录

770(rwxrwx—)

业务数据文件

640(rw-r—–)

业务数据文件目录

750(rwxr-x—)

密钥组件、私钥、证书、密文文件目录

700(rwx——-)

密钥组件、私钥、证书、加密密文

600(rw——-)

加解密接口、加解密脚本

500(r-x——)

规则10:必须使用安全随机数

【说明】

伪随机数生成器(PRNG)使用确定性数学算法来产生具有良好统计属性的数字序列。但是这种数字序列并不具有真正的随机特性。伪随机数生成器通常以一个算术种子值为起始。算法使用该种子值生成一个输出以及一个新的种子,这个种子又被用来生成下一个随机值,以此类推。

Java API 提供了伪随机数生成器(PRNG)—— java.util.Random类。这个伪随机数生成器具有可移植性和可重复性。因此,如果两个java.util.Random类的实例创建时使用的是相同的种子值,那么对于所有的Java实现,它们将生成相同的数字序列。在系统重启或应用程序初始化时,Seed值总是被重复使用。在一些其他情况下,seed值来自系统时钟的当前时间。攻击者可以在系统的一些安全脆弱点上监听,并构建相应的查询表预测将要使用的seed值。

因此,java.util.Random类不能用于安全敏感应用或者敏感数据保护。应使用更加安全的随机数生成器,例如java.security.SecureRandom类。

【正确代码】

public byte [] genRandBytes(int len)

{

byte [] bytes = null;

if (len > 0 && len < 1024)

{

bytes = new byte [len];

SecureRandom random = new SecureRandom();

random.nextBytes(bytes);

}

return bytes;

}

规则11:禁止使用不安全的加密算法、私有算法,使用哈希算法必须带盐值

【说明】

禁用私有算法或者弱加密算法,应该使用经过验证的、安全的、公开的加密算法。

推荐使用的密码算法如下:

1)分组密码算法:AES(密钥长度在128位及以上)(GCM或者CBC模式)

2)流密码算法:AES(密钥长度在128位及以上)(OFB或CTR模式)

3)非对称加密算法:RSA(密钥长度在2048位及以上)

4)哈希算法:SHA2(256位及以上)

5)密钥交换算法:DH(2048位及以上)

6)HMAC(基于哈希的消息验证码)算法:HMAC-SHA2

目前业界已知不安全的加密算法,DES/3DES/SKIPJACK/RC2/RSA(1024位及以下)/MD2/MD4,

如果一个口令的哈希值储存在一个数据库中,由于哈希算法的不可逆性,攻击者就应该不可能还原出口令。如果说可以恢复口令,那么唯一的方式就是暴力破解攻击,比如计算所有可能口令的哈希值,或是字典攻击,计算出所有常用的口令的哈希值。如果每个口令都只仅经过简单哈希,相同的口令将得到相同的哈希值。仅保存口令哈希有以下两个缺陷:

Ÿ 由于“生日判定”,攻击者可以快速找到一个口令,尤其是当数据库中的口令数量较大的时候。

Ÿ 攻击者可以使用事先计算好的哈希列表在几秒钟之内破解口令。

为了解决这些问题,可以在进行哈希运算之前在口令中引入盐值。一个盐值是一个固定长度的随机数。这个盐值对于每个存储入口来说必须是不同的。可以明文方式紧邻哈希后的口令一起保存。在这样的配置下,攻击者必须对每一个口令分别进行暴力破解攻击。这样数据库便能抵御“生日”或者“彩虹表”攻击。

如何选择和使用加密算法,详细可以参考《密码算法应用规范》。

规则12:禁止在日志、异常中打印敏感信息

【说明】

在日志中不能保存口令和密钥,其中的口令包括明文口令和密文口令。对于敏感信息建议采取以下方法,

1)不打印在日志中;

2)若因为特殊原因必须要打印日志,则用“*”代替(不要显示出敏感信息的长度)。

常见的敏感数据包含认证凭据(例如口令、私钥、SNMP团体名、预共享密钥等)、个人数据、用户通信内容等,请参考《产品网络安全红线》中定义的范围为准。

规则13:禁止序列化未加密的敏感数据

【说明】

虽然序列化可以将对象的状态保存为一个字节序列,之后通过反序列化将字节序列又能重新构造出原来的对象,但是它并没有提供一种机制来保证序列化数据的安全性。如果序列化之后的数据用于传输或者持久化存储,必须禁止序列化敏感数据,必须使用安全算法正确加密,或者不对敏感数据序列化。

【错误代码】

public class GPSLocation implements Serializable

{

private double x ; // sensitive field

private double y ; // sensitive field

private String id ; // non-sensitive field

// other content

}

public class Coordinates

{

public static void main(String[] args)

{

FileOutputStream fout = null ;

try

{

GPSLocation p = new GPSLocation(5, 2, “northeast” );

fout = new FileOutputStream( “location.ser” );

ObjectOutputStream oout = new ObjectOutputStream(fout);

oout.writeObject(p);

oout.close();

}

catch (Throwable t)

{

// Forward to handler

}

finally

{

if (fout != null )

{

try

{

fout.close();

}

catch (IOException x)

{

// handle error

}

}

}

}

}

示例代码中,假定坐标信息是敏感的,那么将其序列化到数据流中使之面临敏感信息泄露与被恶意篡改的风险。

【正确代码1】

public class GPSLocation implements Serializable

{

private transient double x ; // transient field will not be serialized

private transient double y ; // transient field will not be serialized

private String id ;

// other content

}

在对某个包含敏感数据的类序列化时,程序必须确保敏感数据不被序列化。包括阻止包含敏感信息的数据成员被序列化,以及不可序列化或者敏感对象的引用被序列化。该示例将相关字段声明为transient,从而使它们不包括在依照默认的序列化机制应该被序列化的字段列表中。这样既避免了错误的序列化,又防止了敏感数据被意外序列化。

【正确代码2】:

public class GPSLocation implements Serializable

{

private double x ;

private double y ;

private String id;

// sensitive fields x and y are not content in serialPersistentFields

private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField( “id”, String. class )};

// other content

}

该示例通过定义serialPersistentFields数组字段来确保敏感字段被排除在序列化之外,除了上述方案,也可以通过自定义writeObject()、writeReplace()、writeExternal()这些函数,不将包含敏感信息的字段写到序列化字节流中。

规则14:代码中禁止硬编码敏感信息

【说明】

如果将敏感信息(包括口令和加密密钥)硬编码在程序中,可能会将敏感信息暴露给攻击者。任何能够访问到class文件的人都可以反编译class文件并发现这些敏感信息。因此,不能将信息硬编码在程序中。同时,硬编码敏感信息会增加代码管理和维护的难度。例如,在一个已经部署的程序中修改一个硬编码的口令需要发布一个补丁才能实现。

规则15:生产代码不能包含任何调试入口点

【说明】

一种常见的做法就是由于调试或者测试目的在代码中添加特定的后门代码,这些代码并没有打算与应用一起交付或者部署。当这类的调试代码不小心被留在了应用中,这个应用对某些无意的交互就是开放的。这些后门入口点可以导致安全风险,因为在设计和测试的时候并没有考虑到而且处于应用预期的运行情况之外。

规则16:禁止部署的应用可被远程监控

【说明】

Java提供了多种API让外部程序来监控运行中的Java程序。这些API也允许不同主机上的程序远程监控Java程序。这样的特征方便对程序进行调试或者对其性能进行调优。但是,如果一个Java程序被部署在生产环境中同时允许远程监控,攻击者很容易连接到JVM来监视这个Java程序的行为和数据,包括所有潜在的敏感信息。攻击者也可以对程序的行为进行控制。

【错误代码】

JVMTI:

${JDK_PATH}/bin/java -agentlib:libname=options ApplicationName

在该错误示例中,JVM Tool Interface通过代理来与运行中的JVM通信。这些代理通常是在JVM启动的时候通过Java命令行参数-agentlib或者-agentpath来加载的,从而允许JVMTI对应用程序进行监控。

JVM监控:

${JDK_PATH}/bin/java -Dcom.sun.management.jmxremote.port=8000 ApplicationName

在以上错误示例中,用命令行参数使得JVM被允许在8000端口上进行远程监控。如果密码强度很弱或者误用SSL协议,可能会导致安全漏洞。

规则17:确保程序不再持有无用对象的引用,避免程序内存泄露。

【说明】

java与C/C++最大的区别之一就是java通过GC(垃圾收集器)自动回收内存,但是这并不意味着java代码中不存在内存泄露的问题。Java中的内存泄露更准确的提法是“无意识的引用保留”,GC会在程序运行过程中对每一个对象进行检查,如果当前程序中不在引用此对象,则此对象被标识为垃圾对象,可以被回收。但是如果程序中保留了对无用对象的引用则会造成GC无法检测出垃圾对象,进而无法回收垃圾对象的内存。

【错误代码】

public class Stack

{

……

public Object pop()

{

if (size == 0)

{

throw new EmptyStackException();

}

/*

*以下代码造成了内存泄露,对象出栈后堆栈仍保留了对此对象的引用,

*当出栈对象变成无用对象后,由于堆栈还持有引用,所以对象不会被

*GC回收。

*/

return elements[–size];

}

……

}

【正确代码】

public class Stack

{

……

public Object pop()

{

if (size == 0)

{

throw new EmptyStackException();

}

Object ret = elements[–size];

elements[size] = null; //清除堆栈持有的对象引用

return ret;

}

……

}

规则18:访问数组、List等容器内的元素时,必须首先检查下标是否越界,杜绝下标越界异常的发生。

【说明】

如果访问数组、List等容器内的元素时,如果下标直接或间接使用了外部输入,则必须要进行严格检查,否则可能通过外部输入构造越界的异常。如果代码内部确保,可不做强制校验。

【错误代码】

public void checkArray(String name)

{

String[] cIds = ContentService.queryByName(name);

if(null != cIds)

{

// 只是考虑到cids有可能为null的情况,但是cids完全有可能是个0长度的数组,因此cIds[0]有可能数组下标越界

String cid=cIds[0];

cid.toCharArray();

}

}

【正确代码】

public void checkArray(String name)

{

String[] cIds = ContentService.queryByName(name);

if(null != cIds && cIds.length > 0 )

{

String cid=cIds[0];

cid.toCharArray();

}

}

规则19:将对象存入HashSet,或作为key存入HashMap(或HashTable)后,必须确保该对象的hashcode值不变,避免因为hashcode值变化导致不能从容器内删除该对象,进而引起内存泄露的问题。

【说明】

对于Hash容器(HashMap,HashSet,HashTable等)而言,对象的hashcode至关重要,在hash容器内查找该对象完全依赖此值。如果一个对象存入Hash容器后hashcode随即发生变化,结果就是无法在容器内找到该对象,进而不能删除该对象,最终导致内存泄露。

【错误代码】

public int hashCode()

{

int result = address.hashCode();

return result;

}

……

HashSet<Email> set = new HashSet<Email>();

Email email = new Email(“huawei.com”);

set.add(email);

……

mail.address = “silong.com”; //修改地址值,导致hashcode值变化

……

set.remove(mail);

【正确代码】

public int hashCode()

{

int result = address.hashCode();

return result;

}

……

HashSet<Email> set = new HashSet<Email>();

Email email = new Email(“huawei.com”);

set.add(email);

……//没有修改address值,保证了hashcode值不变

set.remove(mail);

……

规则20:对多线程访问的变量、方法,必须加锁保护,避免出现多线程并发访问引起的问题。

【说明】

在多线程环境中,如果涉及多线程共享访问的变量、方法,如果没有加锁保护,则可能引起异常或者导致功能问题。

比如说业务上为了限制了外部建立连接的数量,但是计数变量未作加锁保护,攻击者通过瞬间的大量建立连接请求,就可能实际突破代码中的限制,从而引起资源耗尽类的DOS攻击。

规则21:线程使用时,要在代码框架中使用线程池,避免创建不可复用的线程。禁止在循环中创建新线程,否则会引起JVM资源耗尽。

【说明】

频繁创建线程和销毁线程耗时,会大大降低系统效率,用线程池是线程可以复用,能提升效率。如果创建线程过多就不能创建更多的线程了,因此循环创建线程很容易导致JVM资源耗尽。

如果是通过外部业务触发的逻辑,线程创建需要严格限制,不能因为大量的外部业务导致线程创建过多,引起资源耗尽,导致DOS攻击。

【正确代码】

${JDK_PATH}/bin/java -Djava.security.manager ApplicationName

上面的命令行启动JVM时,未启用任何代理。避免在生产设备上使用-agentlib, -Xrunjdwp,和-Xdebug命令行参数,并且安装了默认的安全管理器。

文章来源:智云一二三科技

文章标题:Java安全编码军规

文章地址:https://www.zhihuclub.com/179617.shtml

关于作者: 智云科技

热门文章

网站地图