安全开发规范:开发人员必须了解开发安全规范(一)(涉及安全问题,以及解决方法和代码实现)

6,668 阅读10分钟

  安全问题其实是很多程序员想了解又容易忽略的问题,但需要我们重视起来,提高应用程序的安全性。常出现的安全问题包括,程序接受数据可能来源于未经验证的用户,网络连接和其他不受信任的来源,如果未对程序接受数据进行校验,则可能会引发安全问题等等,具体也可以分成以下几方面:

  • 数据校验
  • 敏感信息
  • 加密算法
  • 序列化与反序列化
  • 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 校验跨信任边界传递的不可数据**

程序接受的不可信数据源跨越任边界传递必须经过内校验,包括输入和出校验。 不可信数据:用户、网络连接等源 不可信数据:用户、网络连接等源 数据入口:

  1. 终端计算机
  2. 互联网出入口
  3. 广域网出入口
  4. 公司对外发布服务的 DMZ服务器
  5. VPN和类似远程连接设备。 信任边界:根据威胁建模划分的信任边 如 web 应用的服务端;

规则 1.2:禁止直接使用不可信数据来拼SQL语句

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

  1. 使用参数化查询 (推荐使用)

  2. 对不可信数据进行校验

  3. 预编译处理

    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<Item> rs= (List<Item>) 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<Item> hrs= (List<Item>) 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<Item> rs= (List<Item>) 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<Item> rs= (List<Item>) 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<Item> rs= (List<Item>) sqlQuery.list();

Mybaits和ibaits的#和$

Mybaits:

<select id="getItems" parameterClass="MyClass" resultClass="Item">
	SELECT * FROM t_item
	WHERE owner = #userName# AND itemName= #itemName#
</select>

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:

<select id="getItems" parameterClass="MyClass"="items">
	SELECT * FROM t_item
	WHERE owner = #userName# AND itemName= '$itemName$'
</select>

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<Book> 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= "<user><role>operator</role><id>" + user.getUserId()+ "</id><description>" + user.getDescription() + "</description></user>";
outStream.write(xmlString.getBytes());
outStream.flush();}}

添加joeadministratorjoe

<user>
	<role>operator</role>
	<id>joe</id>
	<role>administrator</role>
	<id>joe</id>
	<description>I want to be an administrator</description>
</user>

XML Schema或者DTD校验,反例:

private void createXMLStream(BufferedOutputStreamoutStream, User user)throwsIOException{
	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

	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();
}

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
	<xs:element name="user">
		<xs:complexType>
			<xs:sequence>
				<xs:elementname="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>Administrator</role>
<!—"并使用如下字符串作为描述字段:
"-><description>I want to be an administrator"

<user>
<id>joe</id>
	<role>Administrator</role><!--</id> 
	<role>operator</role> <description> -->
	<description>I want to be an administrator</description>
</user>

安全做法:白名单+安全的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><id>"+ user.getUserId()+ "</id><role>operator</role><description>"+ user.getDescription() + "</description></user>";
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注入净化之后的数据

<user>
	<id>joe&lt;/id&gt;&lt;role&gt;Administrator&lt;/role&gt;&lt;!—</id>
	<role>operator</role>
	<description>--&gtlt;description&gt;Iwant to be an administrator</description>
</user>

规则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

安全建议:

  1. 避免直接使用Runtime.exec(),采用标准的API替代运行系统命令来完成任务

  2. 白名单数据校验和数据净化

     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提取文件

  1. 提取出的文件标准路径落在解压的目标目录之外-跨目录解压攻击,

  2. 是提取出的文件消耗过多的系统资源-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 <span>UNTRUSTED DATA</span> HTML Entity编码
String 安全HTML变量 <input type="text" name="fname" value="UNTRUSTED DATA"> 1. HTML Attribute编码
2. 只把不可信数据放在安全白名单内的变量上(白名单在下文列出)
3. 严格地校验不安全变量,如background、id和name
String GET参数 <a href="/site/search?value=UNTRUSTED DATA">clickme URL编码
String 使用在src或href变量上的不可信URLs <a href="UNTRUSTED URL">clickme</a><iframe src="UNTRUSTED URL" / 1. 对输入进行规范化;
2. URL校验;
3. URL安全性认证
4. 只允许使用http和https协议(避免使用JavaScript协议去打开一个新窗口)
5. HTML Attribute编码
String CSS值 <div style="width: UNTRUSTED DATA;">Selection</div> 1. 使用CSS编码;
2. 使用CSS Hex编码;
3. 良好的CSS设计
String JavaScript变量 <script> var currentValue='UNTRUSTED DATA';</script>
<script>someFunction('UNTRUSTED DATA');</script>
1. 确保所有变量值都被引号括起来;
2. 使用JavaScript Hex编码
3. 使用JavaScript Unicode编码;
4. 避免使用“反斜杠转译”(\"、\'或者\)
HTML HTML Body <div>UNTRUSTED HTML</div> [HTML校验(JSoup, AntiSamy, HTML Sanitizer)]
String DOM XSS <script> document.write("UNTRUSTED INPUT: " + document.location.hash);<script/> 基于DOM操作的XSS漏洞防御措施

1、输入过滤:客户端求情参数:包括用户输入,url参数、post参数。

  • 在产品形态上,针对不同输入类型,对输入做变量类型限制。
  • 字符串类型的数据,需要针对<、>、/、’、”、&五个字符进行实体化转义 2、输出编码:浏览器解析中html和js编码不一样,以及上下文场景多样,所以对于后台输出的变量,不同的上下文中渲染后端变量,转码不一样。
特殊字符 实体编码
& &amp
< &lt
> &gt
&quot
/ &#x2F
&#x27

规则1.9:禁止直接解析未验证的xml实体

  当允许引用外部实体时,若程序针对输入xml实体未验证,攻击者通过构造恶意内容,进行xxe注入攻击,可导致读取任意文件、执行系统命令、探测内网端口、攻击内网网站等危害 反例:

public void transform(InputStream xmlStream,OutputStream output)throws Exception{
	Transformer trans=null;
	TransformerFactory transFactory=TransformerFactory.newInstance();
	if(this.style!=null){
		trans=transFactory.newTranformer(this.style);
	}else{
		trans=transFactory.newTranformer();
	}
	/*********UTF-8***/
	trans.setOutputProperty(OutputKeys.ENCOOING,"UTF-8");
	Source source=new SAXSource(new InputSource(xmlStream));
	Result result=new StreamResult(output);
	trans.transform(source,result);
}

正例:

public void transform(InputStream xmlStream,OutputStream output)throws Exception{
	Transformer trans=null;
	TransformerFactory transFactory=TransformerFactory.newInstance();
	if(this.style!=null){
		//trans=transFactory.newTranformer(this.style);
		TransformerFactory trans=TransformerFactory.newInstance();
		trfactory.setFeature(XMLConstans.FEATURE_SECURE_PROCESSING,true);
		trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_DTD,"");
		trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_STYLESHEET,"");

	}else{
		//trans=transFactory.newTranformer();
		TransformerFactory trans=TransformerFactory.newInstance();
		trfactory.setFeature(XMLConstans.FEATURE_SECURE_PROCESSING,true);
		trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_DTD,"");
		trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_STYLESHEET,"");

	}
	/*********UTF-8***/
	trans.setOutputProperty(OutputKeys.ENCOOING,"UTF-8");
	Source source=new SAXSource(new InputSource(xmlStream));
	Result result=new StreamResult(output);
	trans.transform(source,result);
}

不同xml解析器防御xxe注入的方法:
XMLReader To protect a java org.xml.sax.XMLReader from XXE,do this:

XMLReader reader=XMLReaderFactory.createXMLReader();
reader.setFeatrue("http://apache.org/xml/features/disallow-doctype-decl",true);
reader.setFeatrue("http://apache.org/xml/features/disallow-doctype-decl",true);
//stictly required as DTDs should not be allowed at all ,per previous 
reader.setFeatrue("http://xml.org/sax/features/external-general-entilies",false);
reader.setFeatrue("http://xml.org/sax/features/external-parameter-entilies",false);

SAXReader To protect a java org.dom4j.io.SAXReader from XXE,do this:

saxReader.setFeatrue("http://apache.org/xml/features/disallow-doctype-decl",true);
saxReader.setFeatrue("http://xml.org/sax/features/external-general-entilies",false);
saxReader.setFeatrue("http://xml.org/sax/features/external-parameter-entilies",false);

后续会把涉及的其他安全问题全部写出来,可关注本人的下篇文章。


最后可关注公众号,一起学习,每天会分享干货,还有学习视频领取!