java安全问题其实是很多程序员想了解又容易忽略的问题,但需要我们重视起来,提高应用程序的安全性。常出现的安全问题包括,程序接受数据可能来源于未经验证的用户,网络连接和其他不受信任的来源,如果未对程序接受数据进行校验,则可能会引发安全问题等等,具体也可以分成以下几方面:
- 数据校验
- 敏感信息
- 加密算法
- 序列化与反序列化
- I/O操作
- 多线程安全
- 框架和组件
数据校验
数据校验-校验策略
1. 白名单策略 -接受已知好的数据( 任何时候,尽可能使用“白名单”的策略 )
下面的示例代码确保 name参数只包含字母、以及下划线
if (Pattern.matches("^[0 -9A -Za -z_]+$", name)){
throw new IllegalArgumentException("Invalid name");
}
2. 黑名单策略 -拒绝已知好的数据
public String removeJavascript(String input){
Pattern p = Pattern.compile("javascript", Pattern.CASE_INSENSITIVE );
Matcher m = p.matcher(input);
return (! m.matches()) ? input : "";
}
3. 白名单净化
对数据中任何不属于某个已验证的、合法字符列表进行删除编码或者替换,然后再使用这些净化的数据
4. 黑名单净化: 剔除或者转换某些字符(例如,删除引号、转换成HTML实体)
public static String quoteApostrophe(String input){
if (input != null){
return input.replaceAll(" \'","’");
} else{
return null;
}
}
数据校验 -输入输出
规则1.1 校验跨信任边界传递的不可数据**
程序接受的不可信数据源跨越任边界传递必须经过内校验,包括输入和出校验。
不可信数据:用户、网络连接等源 不可信数据:用户、网络连接等源
数据入口:
- 终端计算机
- 互联网出入口
- 广域网出入口
- 公司对外发布服务的 DMZ服务器
- VPN和类似远程连接设备。
信任边界:根据威胁建模划分的信任边 如 web 应用的服务端;
规则 1.2:禁止直接使用不可信数据来拼SQL语句
SQL 注入是指原始SQL查询被动态更改成一个与程序预期完全不同的查询。执行这样后可能导致信息泄露或者数据被篡改。防止 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
}
添加 name' OR 'a' = 'a
SELECT * FROM t_item WHERE owner = 'wiley' AND itemname= 'name' OR 'a'='a';
预编译处理:
PreparedStatement stmt= null
ResultSet rs=null
try
{
String userName= ctx.getAuthenticatedUserName(); //this is a constant
String itemName= request.getParameter("");
// ...Ensure that the length of userName and itemNameis 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(SQLExceptions e)
{
// ... logging and error handling
}
在存储过程中,通拼接参数值来构建查询字符串和应用序代码一样同是有SQL注入风险
反例:
CallableStatement= null
ResultSet results = null;
try{
String userName= ctx.getAuthenticatedUserName(); //this is a constant
String itemName= request.getParameter("itemName");
cs= connection.prepareCall("{call sp_queryItem(?,?)}");
cs.setString(1, userName);
cs.setString(2, itemName);
results = cs.executeQuery();
// ... result set handling
}catch(SQLException se){
// ... logging and error handling
}
对应的SQL Server存储过程:
CREATE PROCEDURE sp_queryItem
@userNamevarchar(50),
@itemNamevarchar(50)
AS
BEGIN
DECLARE @sql nvarchar(500);
SET @sql= 'SELECT * FROM t_item
WHERE owner = ''' + @userName+ '''
AND itemName= ''' + @itemName+ '''';
EXEC(@sql);
END
GO
正例:
** 在存储过程中动态构建sql,采用预编译的方式防御sql注入,**
CallableStatement= null
ResultSet results = 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
// ...
cs= ("{call sp_queryItem(?,?)}");
cs.setString(1, userName);
cs.setString(2, itemName);
results = cs.executeQuery();
// ... result set handling
}catch(SQLException se){
// ... logging and error handling
}
对应的SQL Server存储过程:
CREATE PROCEDURE sp_queryItem
@userName varchar(50),
@itemName varchar(50)
AS
BEGIN
SELECT * FROM t_item
WHERE userName= @userName
AND itemName= @itemName;
END
使用Hibernate,如果在动态构建SQL/HQL查询时包含了不可信输入,同样也会面临SQL/HQL注入的问题。
反例:
//原生sql查询
String userName= ctx.getAuthenticatedUserName();
//this is a constant
String itemName= request.getParameter("itemName");
Query sqlQuery= session.createSQLQuery("select * from where owner = '"
+ userName+ "' and itemName= '" + itemName+ “’”);
List- rs= (List
- ) sqlQuery.list();
//HQL查询
String userName= ctx.getAuthenticatedUserName();
//this is a constant
String itemName=request.getParameter("itemName");
Query hqlQuery= session.createQuery("from Item as item where item.owner= '"
+ userName+ "' and = '" + itemName+ "'");
List
- hrs= (List
- ) hqlQuery.list();
正例:
//HQL中基于位置的参数化查询:
String userName= ctx.getAuthenticatedUserName();
String itemName=request.getParameter("itemName");
Query hqlQuery= session.createQuery("from Item as item where item.owner= ? and item.itemName= ?");
hqlQuery.setString(1, userName);
hqlQuery.setString(2, itemName);
List- rs= (List
- ) hqlQuery.list();
//HQL中基于名称的参数化查询:
String userName= ctx.getAuthenticatedUserName();
String itemName= ("itemName");
Query hqlQuery= session.createQuery("from Item as item where item.owner= :owner and = :itemName");
hqlQuery.setString("owner", userName);
hqlQuery.setString("itemName", itemName);
List
- rs= (List
- ) hqlQuery.list();
//原生参数化查询:
String userName=ctx.getAuthenticatedUserName(); //this is a constant
String itemName= request.getParameter("itemName");
Query sqlQuery= session.createSQLQuery("select * from t_itemwhere owner = ? and itemName= ?");
sqlQuery.setString(0, owner);
sqlQuery.setString(1, itemName);
List
- rs= (List
- ) sqlQuery.list();
Mybaits和ibaits的#和$
Mybaits:
String sqlString= "SELECT * FROM t_itemWHERE owner=? AND itemName=?";
PreparedStatement stmt= connection.prepareStatement(sqlString);
stmt.setString(1, myClassObj.getUserName());
stmt.setString(2, myClassObj.getItemName());
ResultSet rs= stmt.executeQuery();
// ... convert results set to Item objects
ibaits:
String sqlString= "SELECT * FROM t_itemWHERE owner=? AND itemName='" +myClassObj.getItemName() + "'";
PreparedStatementstmt=connection.prepareStatement(sqlString);
stmt.setString(1, myClassObj.getUserName());
ResultSetrs= stmt.executeQuery();
输入验证,针对无法参数化查询的场景
public List queryBooks(queryCondition){
try{
StringBuilder sb= StringBuilder("select * from t_bookwhere ");
Codec oe= new OracleCodec();
if(queryCondition!= null&& !queryCondition.isEmpty()){
for(Expression e : queryCondition){
String exprString=e.getColumn() + e.getOperator() + e.getValue();
String safeExpr= ESAPI.encoder().encodeForSQL(oe, exprString);
sb.append(safeExpr).append(" and ");
}
sb.append("1=1");
Statement stat = connection.createStatement();
ResultSet rs= stat.executeQuery(sb.toString());
//other omitted code
}
}
}
规则1.3 禁止直接使用不可信数据来拼接XML
一个用户,如果他被允许输入结构化的XML片段,则他可以在XML的数据域中注入XML标签来改写目标XML文档的结构与内容。XML解析器会对注入的标签进行识别和解释。
private void createXMLStream(BufferedOutputStreamoutStream, User user) throws IOException{
String xmlString;
xmlString= "operator " + user.getUserId()+ " " + user.getDescription() + " ";
outStream.write(xmlString.getBytes());
outStream.flush();}}
添加joe
operator
joe
administrator
joe
I want to be an administrator
XML Schema或者DTD校验,反例:
private void createXMLStream(BufferedOutputStreamoutStream, User user)throwsIOException{
String xmlString;
xmlString= "" + user.getUserId()+ " operator "+ user.getDescription() + " ";
StreamSource xmlStream= new StreamSource(new StringReader(xmlString));
// Build a validating SAX parser using the schema
SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
StreamSource ss= new StreamSource(newFile("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();
}
某个恶意用户可能会使用下面的字符串作为用户ID:
"joe
"->
joe
Administrator
I want to be an administrator
安全做法:白名单+安全的xml库
private void createXMLStream(BufferedOutputStreamoutStream, User user) throws IOException{
// Write XML string if userID contains alphanumeric and underscore characters only
if (!Pattern.matches("[_a-bA-B0-9]+", user.getUserId())){
// Handle format violation
}
if (!Pattern.matches("[_a-bA-B0-9]+", user.getDescription())){
// Handle format violation
}
String xmlString= ""+ user.getUserId()+ " operator "+ user.getDescription() + " ";
outStream.write(xmlString.getBytes());
outStream.flush();
}
public static void buidlXML(FileWriterwriter, User user) throwsIOException{
Document userDoc= DocumentHelper.createDocumen();
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();
}
}
Xml注入净化之后的数据
joe</id><role>Administrator</role><!—
operator
-->lt;description>Iwant to be an administrator
规则1.4:禁止直接使用不可信数据来记录日志
如果在记录的日志中包含未经校验的不可信数据,则可能导致日志注入漏洞。恶意用户会插入伪造的日志数据,从而让系统
管理员误以为这些日志数据是由系统记录的。例如,一个用户可能通过输入一个回车符和一个换行符(CRLF)序列来将一
条合法日志拆分成两条日志,其中每一条都可能会令人误解。
将未经净化的用户输入写入日志还可能会导致向信任边界之外泄露敏感数据,或者导致违反当地法律法规,在日志中写入和存储了某些类型的敏感数据。
if(loginSuccessful){
logger.severe("User login succeeded for: "+ username);
}else{
logger.severe("User login failed for: "+ username);
}
生成log:
david May 15, 2011 2:25:52 PM java.util.logging.LogManager$RootLogger.log
SEVERE: User login succeeded for: administrator
May 15, 2011 2:19:10 PM java.util.logging.LogManager$RootLogger log
SEVERE: User login failed for: david
May 15, 2011 2:25:52 PM java.util.logging.LogManager log
SEVERE: User login succeeded for: administrator
Username=David(生成标准日志)
May 15, 2011 2:19:10 PM java.util.logging.LogManager$RootLogger log
SEVERE: User login failed for: david
登录之前会对用户名输入进行净化,从而防止注入攻击
if(!Pattern.("[A-Za-z0-9_]+", username)){
// Unsanitized username
logger.severe("User login failed for unauthorized user");
}else if(loginSuccessful){
logger.severe("User login succeeded for: "+ username);
}else{
logger.severe("User login failed for: "+ username);
}
规则1.5:禁止向Runtime.exec() 方法传递不可信、未净化的数据
在执行任意系统命令或者外部程序时使用了未经校验的不可信输入,就会导致产生命令和参数注入漏洞。
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("cmd.exe /c dir" + args[0]);
// ...
}catch(Exception e){
// Handle errors
}
}
}
java DirList"dummy & echo bad"
dirdummy
echo bad
安全建议:
- 避免直接使用Runtime.exec(),采用标准的API替代运行系统命令来完成任务
- 白名单数据校验和数据净化
class DirList{
public static void main(String[] args){
if(args.length== 0){
System.out.println("No arguments");
System.exit(1);
}
try{
File dir= newFile(args[0]);
// the dir need to be validated
if (!validate(dir)) {
System.out.println("An illegal directory");
}else{
for (String file : dir.list()){
System.out.println(file);
}
}
}
}
}
类型 | 举例 | 常见注入模式和结果 |
---|---|---|
管道 | | | | shell_command -执行命令并返回命令输出信息 |
内联 | ; & |
; shell_command -执行命令并返回命令输出信息 & shell_command -执行命令并返回命令输出信息 |
逻辑运算符 | $ && || |
$(shell_command) -执行命令 && shell_command -执行命令并返回命令输出信息 || shell_command -执行命令并返回命令输出信息 |
重定向运算符 | > >> < |
> target_file -使用前面命令的输出信息写入目标文件 >> target_file -将前面命令的输出信息附加到目标文件 < target_file-将目标文件的内容发送到前面的命令 |
规则1.6:验证路径之前应该先将其标准化
绝对路径名或者相对路径名中可能会包含文件链接,对文件名标准化可以使得验证文件路径更加容易,同时可以防御目录遍历引发的安全漏洞。
public static void main(String[] args){
File f = newFile(System.getProperty("user.home")
+ System.getProperty("file.separator") + args[0]);
String absPath= f.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) throwsIOException{
File f = newFile(System.getProperty("user.home")
+ System.getProperty("file.separator") + args[0]);
String canonicalPath= f.getCanonicalPath();
if(!isInSecureDir(Paths.get(absPath))){
// Refer to Rule3.5 for the details of isInSecureDir()
throw new IllegalArgumentException();
}
if(!validate(absPath)){
// Validation
throw new IllegalArgumentException();
}
/* ... */
}
规则1.7:安全地从ZipInputStream提取文件
- 提取出的文件标准路径落在解压的目标目录之外-跨目录解压攻击,
- 是提取出的文件消耗过多的系统资源-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(newBufferedInputStream(fis));
ZipEntry entry;
while((entry = zis.getNextEntry()) != null){
System.out.println("Extracting: "+ entry);
int count;
byte data[] = newbyte[BUFFER];
// Write the files to the disk
FileOutputStreamfos= new FileOutputStream(entry.getName());
BufferedOutputStreamdest= 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构造器。它也未检查解压文件的资源消耗情况,它允许程序运行到操作完成或者本地资源被耗尽
示例
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= newFileInputStream(filename);
ZipInputStreamzis= newZipInputStream(newBufferedInputStream(fis));
ZipEntry entry;
try{
while((entry = zis.getNextEntry()) != null){
System.out.println("Extracting: "+ entry);
int count;
byte data[] = new byte[BUFFER];
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.");
}
FileOutputStreamfos= newFileOutputStream(entry.getName());
BufferedOutputStreamdest= new BufferedOutputStream(fos,BUFFER);
while((count = zis.read(data, 0, BUFFER)) != -1){
dest.write(data, 0, count);
}
dest.flush();
dest.close();
zis.closeEntry();
}
}
}
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 = newFile(intendedDir, entryName);
String canonicalPath= f.getCanonicalPath();
File iD= newFile(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= newZipInputStream(newBufferedInputStream(fis));
ZipEntryentry;
int entries = 0;
int total = 0;
byte[] data = newbyte[BUFFER];
try{
while((entry = zis.getNextEntry()) != null){
System.out.println("Extracting: "+ entry);
int count;
String name = sanitzeFileName(entry.getName(), ".");
FileOutputStream fos= newFileOutputStream(name);
BufferedOutputStream dest= new BufferedOutputStream(fos, BUFFER);
while (total + BUFFER<= && (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.");
}
}
}
}
规则1.8:禁止未经验证的用户输入直接输出到html界面
用户输入未经过验证直接输出到html界面容易导致xss注入攻击,该攻击方式可以盗取用户cookie信息,严重的可以形成xss蠕虫攻击漏洞,也可以结合其他的安全漏洞进一步进行攻击和破坏系统
反例:
String eid=request.getParameter("eid");
eid=StringEscapeUtils.escapeHtml(eid);//insufficient validation
...
ServletOutputStream out=response.getOutputStream();
out.print("Employee ID:"+eid);
...
out.close();
...
正例:
...
Statement stmt=conn.creatStatement();
ResultSet rs=stmt.executeQuery("select * from emp where id ="+eid);
if(rs != null){
rs.next();
String name=StringEscapeUtils.escapeHtml(rs.getString("name"));//insufficient validation
}
ServletOutputStream out =response.getOutputStream();
...
out.close();
...
数据类型 | 上下文 | 示例代码 | 防御措施 |
---|---|---|---|
string | HTML Body | UNTRUSTED DATA | HTML Entity编码 |
String | 安全HTML变量 | 1. HTML Attribute编码 2. 只把不可信数据放在安全白名单内的变量上(白名单在下文列出) 3. 严格地校验不安全变量,如background、id和name |
|
String | GET参数 | clickme | URL编码 |
String | 使用在src或href变量上的不可信URLs | clickme | |
String | CSS值 | Selection |
1. 使用CSS编码; 2. 使用CSS Hex编码; 3. 良好的CSS设计 |
String | JavaScript变量 | 1. 确保所有变量值都被引号括起来; 2. 使用JavaScript Hex编码 3. 使用JavaScript Unicode编码; 4. 避免使用“反斜杠转译”(\"、\'或者\) |
|
HTML | HTML Body | UNTRUSTED HTML |
[HTML校验(JSoup, AntiSamy, HTML Sanitizer)] |
String | DOM XSS |
|