乐趣区

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

关注我,可以获取最新知识、经典面试题以及微服务技术分享

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

  • 数据校验
  • 敏感信息
  • 加密算法
  • 序列化与反序列化
  • 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();}}

添加 joe</id><role>administrator</role><id>joe

<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><role>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
/ | /
‘| ‘

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


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

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

退出移动版