Java代码漏洞检测-常见漏洞与修复建议
在工作中,我们的交付团队在交付项目时,可能会遇到甲方会使用一些第三方工具(奇安信等)对项目代码进行扫描,特别是一些对安全性要求比较高的企业,比如涉及到一些证券公司、银行、金融等。他们会在项目上线前进行代码安全检测,通过了对方才会发布上线。原文链接:https://blog.csdn.net/qq_39560975/article/details/131956264。
背景:
在工作中,项目交付团队在交付项目时,客户方可能会有项目安全要求,会使用一些第三方工具(奇安信等)对项目代码进行扫描,特别是一些对安全性要求比较高的企业,比如涉及到一些证券公司、银行、金融等。他们会在项目上线前进行代码安全检测,通过了对方才会发布上线。
银保等金融类企业信息安全处的安全扫描一般分为五项,主机漏洞,主机基线漏洞,代码检测漏洞,渗透测试漏洞,WEB扫描漏洞,以下漏洞为代码检漏洞.
代码检测常用工具:奇安信代码卫士
跨站脚本
高危:存储型XSS
存储型XSS是指应用程序通过Web请求获取不可信赖的数据,并且在未检验数据是否存在XSS代码的情况下,将其存入数据库。当程序下一次从数据库中获取该数据时,致使页面再次执行XSS代码。存储型XSS可以持续攻击用户,在用户提交了包含XSS代码的数据存储到数据库后,每当用户在浏览网页查询对应数据库中的数据时,那些包含XSS代码的数据就会在服务器解析并加载,当浏览器读到XSS代码后,会当做正常的HTML和JS解析并执行,于是发生存储型XSS攻击。
**例如**:下面JSP代码片段的功能是根据一个已知用户雇员ID(id)从数据库中查询出该用户的地址,并显示在JSP页面上。
<%
...
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from users where id =" + id);
String address = null;
if (rs != null) {
rs.next();
address = rs.getString("address");
}
%>
家庭地址: <%= address %>
如果address的值是由用户提供的,且存入数据库时没有进行合理的校验,那么攻击者就可以利用上面的代码进行存储型XSS攻击。
修复建议
为了避免存储型XSS攻击,建议采用以下方式进行防御:
1.对从数据库或其它后端数据存储获取不可信赖的数据进行合理验证(如年龄只能是数字),对特殊字符(如`<、>、'、"`以及`<script>、javascript`等进行过滤。
2.根据数据将要置于HTML上下文中的不同位置(HTML标签、HTML属性、JavaScript脚本、CSS、URL),对所有不可信数据进行恰当的输出编码。
**例如**:采用OWASP ESAPI对数据输出HTML上下文中不同位置,编码方法如下。
//HTML encode
ESAPI.encoder().encodeForHTML(inputData);
//HTML attribute encode
ESAPI.encoder().encodeForHTMLAttribute(inputData);
//JavaScript encode
ESAPI.encoder().encodeForJavaScript(inputData);
//CSS encode
ESAPI.encoder().encodeForCSS(inputData);
//URL encode
ESAPI.encoder().encodeForURL(inputData);
3.设置HttpOnly属性,避免攻击者利用跨站脚本漏洞进行Cookie劫持攻击。在Java EE中,给Cookie添加HttpOnly的代码如下:
response.setHeader("Set-Cookie","cookiename=cookievalue; path=/; Domain=domainvaule; Max-age=seconds; HttpOnly");
Cookie属性
Name:Cookie名
Value:Cookie值
Domain:Cookie的域。如果设成.test.com,那么子域名a.test.com和b.test.com,都可以使用.test.com的Cookie。
Path:Cookie的路径。如果设成/path/,则只有路径为/path/的页面可以访问该Cookie。如果设为/,则本域名下的所有页面都可以访问该Cookie。
Expires / Max-Age:Cookie的超时时间。若设置其值为一个时间,那么当到达此时间后,此Cookie失效。不设置的话默认值是Session,意思是Cookie会和Session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器)后,此Cookie失效。
Size:Cookie大小。
HttpOnly:若此属性为true,则只有在http请求头中会带有此Cookie的信息,而不能通过document.cookie来访问此Cookie。
Secure:设置是否只能通过https来传递此条Cookie。
SameSite:用来防止 CSRF 攻击和用户追踪。可以设置三个值:Strict、Lax 和 None。
Strict:Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
Lax:Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
None:关闭SameSite属性,提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
Priority:优先级。定义了三种优先级,Low/Medium/High,当Cookie数量超出时,低优先级的Cookie会被优先清除。
高危:反射型XSS
反射型XSS是指应用程序通过Web请求获取不可信赖的数据,并在未检验数据是否存在恶意代码的情况下,将其发送给用户。反射型XSS一般可以由攻击者构造带有恶意代码参数的URL来实现,在构造的URL地址被打开后,其中包含的恶意代码参数被浏览器解析和执行。这种攻击的特点是非持久化,必须用户点击包含恶意代码参数的链接时才会触发。
**例如**:下面JSP代码片段的功能是从HTTP请求中读取输入的用户名(username)并显示到页面。
<%String name= request.getParameter("username"); %>
姓名: <%= name%>
修复建议
如果name里有包含恶意代码,那么Web浏览器就会像显示HTTP响应那样执行该代码,应用程序将受到反射型XSS攻击。
为了避免反射型XSS攻击,建议采用以下方式进行防御:
1.对用户的输入进行合理验证(如年龄只能是数字),对特殊字符(如`<、>、'、"`以及`<script>、javascript`等进行过滤。
2.根据数据将要置于HTML上下文中的不同位置(HTML标签、HTML属性、JavaScript脚本、CSS、URL),对所有不可信数据进行恰当的输出编码。
**例如**:采用OWASP ESAPI对数据输出HTML上下文中不同位置,编码方法如下。
//HTML encode
ESAPI.encoder().encodeForHTML(inputData);
//HTML attribute encode
ESAPI.encoder().encodeForHTMLAttribute(inputData);
//JavaScript encode
ESAPI.encoder().encodeForJavaScript(inputData);
//CSS encode
ESAPI.encoder().encodeForCSS(inputData);
//URL encode
ESAPI.encoder().encodeForURL(inputData);
3.设置HttpOnly属性,避免攻击者利用跨站脚本漏洞进行Cookie劫持攻击。在Java EE中,给Cookie添加HttpOnly的代码如下:
response.setHeader("Set-Cookie","cookiename=cookievalue; path=/; Domain=domainvaule; Max-age=seconds; HttpOnly");
Cookie属性
Name:Cookie名
Value:Cookie值
Domain:Cookie的域。如果设成.test.com,那么子域名a.test.com和b.test.com,都可以使用.test.com的Cookie。
Path:Cookie的路径。如果设成/path/,则只有路径为/path/的页面可以访问该Cookie。如果设为/,则本域名下的所有页面都可以访问该Cookie。
Expires / Max-Age:Cookie的超时时间。若设置其值为一个时间,那么当到达此时间后,此Cookie失效。不设置的话默认值是Session,意思是Cookie会和Session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器)后,此Cookie失效。
Size:Cookie大小。
HttpOnly:若此属性为true,则只有在http请求头中会带有此Cookie的信息,而不能通过document.cookie来访问此Cookie。
Secure:设置是否只能通过https来传递此条Cookie。
SameSite:用来防止 CSRF 攻击和用户追踪。可以设置三个值:Strict、Lax 和 None。
Strict:Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
Lax:Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
None:关闭SameSite属性,提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
Priority:优先级。定义了三种优先级,Low/Medium/High,当Cookie数量超出时,低优先级的Cookie会被优先清除。
输入验证
高危:路径遍历
应用程序对用户可控制的输入未经合理校验,就传送给一个文件API。攻击者可能会使用一些特殊的字符(如`..`和`/`)摆脱受保护的限制,访问一些受保护的文件或目录。
**例如**:下面代码片段通过验证输入路径是否以`/safe_dir/`为开头,来判断是否进行创建、删除操作。
String path = getInputPath();
if (path.startsWith("/safe_dir/")){
File f = new File(path);
f.delete()
}
攻击者可能提供类似下面的输入:
`/safe_dir/../important.dat`
程序假定路径是有效的,因为它是以`/safe_dir/`开头的,但是`../`将导致程序删除`important.dat`文件的父目录。
修复建议
预防路径遍历的威胁,有以下三种方法:
1. 程序对非受信的用户输入做过滤和验证,对网站用户提交的文件路径进行硬编码或统一编码,过滤非法字符。
2. 对文件后缀进行白名单控制,拒绝包含了恶意的符号或空字节。
3. 合理配置Web服务器的目录访问权限。
高危:基于DOM的XSS[NodeJS]
应用程序的客户端代码从
document.location
request.url
document.URL
document.referrer
或其他任何攻击者可以修改的浏览器对象获取数据,如果未验证数据是否存在恶意代码的情况下,就将其动态更新到页面的DOM节点,应用程序将易于受到基于DOM的XSS攻击。
**例如**:下面的JavaScript代码片段可从URL中读取msg信息,并将其显示给用户。
var url=document.URL;
document.write(url.substring(url.indexOf("msg=")+4,url.length);
该段脚本解析URL,读取msg参数的值,并将其写入页面。如果攻击者设计一个恶意的URL,并以JavaScript代码作为msg参数,那么Web浏览器就会像显示HTTP响应那样执行该代码,应用程序将受到基于DOM的XSS攻击。
修复建议
基于DOM的XSS是将用户可控的JavaScript数据输出到HTML页面中而产生的漏洞,为了避免基于DOM的XSS攻击,避免将用户控制的数据直接输出到DOM或脚本中执行。如果不能避免,则应进行严格的过滤。
高危:重定向[Java]
重定向漏洞解决方案:Java中的URL重定向漏洞和防范方法-java教程-PHP中文网
应用程序允许未验证的用户输入控制重定向中的URL,攻击通过构建URL,使用户重定向到任意URL,利用这个漏洞可以诱使用户访问某个页面,挂马、密码记录、下载任意文件等,常被用来钓鱼。
**例如**:以下Servlet代码会接收前台的url参数,然后Servlet进行一系列业务操作后重定向到该链接,一般情况下这个链接可能是默认的,例如登陆处登陆成功后跳到首页,但是这种情况没有限制用户输入自定义的链接。
```java
public class RedirectServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException{
...
String query = request.getQueryString();
if (query.contains("url")) {
String url = request.getParameter("url");
response.sendRedirect(url);
}
}
}
```
常见的场景是受害者收到一封电子邮件,指示该用户打开`http://trusted.example.com/ecommerce/redirect.asp?url=www.wilyhacker.com`链接,用户有可能会打开该链接,因为他会认为这个链接将转到可信赖的站点。然而,一旦用户打开该链接,上面的代码会将浏览器重定向至`http://www.wilyhacker.com`。很多用户都被告知,要始终监视通过电子邮件收到的URL,以确保链接指向一个他们所熟知的可信赖站点。尽管如此,如果攻击者对目标URL进行16进制编码:`http://trusted.example.com/ecommerce/redirect.asp?url=%77%69%6C%79%68%61%63%6B%65%72%2E%63%6F%6D`以提高用户辨别URL的难度。更糟糕的是像这样的url通过例如QQ等程序进行传输的时候,这些程序的安全机制是不能识别url参数中的危险链接的。
修复建议
防止重定向漏洞的方法是创建一份合法URL列表,用户只能从中进行选择,进行重定向操作。
**例如**:以下Servlet代码先对url进行判断,再决定是否重定向到该链接。
```java
public class RedirectServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException{
...
String query = request.getQueryString();
if (query.contains("url")) {
String url = request.getParameter("url");
if(safeUrls.contains(url)){
response.sendRedirect(url);
}
...
}
}
}
```
高危:重定向[NodeJS]
应用程序允许未验证的用户输入控制重定向中的URL,可能会导致攻击者发动钓鱼攻击。
**例1**:以下JavaScript代码从用户输入表单的dest参数中读取目的URL,然后在新窗口中打开。
dsturl = myForm.dsturl.value;
window.open(dsturl,"newwin");
假如攻击者可以控制这个表单,那么用户就有可能打开一个恶意站点。
**例2**:以下是Node.js可能出现的。
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.redirect(req.url);
});
与例1一样假如攻击者控制了这个url,那么就会导致用户可能打开恶意站点。
修复建议
js和Node.js都要避免采用不可信数据源的数据来构造重定向的URL,如果业务逻辑需要涉及到用户输入,那么就创建一份合法URL列表,用户只能从中进行选择,进行重定向操作。
**例如**:以下代码中,用户只能输入数字来选择URL。
dst = myForm.dst.value;
if(dst == "1"){
dsturl = "http://www.1.com"
}else if(dst == "2"){
dsturl = "http://www.2.com"
}else{
dsturl = "http://www.3.com"
}
window.open(dsturl,"newwin");
中危:HTTP参数污染[Java]
HTTP参数污染(HPP)攻击根据HTTP协议中允许同名参数出现多次,如果程序没有正确地检查用户输入,攻击者可以在传输参数的时候传输key相同而value不同的参数,从而达到绕过某些防护与参数校验。
如果向Web应用程序提交参数,并且如果这些参数与现有参数的名称相同,Web应用程序可能会有下列一种反应:Web程序可能从第一个参数或最后一个参数提取数据,也可能从所有参数中获取数据并连接起来。
下面这个表格列举了一些常见的Web服务器对同样名称的参数出现多次的处理方式:
|Web服务器|获取的参数|
|:-:|:-:|
|ASP.NET/IIS |所有参数组成用逗号分隔的字符串|
|ASP/IIS |所有参数组成用逗号分隔的字符串|
|PHP/Apache |最后一个参数
|JSP Servlet/Apache Tomcat |第一个参数|
|JSP Servlet/Oracle Application Server 10g |第一个参数|
|IBM HTTP Server |第一个参数|
|mod_perl/Apache |所有参数成为数组|
Spring 4 起,`UriTemplate`类可执行用于阻止请求参数注入的编码。
**例如**:下列代码使用来自于HTTP 请求的输入来呈现两个超链接。
```java
...
String lang = request.getParameter("lang");
GetMethod get = new GetMethod("http://www.codesafe.com");
get.setQueryString("lang=" + lang + "&id=" + id);
get.execute();
...
```
攻击者可能会提供一个`lang`(例如`en&id=1`),然后攻击者将可以随意更改该`id`。
修复建议
在编译URL时,应对用户输入进行验证并过滤掉多余的参数(如`&`字符)。
中危:日志伪造[Java]
允许日志记录未经验证的用户输入,会导致日志伪造攻击。
**例如**:下面代码片段中,在接收到非法用户请求时,未进行任何数据验证情况下,记录用户的用户名。
```java
if (loginSuccessful) {
logger.severe("User login succeeded for: " + username);
} else {
logger.severe("User login failed for: " + username);
}
```
如果没有经过净化处理,那么可能会出现日志注入攻击。当`username`是jack时,日志信息如下:
```
2013-7-30 java.util.logging.LogManager log
Server:User login failed for:jack
```
如果日志`username`是一个多行字符串,如下所示:
```
jack
2013-7-30 java.util.logging.LogManager log
Server: User login succeeded for: Tom
```
那么日志中将记录错误信息,内容如下:
```
2013-7-30 java.util.logging.LogManager log
Server:User login failed for:jack
2013-7-30 java.util.logging.LogManager log
Server: User login succeeded for: Tom
```
修复建议
防止日志伪造攻击可以采用白名单、黑名单或验证用户输入数据的方式对不可信赖的数据进行校验。
**例如**:下面代码片段使用Encode工具类对`username`做安全编码处理,防止日志伪造攻击的发生。
```java
import org.owasp.encoder.Encode;
...
username = Encode.forJava(username);
if (loginSuccessful) {
logger.severe("User login succeeded for: " + username);
} else {
logger.severe("User login failed for: " + username);
}
```
中危:服务器端请求伪造[Java]
很多Web应用提供了从其他的服务器上获取数据的功能,例如用户指定URL让Web应用加载图片,下载文件等。如果恶意利用这个功能,可以让存在缺陷的Web应用作为代理攻击远程和本地的服务器。这种形式的攻击称为服务端请求伪造攻击`(Server-side Request Forgery, SSRF)`。攻击者利用SSRF可以实现的攻击主要有5种:
1. 可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务器的banner信息;
2. 攻击运行在内网或本地的应用程序(比如溢出);
3. 对内网web应用进行指纹识别,通过访问默认文件实现;
4. 攻击内外网的web应用,主要是使用get参数就可以实现的攻击(比如struts2,sqli等);
5. 利用file协议读取本地文件等。
**例如**:下面的代码片段中,攻击者将能够控制服务器连接URL。
```java
String url = request.getParameter("url");
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response1 = httpclient.execute(httpGet);
```
这种使用用户输入影响的资源可能存在风险。
修复建议
修复方案通常有下面5种:
1. 过滤返回信息,验证远程服务器对请求的响应是比较容易的方法。如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。
2. 统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。
3. 限制请求的端口为http常用的端口,比如80,443,8080,8090。
4. 禁用不需要的协议。仅仅允许http和https请求。可以防止类似于`file:///, gopher:// , ftp://` 等引起的问题。
5. 过滤内网ip,限制访问内网
中危:路径遍历:ZIP条目覆盖[Java]
程序在解压zip文件时,由于没有对文件名进行合法性的校验,而是直接将文件名拼接在待解压目录后面,导致可以将文件解压到正常解压缩路径之外并覆盖可执行文件,从而等待系统或用户调用他们实现代码执行(也可能是覆盖配置文件或其他可敏感文件)。
**例1**:以下代码示例解压zip文件。
```java
...
File sourceFile = new File(sourceName);
ZipFile zipFile = null;
try {
zipFile = new ZipFile(sourceFile,"UTF-8");
} catch (IOException exception) {
exception.printStackTrace();
System.out.println("解压文件不存在!");
}
Enumeration e = zipFile.getEntries();
while(e.hasMoreElements()) {
ZipEntry zipEntry = (ZipEntry)e.nextElement();
System.out.println(`zipEntry.getName()`);
File f = new File(targetFile,`zipEntry.getName()`);
f.getParentFile().mkdirs();
f.createNewFile();
InputStream is = zipFile.getInputStream(zipEntry);
FileOutputStream fos = new FileOutputStream(f);
int length = 0;
byte[] b = new byte[1024];
while((length=is.read(b, 0, 1024))!=-1) {
fos.write(b, 0, length);
}
is.close();
fos.close();
}
if (zipFile != null) {
zipFile.close();
}
```
代码示例未验证`zipEntry.getName()`,如果zip文件放在`/tmp/`目录中,zip条目为`../etc/hosts`,且应用程序在必要的权限下运行,则会导致系统的hosts文件被覆盖。
**例2**:以下代码使用`org.zeroturnaround.zip.ZipUtil`解压zip文件。
```java
public void unZip(String zipPath,String targetPath) {
...
ZipUtil.unpack(new File(zipPath), new File(targetPath));
...
}
```
代码示例中如果使用了`zt-zip 1.13`之前版本。攻击者可借助带有目录遍历名称的zip文件利用该漏洞写入任意文件。
修复建议
防止ZIP条目覆盖导致路径遍历可以通过判定zipEntry路径是否在指定路径内,或者使用一些最新的解压jar包来解压文件。
**例1**:下面的validateFileDir函数来限制zip条目文件路径只在许可的目录内。
```java
//限制文件在许可的目录内
public static String validateFileDir(String fileName, String permitDirectory) throws IOException{
File file= new File(fileName);
String canonicalFilePath= checkFile.getCanonicalPath();
File permitDir = new File(permitDirectory);
String canonicalPermitDir = permitDir.getCanonicalPath();
if (canonicalFilePath.startsWith(canonicalPermitDir)){
return canonicalFilePath;
}else{
throw new IllegalStateException("文件不在许可的目录内");
}
}
```
可以解压zip文件过程中使用`validateFileDir`函数来限制zip条目文件只在许可目录内。
**例2**:使用当前zt-zip 1.13以及之后的版本来进行解压文件。
中危:拒绝服务:正则表达式
正则表达式引擎分成两类:一类称为DFA(确定性有限状态自动机),另一类称为NFA(非确定性有限状态自动机)。Java使用的是NFA正则引擎,使用正则式和文本比较,每碰到一个字符,就把它跟正则式比较,匹配就记下来,然后接着往下比较。一旦不匹配,就会后退直到回到上一次匹配的地方。而不可信赖数据被传递至应用程序并作为正则表达式使用,可能导致线程过度使用 CPU 资源,从而导致拒绝服务攻击。
**例如**:
(e+)+
([a-zA-Z]+)*
(a|aa)+
(a|a?)+
^(a+)
+$
以`^(a+)+$`为例,该正则表达式对aaaax进行匹配时需要经历2^4^次尝试失败才会确定这个字符串不符合要求,对aaaaaaaaax进行匹配时则需要经历2^10^次尝试,随着长度的增加,尝试次数并不是线性增长而是指数型的增长,当长度达到20、30时就会大量消耗cpu导致拒绝服务。目前已知的正则表达式实现方法均无法避免这种漏洞,所有平台和语言都容易受到这种攻击。
Java对于正则的支持:
String类中,很多方法是专门用来支持正则:
split()
replaceAll()
replaceFirst()
matches()
……
java.util.regex包有两个类:Pattren类、Matcher类
修复建议:
不要将不可信赖数据作为正则表达式使用。
中危:访问权限修饰符控制
AccessibleObject类是Field、Method和Constructor对象的基类,能够允许反射对象修改访问权限修饰符,绕过由Java访问修饰符提供的访问控制检查。它让程序员能够更改私有字段或调用私有方法,这在通常情况下是不允许的。
**例如**:以下代码片段中,将Field将`accessible`标记设置为true。
Class clazz = User.class;
Field field = clazz.getField("name");
field.setAccessible(true);
修复建议:
通过有权限的类更改访问权限修饰符,并确保修改的访问权限修饰符参数不能被攻击者控制。
中危:直接绑定敏感字段
目前大部分WEB框架支持将HTTP请求参数与类的属性相匹配的而生成一个对象。因此,攻击者能够将值放入HTTP请求参数中从而绑定系统对象。
**例如**:在以下代码片段中, Spring MVC可以将 HTTP请求参数绑定到 User属性:
@RequestMapping("/login" )
public String login(User user) {
}
其中,User 类定义为:
public class User {
private String username;
private String address;
private int age;
private boolean admin;
}
当Spring MVC未配置为禁止绑定敏感属性,则攻击者可能会通过发送以下使普通用户变为管理员。
name=张三&address=北京&age=22&admin=true
修复建议:
当程序将非将HTTP请求参数直接绑定给对象时,应该要控制绑定到对象的属性,防止暴露敏感属性。
**例1**:在以下代码片段中,在 Spring MVC(3.0版本至最新)禁止绑定敏感属性。
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setDisallowedFields(new String[]{"admin"});
}
@RequestMapping("/login" )
public String login(User user) {
}
**例2**:在 Spring MVC(2.X版本)禁止绑定敏感属性。
@Override
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
binder.setDisallowedFields(new String[]{"admin"});
}
在使用`@RequestBody`注释参数的 Spring MVC应用程序中,绑定过程由`HttpMessageConverter`进行处理,这些实例使用Jackson和JAXB等库将 HTTP请求参数转换为Java对象。这些库提供了注释来控制应允许或禁止的字段。例如对于Jackson,可以使用`@JsonIgnore`注释禁止将某个字段绑定到请求。
**例3**:在以下代码片段中,Jackson禁止绑定敏感属性。
@RequestMapping(value="/add/user", method=RequestMethod.POST, consumes="text/html")
public void addEmployee(@RequestBody User user){
}
public class User {
private String username;
private String address;
@JsonIgnore
private boolean admin;
private int age;
}
同理,Jackson还可以使用`@JsonIgnoreProperties、@JsonIgnoreTyp和 @JsonInclude`等注解告诉框架忽略这些属性,使用JAXB使用`@XmlAccessorType、@XmlAttribute、@XmlElement和 @XmlTransient`等注解告诉框架忽略这些属性,然后使用`@XmlAttribute和@XmlElement`等注解选择应绑定的字段。
**例4**:在以下代码片段中,Jackson使用`@XmlAttribute`选择要绑定的字段。
@XmlRootElement
@XmlAccessorType(XmlAccessType.NONE)
public class User {
private String username;
private String address;
@JsonIgnore
private boolean admin;
private int age;
@XmlAttribute
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@XmlAttribute
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
private boolean isAdmin() {
return admin;
}
private void setAdmin(boolean admin) {
this.admin = admin;
}
}
**例5**:在以下代码片段中,在Struts可以将某个属性的`setter`方法设置为私有从而禁止绑定敏感属性。
private boolean admin;
private void setAdmin(boolean admin) {
this.admin = admin;
}
还有另一种方法是使用将 HTTP请求参数绑定到仅含有 Web表单或 API中定义的属性DTO对象中,再将其映射到User中,防止敏感字段暴露。
低危:拒绝服务:解析Double类型数据
程序调用Double的解析方法时,可能导致线程被挂起。`java.lang.Double.parseDouble()`方法解析位于2^(-1022)^ - 2^(-1075)^到2^(-1022)^ - 2^(-1076)^范围内的任何数字时可能导致线程被挂起,攻击者可以故意触发该漏洞执行拒绝服务攻击。该漏洞在java6 update24或更高版本中进行了修复。
**例如**:下面代码片段中,使用了易受攻击的方法。
Double d = Double.parseDouble(request.getParameter("d"));
攻击者可发送d参数值位于该范围(例如`0.0222507385850720119e-00306`)内的请求,致使程序在处理该请求时被挂起。
修复建议:
修复该缺陷的方式如下:
1. 验证传递给parseDouble数据的合法性。
2. 升级JDK版本到6 Update 24或更高版本。
低危:有风险的资源使用
拒绝服务是攻击者通过消耗应用资源,以致程序崩溃使得其他用户无法继续正常使用的一种攻击方式。
**例1**:下面代码片段中,解压文件前,未检查文件大小,攻击者可以通过提供一个超大文件来占用系统的计算资源从而实施DOS攻击。
static final int SIZE= 512;
public static void unZip(BufferedInputStream bin){
BufferedOutputStream bop= null;
ZipInputStream zi = new ZipInputStream(bin);
ZipEntry zentry;
while ((zentry= zi.getNextEntry()) != null) {
int count;
byte data[] = new byte[SIZE];
FileOutputStream fos = new FileOutputStream(zentry.getName());
bop= new BufferedOutputStream(fos, SIZE);
while ((count = zi.read(data, 0, SIZE)) != -1) {
bop.write(data, 0, count);
}
bop.flush();
bop.close();
}
zi.close();
}
**例2**:下面使用了`waitFor`方法,意味着直到该进程结束才能继续执行后续代码,不正确的处理输入输出流有可能发生死锁,导致程序持续浪费资源甚至崩溃。
process.waitFor();
修复建议:
拒绝服务攻击是一种滥用资源性的攻击。从代码角度来考虑,对于涉及到需要占用系统资源的外部数据而言,代码逻辑中应该包含严格校验,防止无限制的输入。另外,谨慎使用线程阻塞的API,防止浪费系统资源或发生系统崩溃。
**例如**:下面代码片段中,对解压文件进行验证,超过50M,将抛出异常。
static final int MAX= 0x3200000; // 50MB
// write the files to the disk, but only if file is not insanely big
if (entry.getSize() > MAX) {
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());
bop = new BufferedOutputStream(fos, SIZE);
while ((count = zis.read(data, 0, SIZE)) != -1) {
bop.write(data, 0, count);
}
低危:数据跨越信任边界
数据跨越信任边界是指,数据从一个不可信赖域存储到一个可信赖域导致程序错误信赖未验证的数据。
**例如**:下面代码片段中将用户输入的数据`name`存储到Http Session中。
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("userName");
HttpSession sess = req.getSession();
sess.setAttribute("user", name);
}
修复建议:
数据跨越信任边界时需要进行合理的验证,保证信赖域中数据是安全的。
低危:文件上传
允许用户上传文件可能导致危险的文件或代码被注入,并在服务器执行。
**例如**:
<input type="file">
修复建议:
- 如果应用程序不需要文件上传功能,应该禁用文件上传功能。
- 如果应用程序需要文件上传功能,可以参考以下建议:
1. 文件上传的目录设置为不可执行。
2. 采用白名单方式判断文件类型。在判断文件类型时,可采用MIME Type、 后缀检查等方式。
3. 使用随机数改写文件名和文件路径。 如果应用程序采用随机数改写了文件名和路径,可以很大程度上增加攻击的成本。
4. 对于图片的处理,可以采用压缩函数或者resize函数,在处理图片的同时,破坏图片中可能包含的脚本代码。
代码注入
高危:XML外部实体注入[Java]
XML作为一种使用较为广泛的数据传输格式,很多应用程序都包含有处理XML数据的代码,默认情况下,许多过时的或配置不当的 XML处理器都会对外部实体进行引用。如果攻击者可以上传 XML文档或者在 XML文档中添加恶意内容,通过易受攻击的代码、依赖项或集成,就能够攻击包含缺陷的XML处理器,从而造成拒绝服务攻击或者程序崩溃。
**例1**:下面代码片段使用Java原生的SAX对evil.xml文件进行解析
```java
private static void receiveXMLStream(InputStream inStream){
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser;
try {
saxParser = factory.newSAXParser();
saxParser.parse(inStream,new DefaultHandler());
...
} catch (ParserConfigurationException e) {
...
} catch (SAXException e) {
...
} catch (IOException e) {
...
}
}
```
**例2**:下面代码片段使用Java原生的DOM对evil.xml文件进行解析
```java
private static void receiveXMLStream(InputStream stream){
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder;
try {
documentBuilder = documentBuilderFactory.newDocumentBuilder();
documentBuilder.parse(stream);
...
} catch (ParserConfigurationException e) {
...
} catch (SAXException e) {
...
} catch (IOException e) {
...
}
}
```
当使用不可信数据来源作为文件输入流时
```java
String fileName = request.getParameter("filename");
receiveXMLStream(new FileInputStream(fileName));
```
如果evil.xml文件中包含以下文本
```xml
<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "file:/dev/tty">
<foo>bar</foo>
...
```
SAX或者DOM解析器会尝试访问在SYSTEM属性中标识的URL,这意味着它将读取本地/dev/tty文件的内容。在POSIX系统中,读取这个文件会导致程序阻塞,直到可以通过计算机控制台得到输入数据为止。这样,攻击者可以使用这个恶意的XML文件来导致系统挂起,程序会受到XML外部实体注入攻击。
修复建议
对于使用Java原生DOM和SAX以及DOM4J有以下两种修复方法(这里以`DocumentBuilderFactory`为例):
1.禁用DTD(doctypes),这样可以阻止几乎所有的XML实体攻击
```java
//仅限
Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
```
2.如果无法完全禁用DTD,可以采用以下操作一起使用
(1)禁用外部通用实体和参数实体(根据实际使用的jar包)
```java
// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities和http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities和http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
// JDK7 + - http://xml.org/sax/features/external-general-entities和http://xml.org/sax/features/external-parameter-entities
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities",false);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
```
(2)禁用外部DTD
```java
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",false);
```
(3)设置 XInclude 处理的状态为false,禁止实体扩展引用
```java
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
```
上述设置功能的方法`setFeature`也可以换成`setAttribute`,`setAttribute`方法用于用户在底层设置特定属性,使用方法和效果是一样的。
上述防御需要Java 7 Update 67,Java 8 Update 20或更高版本,因为`DocumentBuilderFactory`和`SAXParserFactory`的上述对策在早期Java版本中被破坏。
也可以从其他类上着手防范xxe漏洞:
如`XMLInputFactory、TransformerFactory、Validator、SchemaFactory、SAXTransformerFactory`。但这些类都是调用了`XMLConstants`中的属性,故需要支持JAXP1.5.使用StAX解析xml文件时,可使用下面方法进行防范(这里仅用`XMLInputFactory`类做为例子:其他类的防护手段类似):
```java
//这将完全禁用该工厂的DTD
xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD,false);
//禁用外部实体
xmlInputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities",false);
```
示例1还有一个解决方案是,定义一个`CustomResolver`类,这个类实现了`org.xml.sax.EntityResolver`接口。它可以让SAX应用定制对外部实体的处理。`setEntityResolver()`方法可以将对应的SAX驱动实例注册进来。这个定制的处理器使用的是一个为外部实体定义的简单的白名单。当输入不是任何指定的、安全地实体源路径时,`resolverEntity()`方法会返回一个空的`InputSource`对象。结果是,当解析恶意输入时,这个由自定义的解析器返回的空的`InputStream`对象会抛出`java.net.MalformedURLException`异常。需要注意的是,必须创建一个`XMLReader`对象,以便通过这个对象来设置自定义的实体解析器。
以下为示例代码:
```java
class CustomResolver implements EntityResolver {
public InputSource resolveEntity(String publicId, String systemId)
throws SAXException, IOException {
//check for known good entities
String entityPath = "/home/username/java/xxe/file";
if (systemId.equals(entityPath)) {
System.out.println("Resolving entity: " + publicId + " " + systemId);
return new InputSource(entityPath);
} else {
return new InputSource(); // Disallow unknown entities by returning a blank path
}
}
}
class XXE {
private static void receiveXMLStream(InputStream inStream,DefaultHandler defaultHandler)
throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = null;
try {
saxParser = factory.newSAXParser();
XMLReader reader = saxParser.getXMLReader();
reader.setEntityResolver(new CustomResolver());
reader.setErrorHandler(new DefaultHandler());
InputSource is = new InputSource(inStream);
reader.parse(is);
...
} catch (ParserConfigurationException e) {
...
} catch (SAXException e) {
...
} catch (IOException e) {
...
}
}
...
}
```
高危:动态解析代码[NodeJS]
程序运行时动态解析源代码指令将易于受到攻击。
**例如**:下面代码片段的作用是计算用户输入的表达式的值。
```javascript
...
expression = form.expInput.value;
result = eval(expression);
...
```
如果expression参数是合法的,程序将会正常运行。
**例如**:当该值为"1 + 1 * 2" 时,result变量被赋予的值将为 3。
如果攻击者提供一个恶意的输入,程序在没有进行合理校验的情况,将可以执行任意代码。
修复建议
在任何时候,都应尽可能地避免动态的解析源代码。如果程序的功能要求对代码进行动态解析,应用程序不应该直接执行和解析未验证的用户输入。建议创建一份合法操作和数据对象列表,用户可以指定其中的内容,并且只能从中进行选择。
**例如**:计算表达式的程序可以改为。
```javascript
...
expression = form.expInput.value;
if(/^[\d\-\+]*$/.test(expression)){
result = eval(expression);
}
...
```
中危:JavaScript劫持[NodeJS]
使用JavaScript传送敏感数据的应用程序可能会存在JavaScript劫持的漏洞,该漏洞允许未经授权的攻击者从一个易受攻击的应用程序中读取机密数据。
JavaScript劫持可以简单的理解为模拟授权的用户,窃取用户在服务器上的信息。Web浏览器使用同源策略(Same Origin Policy),以保护用户免受恶意网站的攻击。同源策略规定:如果要使用JavaScript来访问某个网页的内容的话,则JavaScript和网页必须都来源于相同的域。若不采取同源策略,恶意网站便可以使用受害者的客户端凭证来运行 JavaScript,从其他网站加载的敏感信息,并对这些信息进行处理,然后将其返回给攻击者。
使用JSON传输数据的JavaScript应用更容易受到JavaScript劫持攻击。由于JSON使用JavaScript语法的子集表示对象、数组、简单值,JSON本身可以被当做JavaScript执行,且使用*eval*()函数对JSON数据结构求值早被认为是存在风险的,其可能执行恶意代码。
修复建议
尽量避免跨域的数据传输,对于同域的数据传输使用xmlhttp的方式作为数据获取的方式。如果是跨域的数据传输,必须要对敏感的数据获取做权限认证,具体的方式可以包括:
1. referer的来源限制,利用前端referer的不可伪造性来保障请求数据的应用来源于可信的地方,此种方式力度较稀,完全依赖于referer,某些情况下(如存在XSS)可能导致被绕过。
2. 加入Token。利用Token对调用者的身份进行认证,这种方式对于调用者的身份会要求力度较细,但是一旦出现XSS也可能导致前端Token的泄露,从而导致保护失效。
3. 避免直接执行JavaScript响应:在响应中加入一些额外的字符。这些响应只有经过了修改,才能成功地转到JavaScript解释器进行处理。这样可以防止攻击者使用\<script\>标签来进行劫持。比如,可以给响应加上注释符号,使其无法直接执行;或者是在真实的响应前面,添加死循环语句,使其无法正常的直接运行。
中危:XML实体扩展注入[Java]
攻击者主要试图通过消耗目标程序的服务器环境来进行DOS攻击的。这种攻击基于XML实体扩展实现,通过在XML的DOCTYPE中创建自定义实体的定义实现,比如,这种定义可以在内存中生成一个比XML的原始允许大小大出很多的XML结构,来使这种攻击得以耗尽网络服务器正常有效运行的必需内存资源。
**例如**:下面代码片段尝试对外界输入的流进行解析
```java
class XMLEntityExpansion {
private static void receiveXMLStream(InputStream inStream,DefaultHandler defaultHandler)
throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
saxParser.parse(inStream, defaultHandler);
}
public static void main(String[] args)
throws ParserConfigurationException, SAXException, IOException {
receiveXMLStream(new FileInputStream("evil.xml"),
new DefaultHandler());
}
}
```
通过扩展XML自定义实体以达到耗尽服务器资源的目标的方法有以下几种。
1.通用实体扩展攻击:
通用实体扩展攻击同样被称为`Quadratic Blowup Attack`,使用这种方式时,自定义实体被定义为一个极长的字符串。当文件中大量使用这个实体时,该实体在每次调用时都会进行扩展,生成一个大幅超出原XML所需内存大小的XML结构。
```xml
<?xml version=""1.0""?>
<!DOCTYPE results [<!ENTITY long "SOME_SUPER_LONG_STRING">]>
<results>
<result>Now include &long; lots of times to expand
the in-memory size of this XML structure</result>
<result>&long;&long;&long;&long;&long;&long;&long;
&long;&long;&long;&long;&long;&long;&long;&long;
&long;&long;&long;&long;&long;&long;&long;&long;
&long;&long;&long;&long;&long;&long;&long;&long;
Keep it going...
&long;&long;&long;&long;&long;&long;&long;...</result>
</results>
```
通过平衡自定义实体字符串大小和文档主体内使用实体数量,可以创建一个扩展至占用服务器可预测内存空间大小的XML文档或字符串。通过这样重复请求来占用服务器内存,就可以发动一次成功的拒绝服务攻击。该方式的缺陷是,由于产生内存消耗效果是基于简单数乘的,因此初始XML文档或字符串本身需要足够大。
2.递归实体扩展攻击:
通用实体扩展攻击需要足够大的XML输入数据量,而递归实体扩展攻击的平均输入字节能产生更强力的攻击效果。这种攻击方式依赖于XML解析器来解析,从而完成小实体集的指数级增长。通过这种指数爆炸性增长方式,一个比通用实体扩展攻击使用小得多的输入数据量实际可增长得极大。因此这种方式被称为`XML Bomb`或是`Billion Laughs Attack`也是十分恰切的。
```xml
<?xml version=""1.0""?>
<!DOCTYPE results [
<!ENTITY x0 ""BOOM!"">
<!ENTITY x1 ""&x0;&x0;"">
<!ENTITY x2 ""&x1;&x1;"">
<!ENTITY x3 ""&x2;&x2;"">
<!-- Add the remaining sequence from x4...x100 (or boom) -->
<!ENTITY x99 ""&x98;&x98;"">
<!ENTITY boom ""&x99;&x99;"">
]>
<results>
<result>Explode in 3...2...1...&boom;</result>
</results>
```
XML Bomb攻击并不需要可能会被程序限制的大量XML数据输入。实体集像这样指数倍增长,最终形成的扩展后文本大小是初始&x0实体值的2的100次方倍。
3.远程实体扩展攻击:
常规和递归实体扩展攻击都依赖于XML文档类型定义中定义在本地的实体,但是攻击者同样可以进行外部实体定义。这很显然需要XML解析器能够像我们之前在描述XML外部实体注入式攻击(XXE)时遇到的那样,发起远程HTTP请求。而拒绝这种请求对你的XML解析器而言是一种基础的安保措施。因此,防御XXE攻击的措施同样适用于此类XML实体扩展攻击。
虽说可以通过上述方式进行防御,远程实体扩展通过使XML解析器发出远程HTTP请求来获得被引用实体的扩展值来进行攻击。返回结果将自行定义其他XML解析器必须另行HTTP请求的外部实体。如此一来,一些看似并无攻击性的请求会迅速脱离控制,并给服务器的可用资源带来负担。这种情况下,如果请求自包括一个递归扩展攻击,那最终结果会更加糟糕。
修复建议
对于使用Java原生DOM和SAX以及DOM4J有以下两种修复方法(这里以SAXParserFactory为例):
1.禁用DTD(doctypes),这样可以阻止几乎所有的XML实体攻击
```java
//仅限Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
```
2.如果无法完全禁用DTD,可以采用以下操作一起使用
(1)限制XML处理
```java
saxParserFactory.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
```
(2)禁用外部DTD
```java
saxParserFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",false);
```
(3)设置 XInclude 处理的状态为false,禁止实体扩展引用
```java
saxParserFactory.setXIncludeAware(false);
saxParserFactory.setExpandEntityReferences(false);
```
上述设置功能的方法`setFeature`也可以换成`setAttribute`,`setAttribute`方法用于用户在底层设置特定属性,使用方法和效果是一样的。
上述防御需要Java 7 Update 67,Java 8 Update 20或更高版本,因为`DocumentBuilderFactory`和`SAXParserFactory`的上述对策在早期Java版本中被破坏。
也可以从其他类上着手防范xxe漏洞:
如`XMLInputFactory、TransformerFactory、Validator、SchemaFactory、SAXTransformerFactory`。但这些类都是调用了`XMLConstants`中的属性,故需要支持JAXP1.5.使用STAX解析xml文件时,可使用下面方法进行防范(这里仅用`XMLInputFactory`类做为例子:其他类的防护手段类似):
```java
//这将完全禁用该工厂的DTD
xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD,false);
//禁用替换内部实体引用
xmlInputFactory.setProperty("javax.xml.stream.isReplacingEntityReferences",false);
```
中危:同源方法执行[Java]
JSONP可以利用callback的特性,来跨域传输数据,同域间也可以传输数据。攻击者可以先创建一个恶意网站,再在该站点构建一个页面,当用户从此页面点击进入另一此站点页面时,使第一个页面重定向至目标站点页面,然后第二个页面引用目标站点漏洞页面,则可以在该目标站点页面上执行任意 JavaScript函数。
**例如**:下面代码片段,构造回调函数名称可由用户控制的JSONP响应。
```java
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
public JsonpAdvice() {
super("callback");
}
}
```
当用户发送`GET /api/gerData?callback=myFunction` 的请求,控制器将生成如下响应:
```
HTTP/1.1 200 Ok
Content-Type: application/json; charset=utf-8
Date: Thu, 28 Jun 2018 11:27:53 GMT
Server: Apache
Content-Length: 300
Connection: Close
myFunction({<data>})
```
攻击者可以从JSONP端点加载该响应,从而执行`myFunction`函数。攻击者可以使用其他回调名称调用DOM来交互。 **例如**:`window.opener.document.body.someElemnt.firstChild.nextElementSibling.submit`可用于查找目标页面中的表格并进行提交。
修复建议
检查程序逻辑,是否需要通过外界数据控制回调函数,对数据进行过滤或者构建白名单来供用户选择,以避免攻击者执行任意代码。
中危:HTTP响应截断[Java]
程序从一个不可信赖的数据源获取数据,未进行验证就置于HTTP头文件中发给用户,可能会导致HTTP响应截断攻击。
**例如**:下列代码片段中,程序从HTTP请求中获取`author`的值,并将其设置到HTTP响应文件的cookie中。
String author = request.getParameter(AUTHOR_PARAM);
Cookie cookie = new Cookie("author", author);
cookie.setMaxAge(cookieExpiration);
response.addCookie(cookie);
如果请求中提交的是一个`Jane Smith`字符串,那么包含该cookie的HTTP响应可能表现为以下形式:
HTTP/1.1 200 OK
...
Set-Cookie: author=Jane Smith
那么如果攻击者提交的是一个恶意字符串,比如
`Wiley Hacker\r\nHTTP/1.1 200 OK\r\n...`
那么HTTP响应就会被分割成以下形式的两个响应:
HTTP/1.1 200 OK
...
Set-Cookie: author=Wiley Hacker
HTTP/1.1 200 OK
这样第二个响应已完全由攻击者控制,攻击者可以用所需的头文件和正文内容构建该响应实施攻击。
修复建议
防止HTTP响应截断攻击的最安全的方法是创建一份安全字符白名单,只接受完全由这些受认可的字符组成的输入出现在HTTP响应头文件中。
**例如**:以下代码片段中,验证了`author`的值是否由标准的字母数字字符组成。
String author = request.getParameter(AUTHOR_PARAM);
if (Pattern.matches("[0-9A-Za-z]+", author)) {
Cookie cookie = new Cookie("author", author);
cookie.setMaxAge(cookieExpiration);
response.addCookie(cookie);
}
中危:HTTP响应截断:Cookies[Java]
HTTP响应截断是由于应用程序未对用户提交的数据进行严格过滤,当用户恶意提交包含CR(回车,即URL编码%0d或r)和LF(换行符,即URL编码%0a或n)的HTTP请求,服务器可能会创建两个 HTTP 响应,攻击者可以控制第二个响应并加载攻击。攻击者可控制响应的内容构造 XSS 攻击,其中响应中包含恶意的 JavaScript 或其它代码在用户的浏览器中执行,也有可能让用户重定向到攻击者控制的Web内容或在用户的主机上执行恶意操作。
**例1**:下列代码片段中,程序读取HTTP请求参数AUTHOR_PARAM的值,并将它设置到HTTP响应的Cookie中。
```java
...
String author = request.getParameter(AUTHOR_PARAM);
...
Cookie cookie = new Cookie("author", author);
cookie.setMaxAge(cookieExpiration);
response.addCookie(cookie);
...
```
正常情况下,如果请求中提交参数是字符串`Jane Smith`,那么包含Cookie的HTTP响应头格式如下:
```java
HTTP/1.1 200 OK
...
Set-Cookie: author=Jane Smith
...
```
但是如果攻击者提交一个恶意字符串,比如`Wiley Hacker\r\nHTTP/1.1 200 OK\r\n...`,那么HTTP应答就会被分割成下面两个响应:
```java
HTTP/1.1 200 OK
...
Set-Cookie: author=Wiley Hacker
HTTP/1.1 200 OK
...
```
这样第二个响应已完全由攻击者控制,攻击者可以用所需的头文件和正文内容构建该响应实施攻击。
**例2**:以下Android代码会从Activity中直接取值,并将其置于一个HTTP响应的cookie头文件中,而该值很可能是受用户控制的。
```java
...
CookieManager cookieManager = CookieManager.getInstance();
Intent intent = activity.getIntent();
String value = intent.getStringExtra("value");
cookieManager.setCookie("url", value);
...
```
修复建议
防止HTTP响应截断攻击的最安全的方法是创建一份安全字符白名单,只接受完全由这些受认可的字符组成的输入出现在HTTP响应头文件中。
**例如**:以下代码片段中,使用URLEncoder工具类对`para`进行安全编码,防止攻击。
```java
import java.net.URLEncoder;
...
String para = request.getParameter(para);
para = URLEncoder.encode(para, "UTF-8");
Cookie cookie = new Cookie("para", para);
cookie.setMaxAge(expirationAge);
response.addCookie(cookie);
...
```
中危:有风险的SQL查询:MyBatis
SQL注入是一种数据库攻击手段。攻击者通过向应用程序提交恶意代码来改变原SQL语句的含义,进而执行任意SQL命令,达到入侵数据库乃至操作系统的目的。在Mybatis Mapper Xml中,`#`变量名称创建参数化查询SQL语句,不会导致SQL注入。而`$`变量名称直接使用SQL指令,而`$`变量名称直接使用SQL指令,将会存在一定风险,当SQL指令所需的数据来源于不可信赖的数据源时,可能会导致SQL注入。
**例如**:以下代码片段采用`$`变量名称动态地构造并执行了SQL查询。
<!--select user information by name-->
<select id="queryByUserName" resultMap="userResultMap" parameterType="String">
select * from db_user where user_name=${username}
</select>
如果攻击者能够替代username中的任意字符串,它们可以使用下面的关于username的字符串进行SQL注入。
`validuser' OR '1'='1`
当其注入到命令时,命令就会变成:
`select * from db_user where user_name ='validuser' OR '1'='1'`
即使所输入字符串不是来源于不可信赖的数据源,程序仍然存在着一定风险。
修复建议:
造成SQL注入攻击的根本原因在于攻击者可以改变SQL查询的上下文,使程序员原本要作为数据解析的数值,被篡改为命令了。防止SQL注入的方法如下:
1. 正确使用参数化API进行SQL查询。
2. 如果构造SQL指令时需要动态加入约束条件,可以通过创建一份合法字符串列表,使其对应于可能要加入到SQL指令中的不同元素,来避免SQL注入攻击。
**例如**:以下代码片段采用`#`变量名称,创建参数化查询SQL语句。
<!--select user information by name-->
<select id="queryByUserName" resultMap="userResultMap" parameterType="String">
select * from db_user where user_name=#{username}
</select>
中危:公式注入
微软公司的Excel或 Apache OpenOffice的Calc等电子表格都支持公式运算,如果攻击者控制了这类表格的数据,系统可能会导致任意命令执行或泄漏敏感信息。
**例如**:下面代码片段中,使用未经检查的数据来生成 CSV,并通过Spring控制器响应。
@RequestMapping(value = "/download/count.csv")
public ResponseEntity<String> service(String username) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", "application/csv; charset=utf-8");
responseHeaders.add("Content-Disposition", "attachment;filename=count.csv");
String data = getCSVDataForUserName(username);
return new ResponseEntity<>(data, responseHeaders, HttpStatus.OK);
}
当data中被注入`:=cmd|'/C calc.exe'!Z0`时,当用户打开此电子表格, Windows中的计算器将在其系统上运行。
修复建议
防止公式注入的方法如下:
1. 程序对非受信的用户输入数据进行净化,删除不安全的字符。
2. 创建一份安全字符串列表,限制用户只能输入该列表中的数据。
中危:资源注入
使用用户输入控制资源标识符,借此攻击者可以访问或修改其他受保护的系统资源。当满足以下两个条件时,就会发生资源注入:
1. 攻击者可以指定已使用的标识符来访问系统资源。例如:攻击者能够指定用来连接到网络资源的端口号。
2. 攻击者可以通过指定特定资源来获取某种权限,而这种权限在一般情况下是不可能获得的。例如:程序可能会允许攻击者把敏感信息传输到第三方服务器。
**例1**:下面的代码片段从HTTP请求获取端口号,并使用此端口号创建一个套接字,而不进行任何验证。使用代理的用户可以修改此端口并获得与服务器的直接连接。
String port = request.getParameter("port");
ServerSocket serverSocket = new ServerSocket(port);
Socket socket = serverSocket.accept();
这种利用用户输入影响的资源可能存在风险。
**例2**:下面的代码利用WebView的File域协议读取任意可读文件或受害应用的私有文件。
WebView webView=(WebView) findViewById(R.id.webView);
String url= getIntent().getData().toString();
webView.loadUrl(url);
查看hosts文件:`adb shell am start -n com.mytest/.MainActivity -d file:///system/etc/hosts `
查看应用私有文件:`adb shell am start -n /data/data/com.cn.test/databases/user.db`
修复建议:
为了避免资源注入漏洞攻击,可以采用黑名单或白名单策略。黑名单会有选择地拒绝或避免潜在的危险字符。但是,任何这样一份黑名单都不可能是完整的,而且将随着时间的推移而过时。比较好的方法是创建白名单,允许其中的字符出现在资源名称中,且只接受完全由这些被认可的字符所组成的输入。
中危:HTTP响应截断
程序从一个不可信赖的数据源获取数据,未进行验证就置于HTTP头中发给用户,可能会导致HTTP响应截断攻击。
**例如**:下列代码片段中,程序从HTTP请求中获取author的值,并将其设置到HTTP请求头的cookie中。
insecureInfo = document.URL;
document.cookie = "author=" + insecureInfo + ";expires="+ cookieExpiration;
author是来自用户输入,未经任何处理就存入了cookie中,使应用有可能受到cookie篡改的攻击。如果服务端解析了cookie,则可以造成其他攻击。
修复建议:
创建一份安全字符白名单,只接受完全由这些受认可的字符组成的输入出现在HTTP头中。
**例如**:以下代码片段中,验证了author的值是否由标准的字母数字字符组成。
insecureInfo = document.URL;
var exp = /[0-9A-Za-z]/;
var objExp = new RegExp(exp);
if (objExp.test(insecureInfo)==true) {
document.cookie = "author=" + insecureInfo + ";expires="+ cookieExpiration;
}
低危:有风险的反序列化
某些协议如RMI和JMX会在传输层后台使用Java序列化。当远程调用这些方法时,应用程序会在服务器上对参数进行反序列化,此时攻击者可以注入恶意对象。
**例1**:下列代码是public的RMI接口的示例,其包含的方法具有一个或多个参数。
public interface MyWebService extends Remote {
public Object doSomething (Object arg0) throws RemoteException;
}
**例2**:JMX MBeans也使用 Java序列化传输调用参数。在下面的示例中:MyManagedBean中的方法将会暴露给客户端。
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("com.example:type=MyManagedBean");
MyManagedBean myManagedBean = new MyManagedBean();
mBeanServer.registerMBean(myManagedBean, name);
而一些类如RedisTemplate使用默认的序列化器存在着不足,攻击者可以注入恶意对象,从而使反序列化产生非预期的对象,非预期的对象有可能带来意想不到的结果。
**例3**:下列代码是直接返回RedisTemplate,未对RedisTemplate设置序列化器。
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
修复建议:
检查程序逻辑,确定是否需要使用RMI和JMX等存在反序列化问题的协议,对于一些序列化器要设定可选择策略,以避免产生Java反序列化漏洞。
**例如**:以下代码对RedisTemplate设置了序列化器`Jackson2JsonRedisSerializer`。
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
template.setValueSerializer(jacksonSeial);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jacksonSeial);
template.afterPropertiesSet();
return template;
}
低危:JavaScript劫持:易受攻击的框架
使用JavaScript传送敏感数据的应用程序可能会存在JavaScript劫持的漏洞,该漏洞允许未经授权的攻击者从一个易受攻击的应用程序中读取机密数据。
JavaScript劫持可以简单的理解为模拟授权的用户,窃取用户在服务器上的信息。Web浏览器使用同源策略(Same Origin Policy),以保护用户免受恶意网站的攻击。同源策略规定:如果要使用JavaScript来访问某个网页的内容的话,则JavaScript和网页必须都来源于相同的域。若不采取同源策略,恶意网站便可以使用受害者的客户端凭证来运行 JavaScript,从其他网站加载的敏感信息,并对这些信息进行处理,然后将其返回给攻击者。
使用JSON传输数据的JavaScript应用更容易受到JavaScript劫持攻击。由于JSON使用JavaScript语法的子集表示对象、数组、简单值,JSON本身可以被当做JavaScript执行,且使用*eval*()函数对JSON数据结构求值早被认为是存在风险的,其可能执行恶意代码。
应该注意的是,某些过时的第三方Javascript框架也可能存在上述的问题,使用它们将导致Javascript劫持。
修复建议:
不使用过时的有风险的Javascript框架。
API误用
中危:依赖未经验证和完整性检查的cookie[Java]
应用程序当执行安全性非常重要的操作时,依赖Cookies的存在或它的值,而有没有正确地保证这些设置对相关的用户是有效的。攻击者可以很容易地在浏览器中或浏览器外的客户端代码修改cookie。依赖未经验证和完整性检查的cookie可以允许攻击者绕过验证,执行像SQL注入这样的注入攻击和跨站脚本攻击或者以不期待的方式修改输入。
**例如**:下面代码片段中,依赖未经验证和完整性检查的`cookie`进行身份鉴别。
```java
public class Example {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (int i=0; i< cookies.length; i++) {
if ("isAdmin".equals(cookies[i].getName())) {
String isAdmin = cookies[i].getValue();
// 用cookie存储的isAdmin字段来判断用户是否为管理员
if ("true".equals(isAdmin)) {
...
}
...
break;
}
}
}
...
}
```
修改建议
在做一个安全相关的决定时,依赖服务器端存储的数据,避免依赖客户端传过来的Cookie数据。
**例如**:下面代码片段中,根据服务器端存储的session进行身份鉴别。
```java
public class Example {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
String isAdmin = (String) session.getAttribute("isAdmin");
if ("true".equals(isAdmin)) { // 根据session判断用户是否为管理员
...
}
...
}
}
```
中危:不安全的框架绑定
目前大部分WEB框架支持将HTTP请求参数与类的属性相匹配的而生成一个对象。因此,攻击者能够将值放入HTTP请求参数中从而绑定系统对象。
**例如**:在以下代码片段中, Spring MVC可以将 HTTP请求参数绑定到User所有属性。
@RequestMapping("/login" )
public String login(User user) {
}
其中,User 类定义为:
public class User {
private String username;
private String address;
private boolean admin;
private int age;
}
修复建议:
当程序将非将HTTP请求参数直接绑定给对象时,应该要控制绑定到对象的属性,防止暴露所有属性。
在Spring MVC中,可以配置绑定器使用`setAllowedFields`和`setDisallowedFields`方法控制属性绑定过程以控制应绑定的属性。
**例1**:在以下代码片段中,在 Spring MVC(3.0版本至最新)通过`setDisallowedFields`方法禁止绑定敏感属性。
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setDisallowedFields(new String[]{"admin"});
}
@RequestMapping("/login" )
public String login(User user) {
}
**例2**:在 Spring MVC(2.X版本)通过`setDisallowedFields`方法禁止绑定敏感属性。
@Override
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
binder.setDisallowedFields(new String[]{"admin"});
}
而在使用 `@RequestBody`注释参数的 Spring MVC应用程序中,绑定过程由HttpMessageConverter进行处理,这些实例使用Jackson和JAXB等库将 HTTP请求参数转换为Java对象。这些库提供了注释来控制应允许或禁止的字段。例如对于Jackson,可以使用`@JsonIgnore`注释禁止将某个字段绑定到请求。
**例3**:在以下代码片段中,Jackson禁止绑定敏感属性。
@RequestMapping(value="/add/user", method=RequestMethod.POST, consumes="text/html")
public void addEmployee(@RequestBody User user){
}
public class User {
private String username;
private String address;
@JsonIgnore
private boolean admin;
private int age;
}
同理,Jackson还可以使用`@JsonIgnoreProperties、@JsonIgnoreType和 @JsonInclude`等注解告诉框架忽略这些属性,使用JAXB使用`@XmlAccessorType、@XmlAttribute、@XmlElement和 @XmlTransient`等注解告诉框架忽略这些属性,然后使用`@XmlAttribute和@XmlElement`等注解选择应绑定的字段。
**例4**:在以下代码片段中,Jackson使用`@XmlAttribute`选择要绑定的字段。
@XmlRootElement
@XmlAccessorType(XmlAccessType.NONE)
public class User {
private String username;
private String address;
@JsonIgnore
private boolean admin;
private int age;
@XmlAttribute
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@XmlAttribute
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
private boolean isAdmin() {
return admin;
}
private void setAdmin(boolean admin) {
this.admin = admin;
}
}
在Struts 1和 2 中,如果某个属性不应绑定到请求,则应将其 `setter`方法设置为私有即可。
**例5**:在以下代码片段中,在Struts可以将某个属性的`setter`方法设置为私有从而禁止绑定敏感属性。
private boolean admin;
private void setAdmin(boolean admin) {
this.admin = admin;
}
还有另一种方法是使用将 HTTP请求参数绑定到仅含有Web表单或API中定义的属性DTO对象中,再将其映射到User中,防止敏感字段暴露。
低危:忽略返回值
一般在开发过程中,程序员凭借经验可以断言某些用户事件或条件,例如某个函数永远不会执行失败。但是,攻击者往往会故意触发一些异常情况,导致冲破了程序员的断言,给系统带来了不稳定性,利用不正确的行为触发出发程序的漏洞。**例如**:程序调用删除权限函数,但不检查返回值判断是否成功删除权限,则程序将继续以更高权限运行。
**例如**:在下面的代码片段中,程序没有对`read()`返回值做判断。
int bytesToRead = 1024;
byte[] byteArray = new byte[bytesToRead];
streamFileInput = new FileInputStream("C:\\file.txt");
streamFileInput.read(byteArray);
修复建议:
程序应检查方法的返回值,确保方法返回的是期望的数据,再进行下一步操作。
低危:HTTP响应完成后继续操作输出流
转发`HttpServletRequest`、重定向`HttpServletResponse`或刷新servlet的输出流缓冲区会导致提交相关的数据流,程序后续再执行到缓冲区重置或数据流提交,将会抛出`IllegalStateException`异常。
此外,servlets允许使用`ServletOutputStream`或`PrintWriter`将数据写入响应数据流。如果在调用`getOutputStream()`之后再调用`getWriter()`或者反向调用,会导致抛出`IllegalStateException`异常,使其中断响应。
**例如**:在下面代码片段中,会在servlet的输出流缓冲区刷新之后进行重定向,会抛出`IllegalStateException`异常。
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
OutputStream os = response.getOutputStream();
os.flush();
os.close();
response.sendRedirect("http://www.codesafe.cn");
}
修复建议:
避免HTTP响应完成后继续操作输出流,应注意:
1. 提交servlet的输出流之后,不要重置数据流缓冲区或执行重新提交该数据流。
2. 避免在调用`getOutputStream()`之后调用`getWriter()`,或者在调用`getWriter()`后调用`getOutputStream()`。
低危:缺少对方法返回值的null检查
程序没有对有可能返回null的方法返回值进行检查,可能会导致NullPointException。
**例如**:下列代码片段中,未对`getenv()`方法的返回值data进行null检查,可能会抛出NullPointException。
String data = System.getenv("ADD");
if (data.equalsIgnoreCase("XXX") ){
...
}
修复建议:
程序应对可能返回null的方法的返回值进行检查,避免产生NullPointException。
密码管理
中危:不安全的随机数
Java API中提供了`java.util.Random`类实现`PRNG()`,该PRNG是可移植和可重复的,如果两个`java.util.Random`类的实例使用相同的种子,会在所有Java实现中生成相同的数值序列。
**例如**:下面代码片段中,使用了`java.util.Random`类,该类对每一个指定的种子值生成同一个序列。
import java.util.Random;
public static void main (String args[]) {
for (int i = 0; i < 10; i++) {
Random random = new Random(123456);
int number = random.nextInt(21);
}
}
修复建议:
在安全性要求较高的应用中,应使用更安全的随机数生成器,如`java.security.SecureRandom`类。
**例如**:下面代码片段中,使用`java.security.SecureRandom`来生成更安全的随机数。
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
public static void main (String args[]) {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
for (int i = 0; i < 10; i++) {
int number = random.nextInt(21);
}
} catch (NoSuchAlgorithmException nsae) {
}
}
中危:空密码
程序中使用了空的密码值,系统安全性将会受到威胁。
**例如**:下列代码中采用了空的密码。
public Connection getConnection(){
String url = "localhost";
String name = "admin";
String password = "";
}
修复建议:
程序中不应使用空的密码值,程序所需密码应从配置文件中获取加密的密码值。
**例如**:下列代码中从配置文件中获取经过加密的密码值并解密使用。
public Connection getConnection(){
String url = EncryptUtil.decrypt(PropertiesUtil.get("connection.url"));
String name = EncryptUtil.decrypt(PropertiesUtil.get("connection.username"));
String password = EncryptUtil.decrypt(PropertiesUtil.get("connection.password"));
}
中危:硬编码密码
程序中采用硬编码方式处理密码,一方面会降低系统安全性,另一方面不易于程序维护。
**例如**:下列代码中采用硬编码方式处理密码。
public class ConnectionConfig{
String url = "localhost";
String name = "admin";
String password = "123456";
}
修复建议:
程序中不应对密码进行硬编码,可以使用配置文件或数据库存储的方式来存储系统所需的数据;并且录入数据时,还可以在对敏感数据做加密处理之后再进行数据的录入。
**例如**:下列代码中从配置文件中获取经过加密的密码值并解密使用。
public class ConnectionConfig{
String url = EncryptUtil.decrypt(PropertiesUtil.get("connection.url"));
String name = EncryptUtil.decrypt(PropertiesUtil.get("connection.username"));
String password = EncryptUtil.decrypt(PropertiesUtil.get("connection.password"));
}
中危:弱加密
在安全性要求较高的系统中,使用不安全的加密算法(如DES、RC4、RC5等),将无法保证敏感数据的保密性。
**例如**:下面代码片段中,采用DES对数据进行加密。
BufferedReader bufread2 = null;
InputStreamReader inread2 = null;
try {
inread2 = new InputStreamReader(System.in);
bufread2 = new BufferedReader(inread2);
String str = bufread2.readLine();
/* FLAW: Insecure cryptographic algorithm (DES) */
Cipher des = Cipher.getInstance("DES");
SecretKey key = KeyGenerator.getInstance("DES").generateKey();
des.init(Cipher.ENCRYPT_MODE, key);
byte[] enc_str = des.doFinal(str.getBytes());
IO.writeLine(IO.toHex(enc_str));
} catch(IOException e) {
log_bsnk.warning("Error reading from console");
} finally{
}
修复建议:
在安全性要求较高的系统中,建议应使用安全的加密算法(如AES、RSA)对敏感数据进行加密。
**例如**:下面代码片段中,使用AES取代DES保证数据完整性。
BufferedReader bufread2 = null;
InputStreamReader inread2 = null;
try {
inread2 = new InputStreamReader(System.in);
bufread2 = new BufferedReader(inread2);
String str = bufread2.readLine();
/* FIX: Secure cryptographic algorithm (AES) */
Cipher aes = Cipher.getInstance("AES");
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(128);
SecretKey key = kg.generateKey();
aes.init(Cipher.ENCRYPT_MODE, key);
byte[] enc_str = aes.doFinal(str.getBytes());
IO.writeLine(IO.toHex(enc_str));
} catch(IOException e) {
log_gsnk.warning("Error reading from console");
} finally{
}
中危:配置文件中的明文密码
配置文件中采用明文存储密码,所有能够访问该文件的人都能访问该密码,将会降低系统安全性。
**例如**:下列配置文件中采用明文存储密码。
jdbc.username=user
jdbc.password=123456
修复建议:
即使不能阻止应用程序被那些可以访问配置文件的攻击者入侵,也可以通过加密密码提升攻击者入侵难度,故配置文件中的密码应进行加密存储。
**例如**:下列配置文件中采用jasypt加密的密码存储密码。
jdbc.username=user
jdbc.password=ENC(AaDVxaWVcgnN4lZswvK46QQkaxCfD7Xa)
中危:硬编码加密密钥
当程序中使用硬编码加密密钥时,所有项目开发人员都可以查看该密钥,甚至如果攻击者可以获取到程序class文件,可以通过反编译得到密钥,硬编码加密密钥会大大降低系统安全性。
**例如**:下列代码使用硬编码加密密钥执行AES加密。
private static String encryptionKey = "dfashsdsdfsdgagascv";
byte[] keyBytes = encryptionKey.getBytes();
SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
Cipher encryptCipher = Cipher.getInstance("AES");
encryptCipher.init(Cipher.ENCRYPT_MODE, key);
修复建议:
程序应采用不小于8个字节的随机生成的字符串作为密钥。
**例如**:以下代码使用KeyGenerator来生成密钥。
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] keyBytes = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
Cipher encryptCipher = Cipher.getInstance("AES");
encryptCipher.init(Cipher.ENCRYPT_MODE, key);
低危:注释中的密码
应用程序注释中保留密码等敏感信息,将使敏感信息对任何能够获取到该文件的人员可见。
修复建议:
应用程序注释中不应保留密码等敏感信息。
低危:不安全的哈希算法
在安全性要求较高的系统中,不应使用被业界公认的不安全的哈希算法(如MD2、MD4、MD5、SHA、SHA1等)来保证数据的完整性。
**例如**:下面代码片段中,采用MD5算法来保证数据的完整性。
byte[] b = str.getBytes();
MessageDigest md = null;
try {
md = MessageDigest.getInstance("MD5");
md.update(b);
}catch (NoSuchAlgorithmException e){
}
修复建议:
在安全性要求较高的系统中,应采用散列值>=224比特的SHA系列算法(如SHA-224、SHA-256、SHA-384和SHA-512)来保证敏感数据的完整性。
**例如**:下面代码片段中,使用SHA-256算法取代MD5算法保证数据完整性。
byte[] b = str.getBytes();
MessageDigest md = null;
try {
md = MessageDigest.getInstance("SHA-256");
md.update(b);
} catch (NoSuchAlgorithmException e) {
...
}
低危:弱加密:不安全的块密码加密模式
块密码又称为分组加密,一次加密明文中的一个块。将明文按一定的位长分组,明文组经过加密运算得到密文组,密文组经过解密运算(加密运算的逆运算),还原成明文组。这种加密算法共有四种操作模式用于描述如何重复地应用密码的单块操作来安全的转换大于块的数据量,分别是电子代码(ECB)、密码块链(CBC)、密码反馈(CFB)以及输出反馈(OFB)。其中ECB模式下相同的明文块总是会得到相同的密文,故不能抵挡回放攻击,而CBC模式则没有这个缺陷。
**例如**:以下代码将AES密码用于ECB模式。
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, createSecretKey(seed));
修复建议:
加密大于块的数据时,应该避免使用ECB模式,因为ECB模式下相同的明文块总是会得到相同的密文,有回放攻击的风险。CBC模式可以避免回放攻击,但是其效率较低,并且在和SSL一起使用时会造成严重风险。故可以改用`CCM(Counter with CBC-MAC)`模式,如果更注重性能,在可用的情况下则使用`GCM(Galois/Counter)`模式。
**例如**:以下代码将AES密码用于GCM模式。
GCMParameterSpec s = new GCMParameterSpec(tLen, src);
Cipher cipher = Cipher.getInstance("AES/OFB8/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, createSecretKey(seed), s);
低危:不安全的随机数
JavaScript的`Math.random()`实现PRNG(伪随机数序列发生器),该PRNG是可移植和可重复的,因此如果两个
`Math.random()`使用相同的种子,会生成相同的数值序列。
**例如**:下面的代码使用
Math.random()
创建的Token很容易被猜到。
function getToken (){
var token = Math.random();
return token;
}
修复建议:
JavaScript在安全性要求较高的环境中生成随机数,常规的建议是使用 Mozilla API 中的`window.crypto.getRandomValues()`函数,但这种方法受限于浏览器的兼容性,只在较新的版本(Chrome>=11.0, Firefox>=21, Edge, IE>=11, Safari>=3.1)可以使用。如果考虑到旧版本浏览器,则应在javascript之外处理PRNG功能。
**例如**:缺陷描述中的例子可以改写为。
function getToken (){
var array = new Uint32Array(1);
window.crypto.getRandomValues(array);
var token = array[0];
return token;
}
低危:注释中的密码
应用程序注释中保留密码等敏感信息,将使敏感信息对任何能够获取到该文件的人员可见。
**例如**:
//password:123
修复建议:
应用程序注释中不应保留密码等敏感信息。
低危:不安全的提交
使用HTTP GET的方式提交敏感信息如密码,可能造成密码被显示,记录等。
**例如**:以下示例使用HTTP GET的方式提交表单中的密码。
<form method="get">
密码: <input type="password" >
<input type="submit" name="" value="提交" >
</form>
修复建议:
避免使用HTTP GET的方式提交敏感数据,应使用HTTP POST传输敏感数据。
**例1**:以下示例通过HTTP POST的方式提交用户的密码。
<form method="post">
密码: <input type="password" >
<input type="submit" name="" value="提交" >
</form>
HTML5新增了一项功能,可以将formmethod属性作为 submit 和 image 输入标签一部分的功能,并且该属性值会覆盖相应form标签中 method 属性值。
**例2**:以下示例用户使用HTTP POST的方式提交密码,是由submit输入标签 formmethod 的属性值所指定。
<form method="get">
密码: <input type="password" >
<input type="submit" name="" value="提交" formmethod="post">
</form>
注意,如果将formmethod的值设为get,form的method的无论为什么,都会通过HTTP GET的方式提交表单。
资源管理
中危:路径访问控制[Java]
程序未进行恰当的访问控制,使用用户控制的值作为路径,可能会导致攻击者访问未经授权的文件。
**例如**:下面代码片段直接通过外部输入id下载相应图片。
```java
...
String id = request.getParameter("id");
File f = new File("/user/"+Integer.valueOf(id)+".png");
download(f);
...
```
以上代码将通过用户ID获取文件,其正常的使用方式是:
`http://www.example.com/download?id=00001`
当攻击者访问如下网址,服务器将会获取其他用户文件,导致信息泄露:
`http://www.example.com/download?id=00002`
修复建议
任何情况下都不允许用户在没有取得相应权限的情况下获取或修改文件。可以通过把当前被授权的用户信息作为获取文件方式来实现。
**例如**:下面代码片段通过当前用户id下载相应图片。
```java
...
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = (User)principal;
File f = new File("/user/"+user.getId()+".png");
download(f);
...
```
中危:单例成员变量[Java]
`javax.servlet.http.HttpServlet`和`org.apache.struts.action.Action`的对象不是线程安全的。对于同一个Servlet对象(或struts1的Action对象)的多个请求,Servlet对象(或struts1的Action对象)将在一个多线程的环境中并发执行。Web容器默认采用单实例多线程的方式来处理Http请求,这将导致Servlet对象(或struts1的Action对象)成员变量访问的线程安全问题。
**例如**:下面是一段JAVA EE的Servlet代码片断,变量`username`为成员变量,当LoginServlet的对象处理多个请求时,变量`username`将被多个请求共享,这将导致线程间数据泄露。
```java
...
import javax.servlet.http.HttpServlet;
public class LoginServlet extends HttpServlet {
String username;
protected void doPost(HttpServletRequest req,HttpServletResponse res) {
username = req.getParameter("username");
...
out.println( " 欢迎您,"+username+" !");
}
}
...
```
修复建议
使用Java EE的Servlet或struts1的Action时,必须保证其是线程安全的。修复方式举例如下:
**例如**:下面代码示例,使用局部变量,因为局部变量在每个线程中都有各自的实例,从而保证线程安全。
```java
...
import javax.servlet.http.HttpServlet;
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest req,HttpServletResponse res) {
String username = req.getParameter("username");
...
out.println("欢迎您,"+username+"!");
}
}
...
```
另外,也可以使用同步代码块访问servlet实例变量。但使用同步代码块可能会导致性能问题。
中危:反射型文件下载[NodeJS]
反射型文件下载(RFD)是一种攻击技术,通过从受信任的域(例如Google.com和Bing.com)虚拟下载文件,攻击者可以使攻击者获得对受害者计算机的完全访问权限。
**例如**:以下示例直接获取网络上的资源。
```javascript
jQuery.getJSON("http://example.com", function(a) {
});
```
修复建议
可以使用以下方式防止反射型文件的下载。
1. 使用`Content-Disposition`强行限制文件名;
2. 使用 CSRF 令牌;
3. 为所有API实现安全的请求头信息。
中危:格式化缺陷[Java]
格式化对象是非线程安全的,java.text.Format中的`parse()`和`format()`方法包含一个可导致用户看到其他用户数据的race condition。
**例如**:下面代码片段中,定义了一个成员变量(静态的日期格式对象)。
```java
public class DateFormat extends Thread{
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
private String name;
private String dateStr;
public DateFormat(String name, String dateStr) {
this.name = name;
this.dateStr = dateStr;
}
@Override
public void run() {
try {
Date date = sdf.parse(dateStr);
System.out.println("线程"+name+"运行日期:"+date);
} catch (ParseException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.execute(new DateFormat("A", "2017-06-10"));
executorService.execute(new DateFormat("B", "2016-06-06"));
executorService.shutdown();
}
}
```
如上代码中输出会有两种情况,一种情况是报错,还有一种情况是两个线程输出一致。出现这种情况的原因是因为SimpleDateFormat类内部有一个Calendar对象引用,它用来储存和这个SimpleDateFormat相关的日期信息。这样就会导致一个问题,如果SimpleDateFormat是static的, 那么多个thread之间就会共享SimpleDateFormat, 同时也是共享Calendar引用。在高并发的情况下,容易出现幻读成员变量的现象。
修复建议
有如下四种解决方法
1. 将格式化对象定义成局部变量,但是每调用一次方法意味创建一个格式化对象,浪费内存。
2. 方法加同步锁synchronized,在同一时刻,只有一个线程可以执行类中的某个方法。这样性能较差,每次都要等待锁释放后其他线程才能进入。
3. 使用第三方库joda-time,由第三方考虑线程不安全的问题。
4. 使用ThreadLocal:每个线程拥有自己的格式化对象。
中危:数据库访问控制
程序未进行恰当的访问控制,执行了一个包含用户控制主键的SQL语句,可能会导致攻击者访问未经授权的记录。
**例如**:下面代码片段中的SQL语句用于查询与指定标识符相匹配的清单。
id = Integer.decode(request.getParameter("invoiceID"));
String query = "SELECT * FROM invoices WHERE id = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setInt(1, id);
ResultSet results = stmt.execute();
在上面代码中,攻击者可以通过为`invoiceID`设置不同的值,获取所需的任何清单信息。
修复建议:
任何情况下都不允许用户在没有取得相应权限的情况下获取或修改数据库中的记录。可以通过把当前被授权的用户名作为查询语句的一部分来实现。
**例如**:下面代码片段中,通过把当前被授权的用户名作为查询语句的一部分来限制用户对清单的访问。
userName = ctx.getAuthenticatedUserName();
id = Integer.decode(request.getParameter("invoiceID"));
String query = "SELECT * FROM invoices WHERE id = ? AND user = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, id);
stmt.setString(2, userName);
ResultSet results = stmt.execute();
中危:资源未释放:Sockets[Java]
程序创建或分配Socket后,不进行合理释放,将会降低系统性能。攻击者可能会通过耗尽资源池的方式发起拒绝服务攻击。
**例如**:下面代码片段中,创建了一个套接字socket对象,未进行合理释放。
```java
public void getSocket(String host,int port){
try {
Socket socket = new Socket(host,port);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(reader.readLine()!=null){
...
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
```
修复建议
程序不应依赖于虚拟机的`finalize()`方法来自动回收Socket资源,而需要手动在finally代码块中做Socket资源的释放操作。
**例如**:下面代码片段中,使用完之前创建的socket套接字资源后,在finally代码块中进行了释放。
```java
public void getSocket(String host,int port){
Socket socket = null;
try {
socket = new Socket(host,port);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(reader.readLine()!=null){
...
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
```
中危:资源未释放:流
程序创建或分配流资源后,不进行合理释放,将会降低系统性能。攻击者可能会通过耗尽资源池的方式发起拒绝服务攻击。
**例如**:在下面Java方法中,创建I/O流对象后未进行合理释放,程序依靠Java虚拟机的垃圾回收机制释放I/O流资源,事实上,程序不能确定何时调用虚拟机的`finalize()`方法。在繁忙的程序环境下,可能导致Java虚拟机不能有效的使用I/O对象。
public void processFile(String filePath){
try {
FileInputStream fis = new FileInputStream(filePath);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr);
String line="";
while((line=br.readLine())!=null){
processLine(line);
}
} catch (FileNotFoundException e) {
log(e);
} catch (IOException e){
log(e);
}
}
程序不应依赖于Java虚拟机的`finalize()`方法来自动回收流资源,而需要手动在finally代码块中进行流资源的释放。
**例如**:下面代码片段中,在finally代码块中对流资源进行了合理的释放。
public void processFile(String filePath){
FileInputStream fis = null;
InputStreamReader isr = null;
BufferedReader br = null;
try {
fis = new FileInputStream(filePath);
isr = new InputStreamReader(fis);
br = new BufferedReader(isr);
String line="";
while((line=br.readLine())!=null){
//processLine(line);
}
} catch (FileNotFoundException e) {
//log(e);
} catch (IOException e){
//log(e);
}finally{
if(br!=null){
try {
br.close();
} catch (IOException e) {
//log(e);
}
}
if(isr!=null){
try {
isr.close();
} catch (IOException e) {
//log(e);
}
}
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
//log(e);
}
}
}
}
中危:使用不安全的target blank
在 `<a>`标签中使用target属性,值设置为`_blank`攻击者会针对`window.opener`API进行恶意行为的攻击,有可能导致钓鱼安全漏洞问题。
**例如**:以下示例使用的`targer`属性,但是没有设置`rel`属性。
<a href="www.example.com" target="_blank"/>
修复建议:
建议使用`target="_blank"`时,配合使用`rel="noopenner noreferrer"`。
**例如**:配合使用`rel`属性。
<a href="www.example.com" target="_blank" rel="noopenner noreferrer"/>
代码质量
高危:邮件服务器建立未加密的连接[Java]
邮件服务器使用的是未加密的连接,使用未加密网络发送敏感信息可能会被恶意攻击者通过拦截网络通信读取并修改信息。
**例如**:下面代码示例中:AuthenticatingSMTPClient未使用TLS进行数据的发送。
```java
AuthenticatingSMTPClient client = new AuthenticatingSMTPClient();
client.connect(url);
...
Writer writer = client.sendMessageData();
...
```
修复建议
可使用SSL/TLS对通过网络发送的所有数据进行加密,或者将现有的未加密连接升级到SSL/TLS。
**例如**:下面代码示例中:AuthenticatingSMTPClient未使用TLS进行数据的发送。
```java
AuthenticatingSMTPClient client = new AuthenticatingSMTPClient();
client.connect(url);
...
client.execTLS();
Writer writer = client.sendMessageData();
...
```
中危:webpack源码泄露[NodeJS]
通过打印或日志记录功能将系统数据或调试信息发送到本地文件、控制台或屏幕时,就会发生内部信息泄露。
webpack项目源码在泄漏的情况下,可以在浏览器控制台中的Sources—> Page—> webpack://中查看源代码。
使用webpack打包应用程序会在网站js同目录下生成js.map文件。
1.直接查看网站的js文件,可以在末尾处有js.map文件名。
2.直接在当前访问的js后面拼接.map即可访问下载
通过以上两种方式可以判断目标网站存在webpack源码泄露问题。
js.map文件可以配合使用reverse-sourcemap或SourceDetector进行js.map文件还原操作,从而泄露源代码
修复建议
1.在项目路径下修改config/index.js中build对象productionSourceMap: false;
2.建议删除或禁止访问正式环境中的js.map文件;
中危:不安全的信息传输[Java]
程序使用HTTP、FTP等协议进行通信,未对通信信息进行加密和验证,可能会受到危害,尤其是移动设备利用WiFi连接不安全的、公共的无线网络。
**例如**:下面代码片段中表示将使用HTTP进行连接,传递的敏感信息容易被攻击者窃取。
```java
...
HttpHost httppost = HttpHost(inetAddress,port,"http");
...
```
修复建议
程序应尽可能基于HTTPS等安全协议与服务器进行通信,尤其是进行敏感信息的传递。
中危:未设置httpOnly[NodeJS]
目前主流的浏览器都支持cookie的httpOnly属性,该属性设置为true的时候表示cookie只能由web服务器访问,如果不设置该属性的话默认为false这样攻击者就可以很容易的访问到cookie信息,从而获取authentication标记或者一些会话信息。
**例如**:下面代码段创建cookie,但是没有设置httpOnly属性。
```javascript
var info = {domain: 'secure.com', path: '/userInfo',secure:true};
response.cookie("cookieName",data,info)
```
修复建议
在发送cookie的时候应该设置httpOnly属性,
**例如**:
```javascript
var info = {domain: 'secure.com', path: '/userInfo',secure:true,httpOnly:true};
response.cookie("cookieName",data,info)
```
中危:Cookie:未经过SSL加密[Java]
使用未加密的网络传输通道发送Cookie易受到网络截取攻击。创建Cookie时,需要将secure标记设置为true,表明浏览器必须通过HTTPS来加密传输Cookie。
**例如**:下面代码片段中,将Cookie添加到HTTP响应中时,未设置secure标记。
```java
...
Cookie cookie = new Cookie("SessionID", sessionID);
response.addCookie(cookie);
...
```
修改建议
为Cookie添加secure标记,要求浏览器通过HTTPS发送Cookie,这样有助于保护Cookie值的保密性。
中危:硬编码IP[NodeJS]
程序中采用硬编码方式处理IP地址,一方面会降低系统安全性,另一方面不易于程序维护。
**例如**:下列代码中采用硬编码方式处理IP地址。
```javascript
var ConnectionConfig = {
"url" : "42.81.56.36";
"name" : "admin";
"password" : "123456";
...
}
```
修复建议
程序中不应对IP地址进行硬编码,可以使用配置文件或数据库存储的方式来存储系统所需的数据;并且录入数据时,还可以做加密处理之后再进行数据的录入,从而防止敏感数据泄露。
**例如**:下列代码中从配置文件中获取经过加密的IP地址并解密使用。
```javascript
var ConnectionConfig = {
"url" : EncryptUtil.decrypt(PropertiesUtil.get("connection.url"));;
"name" : EncryptUtil.decrypt(PropertiesUtil.get("connection.username"));
"password" : EncryptUtil.decrypt(PropertiesUtil.get("connection.password"));
...
}
```
中危:硬编码手机号码[NodeJS]
程序中采用硬编码方式处理手机号码,一方面会降低系统安全性,另一方面不易于程序维护。
**例如**:下面是某系统对用户信息管理的源码片段。
```javascript
var emps = [
{
"id": "1001",
"name": "周卫国",
"age": 22,
"tel": "18911295900",
"email": "meiya@cn-meiya.com"
},
{
"id": "1002",
"name": "马东强",
"age": 30,
"tel": "18911295766",
"email": "meiyahr@163.com"
},
{
"id": "1003",
"name": "黄毅",
"age": 27,
"tel": "18911297366",
"email": "chingpeplo@sina.com"
}
]
```
在上面的源码片段中,程序直接将用户的手机号码等个人信息以硬编码的方式进行存储,这样做会让数据与程序直接绑定,提高了系统的耦合性,使得系统和数据的维护变得困难。同时,如果对程序的执行文件做反编译操作,很容易就能够得到雇员的真实信息,导致敏感数据泄露,大大降低了系统的安全性。
修复建议
程序中不应对手机号进行硬编码,可以使用配置文件或数据库存储的方式来存储系统所需的数据;并且录入数据时,还可以做加密处理之后再进行数据的录入,从而防止敏感数据泄露。
**例如**:下面代码采用了从后端接口查询手机号并解密。
```javascript
var emps = [
{
"id": decrypt(getIdByUserId(1001)),
"name": "周卫国",
"age": 22,
"tel": decrypt(getPhoneByUserId(1001)),
"email": decrypt(getEmailByUserId(1001)),
},
{
"id": decrypt(getIdByUserId(1002)),
"name": "马东强",
"age": 30,
"tel": decrypt(getPhoneByUserId(1002)),
"email": decrypt(getEmailByUserId(1001)),
},
{
"id": decrypt(getIdByUserId(1003)),
"name": "黄毅",
"age": 27,
"tel": decrypt(getPhoneByUserId(1003)),
"email": decrypt(getEmailByUserId(1001)),
}
]
```
中危:硬编码邮箱地址[Java]
程序中采用了硬编码方式处理邮箱地址,一方面会降低系统安全性,另一方面不易于程序维护。
**例如**:下面代码采用了硬编码方式处理邮箱地址。
```java
public class UserDAO {
private static Map<Integer, User> users = new LinkedHashMap<Integer, User>();
static {
users.put(1001, new User(1001, "周卫国", 22, "18911295900", "meiya@cn-meiya.com"));
users.put(1002, new User(1002, "马东强", 30, "18911295766", "meiyahr@163.com"));
users.put(1003, new User(1003, "黄毅", 27, "18911297366", "chingpeplo@sina.com"));
// ...
}
// 用户信息的增删改查方法
// ...
}
public class User {
private int id;
private String name;
private int age;
private String tel;
private String email;
public User() {}
public User(int id, String name, int age, String tel, String email) {
this.id = id;
this.name = name;
this.age = age;
this.tel = tel;
this.email = email;
}
// Getter and Setter
// ...
}
```
在上面的源码片段中,程序直接将用户的邮箱地址等个人信息以硬编码的方式进行存储,这样做会让数据与程序直接绑定,提高了系统的耦合性,使得系统和数据的维护变得困难。同时,如果对程序的执行文件做反编译操作,很容易就能够得到雇员的真实信息,导致敏感数据泄露,大大降低了系统的安全性。
修复建议
程序中不应对邮箱地址进行硬编码,可以使用配置文件或数据库存储的方式来存储系统所需的数据;并且录入数据时,还可以在对敏感数据做加密处理之后再进行数据的录入。
**例如**:下面代码采用了从数据库查询邮箱地址并解密。
```java
public class UserDAO {
private static Map<Integer, User> users = new LinkedHashMap<Integer, User>();
static {
users.put(1001, new User(1001, "周卫国", 22, EncryptUtil.encrypt(phoneDAO.getPhoneById(1001)), EncryptUtil.encrypt(emailDAO.getEmailById(1001))));
users.put(1002, new User(1002, "马东强", 30, EncryptUtil.encrypt(phoneDAO.getPhoneById(1002)), EncryptUtil.encrypt(emailDAO.getEmailById(1002))));
users.put(1003, new User(1003, "黄毅", 27, EncryptUtil.encrypt(phoneDAO.getPhoneById(1003)), EncryptUtil.encrypt(emailDAO.getEmailById(1003))));
// ...
}
// 用户信息的增删改查方法
// ...
}
public class User {
private int id;
private String name;
private int age;
private String tel;
private String email;
public User() {}
public User(int id, String name, int age, String tel, String email) {
this.id = id;
this.name = name;
this.age = age;
this.tel = tel;
this.email = email;
}
// Getter and Setter
// ...
}
```
中危:请求参数自动填充数据库实体[Java]
持久性框架(如 Hibernate或JPA)通常会自动更新数据库实体,当系统允许请求参数可以直接自动填充数据库持久实体时,攻击者将能够通过提供附加的请求参数向数据库中注入非预期的值。
**例如**:在下面的示例中:Order、User都是 Hibernate持久类。
```java
public class Order {
String orderId;
List goods;
User User;
...
}
public class User {
String userId;
String username;
String password;
...
}
```
OrderController是处理该请求的Spring控制器类:
```java
@Controller
public class OrderController {
...
@RequestMapping("/updateOrder")
public String updateOrder(Order order) {
...
session.save(order);
}
}
```
当系统允许请求自动绑定到数据库实体时,攻击者可以通过在该请求中添加如下请求参数来更新其他用户的密码:`https://www.shopwebsite.com/myOrder/updateOrder?order.user.userId=1234&order.user.password=newpassword`
修复建议
程序对非受信的用户输入数据进行净化,不要直接填充给数据库实体。
中危:非静态内部类实现序列化接口[Java]
非静态内部类(包括普通内部类、Local内部类和匿名内部类)的实例持有一个外部类实例的隐式引用。非静态内部类实现序列化接口,当其被序列化时会出现运行时异常或泄露外部类中的信息。
下面代码示例中:内部类间接实现序列化接口
```java
public class SuperSer implements Serializable{
//...
}
public class OuterSer{
private int rank;
class InnerSer extends SuperSer{
protected String name;
}
}
```
**例1**:下面代码示例中:内部类实现或间接实现序列化接口,外部类没有实现序列化,当内部类被序列化时,程序会报出NotSerializableException
```java
public class OuterSer{
private int rank;
class InnerSer implements Serializable{
protected String name;
}
}
```
修复建议
内部类不要实现或间接实现Serializable接口,或将内部类声明为静态的。
**例1**:下面代码示例中:内部类没有实现序列化接口。
```java
public class OuterSer implements Serializable {
private int rank;
class InnerSer{
protected String name;
}
}
```
**例2**:下面代码示例中:将内部类声明为静态内部类。
```java
public class OuterSer implements Serializable {
private int rank;
static class InnerSer implements Serializable{
protected String name;
}
}
```
中危:使用==或!=比较基本数据类型的包装类
不能直接使用`==`或`!=`操作符来比较的两个基本数据类型的包装类型的值,因为这些操作符比较的是对象的引用而不是对象的值。
不过由于Java的缓存机制,所以如果基本类型的包装类是一个整数且在-128和127之间,或是布尔类型true或false,或者是'\u0000'和'\u007f'之间的字符文本,可以使用`==`或`!=`进行比较。也就是说,如果使用了基本类型的包装类型(除去Boolean或Byte),则会缓存或记住一个值区间。对于值区间内的值,使用`==`或`!=`会返回正确的值,而对于值区间外的值,将返回对象地址的比较结果。
**例如**:在这个不符合规范的代码示例中:使用`==`操作符来比较两个Integer对象的值。然而,这个`==`操作比较的是对象的引用,而不是对象的值。
public class Wrapper {
public static void main(String[] args){
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 1000;
Integer i4 = 1000;
System.out.println(i1 == i2);
System.out.println(i1 != i2);
System.out.println(i3 == i4);
System.out.println(i3 != i4);
}
}
Integer类只能保证缓存介入-128~127的整型数值,当使用相等操作符的时候,这会导致在这个范围之外的等价数值的比较是不相等的。比如,在那些不缓存任何值的JVM虚拟机中,运行程序会产生以下结果:
true
false
false
true
修复建议:
不能直接使用`==`或`!=`操作符来比较的两个基本数据类型的包装类的值,因为这些操作符比较的是对象的引用而不是对象的值。
符合规范的方案使用`equals()`而不是`==`操作符来比较两个对象的值。这个程序在所有的平台运行时都会打印true、false、true、false的结果,这符合预期。
**例如**:以下代码使用`equals()`方法比较两个Integer对象的值。
public class Wrapper {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 1000;
Integer i4 = 1000;
System.out.println(i1.equals(i2));
System.out.println(!i1.equals(i2));
System.out.println(i3.equals(i4));
System.out.println(!i3.equals(i4));
}
}
中危:比较Locale相关的数据未指定适当的Locale
在比较数据时,如果可能与Locale设置相关,则应指定相应的Locale。
**例1**:下面代码示例中:使用了Locale相关的`String.toUpperCase()`将字符转换为大写字符。在英文Locale中,会将`title`转换为`TITLE`;在土耳其Locale中,会将`title`转换为`T?TLE`,其中的`?`是拉丁字母的`I`。
"title".toUpperCase();
"TITLE".toLowerCase();
修复建议:
在比较数据时,如果可能与Locale设置相关,则应指定相应的Locale。
**例1**:下面代码示例将Locale设置为英文,从而避免产生意外的问题。
"title".toUpperCase(Locale.ENGLISH);
"TITLE".toLowerCase(Locale.ENGLISH);
**例2**:可以在对字符串处理前,将默认的Locale设置为English。
Locale.setDefault(Locale.ENGLISH);
"title".toUpperCase();
"TITLE".toLowerCase();
中危:null引用
程序间接引用了可能为null的变量,从而引发空指针异常。
**例如**:下面代码片段中,在使用变量data之前没有判断它是否为null。
Data data = null
data.setId(id);
修复建议:
程序在间接引用可能为null的对象之前应对其进行判断。
**例如**:下面代码片段中,程序data设置为null,使用之前进行判断是否为null。
Data data = null
if(data != null){
data.setId(id);
}
中危:系统信息泄露:外部[Java]
系统数据或调试信息通过网络流向远程机器时,发生外部信息泄露。
**例如**:以下代码泄露了HTTP响应中的异常信息:
```java
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
PrintWriter out = res.getWriter();
try {
//...
} catch (Exception e) {
out.println(e.getMessage());
}
}
```
修复建议
程序不应通过系统输出流或程序日志将系统数据或调试信息输出程序。
中危:系统信息泄露:Session传递
在localStorage和sessionStorage之间传输值会不知不觉地暴露敏感信息。
实现了Web Storage的HTML5浏览器提供了localStorage和sessionStorage两个对象用于存储数据。 localStorage和sessionStorage的区别在于存储的作用域和有效期不同。两者的作用域都限定在文档源级别,sessionStorage还被限定在窗口中。localStorage存储的数据是永久性的,除非Web应用刻意删除这些存储的数据,或者用户通过浏览器配置来删除,否则数据将一直存储在用户的电脑上。sessionStorage在当前页面实例和当前浏览器会话期间为页面提供存储,一旦窗口或者标签页被永久关闭,所有通过sessionStorage存储的数据会被删除。将数据在localStorage和sessionStorage之间传输,会将浏览器生命周期以内的数据和本地永久数据互相暴露。
**例如**:以下代码中,为了免重复输入,开发人员将用户的手机号存储在sessionStorage对象中。但是,在存储用户信息到localStorage时,又错误的将手机号码一并存了进去。这样将导致敏感信息被存储到localStorage。
sessionStorage.setItem("phone", phone_number);
var userInfo = {};
var userInfo["phone"] = sessionStorage.getItem("phone");
localStorage.setItem("phone", userInfo);
修复建议:
不要将敏感数据存储在localStorage和sessionStorage中。将数据从一种存储格式迁移至另一种存储格式时,需要考虑迁移对于数据的隐私性和依赖该数据的业务逻辑的影响。
低危:JavaEE程序:直接使用线程
JAVA EE标准禁止在某些环境下使用Web应用程序中的线程管理,因为此时使用线程管理非常容易出错。线程管理起来很困难,可能还会以不可预知的方式干扰应用程序容器。即使容器没有受到干扰,线程管理通常还会导致各种难以发现的错误,如死锁、竞争条件及其他同步错误等。
**例如**:下面代码片段中,在Servlet的`doGet()`方法中,直接创建了一个线程对象。
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Perform servlet tasks.
// Create a new thread to handle background processing.
Runnable r = new Runnable() {
public void run() {
// Process and store request statistics.
...
}
};
new Thread(r).start();
}
修复建议:
建议使用框架(如EJB、Spring等)提供的线程管理功能来替代直接使用操作线程。
低危:JavaEE程序:遗留的调试代码
应用程序中的测试代码会建立一些意想不到的入口,这些入口可能会被攻击者作为“后门”进行利用
**例如**:JAVA EE程序中的`main()`方法。
public static void main(String[] args) {
...
}
修复建议:
系统在发布之前应删除测试代码。
低危:日志记录:使用系统输出流
使用`System.out`或`System.err`进行程序日志输出,会导致程序的运行状况难以监控。
**例如**:下列代码中使用`System.out`进行程序日志输出
System.out.println(log);
修复建议:
建议使用日志记录工具代替`System.out`或`System.err`记录程序日志。
**例如**:下列代码中使用Log4j工具类进行程序日志输出
logger.info(log);
低危:使用==或!=比较字符串
程序中使用`==`或`!=`比较两个字符串是否相等,其实比较的是两个对象在内存中的地址值,而不是字符串的值。
**例如**:下面代码片段中,将request中获取的参数值与"admin"使用`==`做比较,`dosomething()`方法将永远不会被执行。
String username = (String)request.getAttribute("usernamae");
if(username=="admin"){
dosomething();
}
修复建议:
程序中应采用`equals()`方法来对字符串进行比较,而不是通过==或!=运算符的方式来操作。
低危:硬编码文件分隔符
路径分割符号问题,不同的操作系统不同。在程序中不要硬性编码与平台相关的任何常量,比如行分隔符,文件分隔符,路径分隔符等等。例如文件分隔符,Windows系统使用"\"或"/",而UNIX系统则使用"/"。应用程序需要在不同的平台上运行时,使用硬编码文件分隔符会导致应用程序逻辑执行错误,并有可能导致拒绝服务。
**例如**:以下代码使用硬编码文件分隔符来打开文件:
File file = new File(dirName + "\\" + fileName);
修复建议:
不应使用硬编码文件分隔符,而应使用语言库提供的独立于平台的API,如`java.io.File.separator`,也可以通过使用`java.lang.System.getProperty("file.separator")`来获取。
**例如**:针对示例代码的修改方法是:
File file = new File(dirName + File.separator + fileName);
低危:使用浮点数进行精确计算
Java中浮点数采用的是IEEE 754标准,所以在精确计算中使用浮点类型会发生精度缺失从而产生不正确的数值。
**例如**:下面代码输出为false和0.060000000000000005
public static void main(String[] args){
double a = 0.05 + 0.01;
System.out.println(a == 0.06);
System.out.println(a);
}
修复建议:
程序中避免使用浮点数进行精确计算,可以考虑采用整数类型或用于精确表达小数的BigDecimal类型替代。
低危:系统信息泄露:内部
通过系统输出流(非标准错误流)打印或日志功能将系统数据或调试信息输出到本地文件或屏幕时,发生内部信息泄露。
**例如**:下面代码片段中,通过日志对象输出异常的堆栈信息,攻击者可能会利用这些堆栈信息制定相应的攻击计划。
try{
...
}catch(Exception e){
logger.error("程序发生异常:", e);
}
修复建议:
程序不应通过系统输出流或程序日志将系统数据或调试信息输出显示。
低危:试图重写静态方法
静态方法无法覆盖,在子类中覆盖父类的静态方法,这样容易产生混淆,从而导致错误。
**例如**:下面代码示例中:子类不能覆盖父类的方法,`choose("user")`和`choose("admin")`都调用了父类中的方法。
class GrantAccess {
public static void displayAccountStatus() {
System.out.println("Account details for admin: XX");
}
}
class GrantUserAccess extends GrantAccess {
public static void displayAccountStatus() {
System.out.println("Account details for user: XX");
}
}
public class StatMethod {
public static void choose(String username) {
GrantAccess admin = new GrantAccess();
GrantAccess user = new GrantUserAccess();
if (username.equals("admin")) {
admin.displayAccountStatus();
} else {
user.displayAccountStatus();
}
}
public static void main(String[] args) {
choose("user"); // Account details for admin: XX
choose("admin"); // Account details for admin: XX
}
}
修复建议:
应该尽量避免静态方法的命名与超类中的命名相同,以避免产生混淆。在开发中,可以将静态方法放入单独的final类中, 因为final类不会创建子类,还可以创建私有的构造函数,以防止针对类的实例而非类来调用静态方法。
**例如**:下面代码示例中:去掉了static关键字,将`displayAccountStatus()`方法声明为实例方法,`choose("user")`和`choose("admin")`调用实现了预期的程序逻辑。
class GrantAccess {
public void displayAccountStatus() {
System.out.println("Account details for admin: XX");
}
}
class GrantUserAccess extends GrantAccess {
@Override
public void displayAccountStatus() {
System.out.println("Account details for user: XX");
}
}
public class StatMethod {
public static void choose(String username) {
GrantAccess admin = new GrantAccess();
GrantAccess user = new GrantUserAccess();
if (username.equals("admin")) {
admin.displayAccountStatus();
} else {
user.displayAccountStatus();
}
}
public static void main(String[] args) {
choose("user"); // Account details for user: XX
choose("admin"); // Account details for admin: XX
}
}
低危:系统信息泄露:标准错误流
通过系统输出流(标准错误流)打印或日志功能将系统数据或调试信息输出到本地文件或屏幕时,在一些类似Eclipse的程序,为了让错误信息更加显眼,会将错误信息以红色文本的形式通过System.err输出到控制台上,更容易发生内部信息泄露。
**例如**:下面代码片段中,`printStackTrace()`方法内部使用了标准错误流输出异常的堆栈信息,攻击者可能会利用这些堆栈信息制定相应的攻击计划。
try{
...
}catch(Exception e){
e.printStackTrace();
}
修复建议:
程序不应通过系统输出流或程序日志将系统数据或调试信息输出程序。
低危:泛化的捕获异常
使用一个catch块捕获泛化的异常类(如Exception),可能会混淆那些需要特殊处理的异常,或是捕获了不应在该程序点捕获的异常。捕获范围过大的异常与“Java的异常处理机制”是相违背的。
**例如**:下面的代码片段中,程序捕获了一个泛化的Exception异常。
try {
doExchange();
} catch(Exception e){
logger.error("doExchange failed", e);
}
修复建议:
不应捕获范围过大的异常类,比如Exception、SystemException或ApplicationException,除非是级别非常高的程序或线程。
低危:泛化的抛出异常
Java语言通过面向对象的异常处理机制来解决运行期间的错误,可以预防错误的程序代码或系统错误所造成的不可预期的结果发生。减少编程人员的工作,增加了程序的灵活性,增加程序的可读性和健壮性。如果程序只是抛出一个泛化的过于笼统的异常,不仅违反了该系统还会使调用者很难处理和修复发生的异常。
**例如**:下面代码片段中,程序抛出了一个Exception异常,而不是具体的FileNotFoundException子类型异常,这样使得调用者很难理解和处理可能发生的异常。
public void doExchange() throws Exception {
FileInputStream fis = new FileInputStream("filename.txt");
}
修复建议:
不应泛化的抛出Exception或Throwable类型的异常。
低危:表达式永远为false
表达式的计算结果永远为false,表明基于该表达式的代码快将永远不会被执行,是程序中的死代码或用于调试的代码,这些死代码会增加代码的阅读、理解和维护难度,甚至该段代码可能是调试过程中用来发现程序错误的,这样该代码块可能被攻击者利用。
**例如**:下列代码中,if的条件表达式为false,if代码块永远都不会被执行。
if (false){
//doSomething
}
修复建议:
程序异常中断了预期中的程序流程,应恰当地进行异常处理,要么从异常中恢复,将异常重新抛出,由下一个和try语句对应的catch段来处理,要么根据catch程序段的上下文抛出另一个合适的异常。
低危:表达式永远为true
表达式的计算结果永远为true,表明程序中基于该表达式的段代码永远被执行,因此没有必要进行条件判断。
**例如**:下列代码中,if的条件表达式为true,if代码块将永远被执行。
if (true){
//doSomething
}
修复建议:
检查程序逻辑,修改表达式判断条件。
低危:未使用的字段
未使用的变量在程序中没有起到任何作用,是程序中的死代码或者是被注释掉的调试代码。这些死代码会增加代码的阅读、理解和维护难度。
**例如**:如下代码中MyClass类中的字段`userName`从未使用过
public class MyClass {
private String userName="admin";
public static void main(String[] args) {
System.out.println("UnusedField");
}
}
修复建议:
检查程序逻辑,如果确定程序中的死代码没有作用,应该将其删除。
低危:未使用的方法
未使用的方法在程序中没有起到任何作用,是程序中的死代码或者是被注释掉的调试代码。这些死代码会增加代码的阅读、理解和维护难度。
**例如**:如下代码中MyClass类中的`unusedMethod()`方法从未使用过
public class MyClass {
private String unusedMethod(){
return "myclass";
}
public static void main(String[] args) {
System.out.println("UnusedMethod");
}
}
修复建议:
检查程序逻辑,如果确定程序中的死代码没有作用,应该将其删除。
低危:冗余的初始化
程序未使用变量的初始值,变量初始化后,被重新赋值或者转向作用域之外。
**例如**:下列程序声明一个变量`total`并赋值,后续并未使用所赋的值,再次赋值。
double total = getTotal();
total = getOtherTotal();
修复建议:
为了使代码易于理解和维护,删除不必要的赋值。
低危:未使用的值
变量赋值后,程序未使用该变量。
**例如**:下列程序声明一个字符串并赋值,后续一直未使用
String username = "张三";
修复建议:
为了使代码易于理解和维护,删除不必要的赋值。
低危:冗余的null检查
程序对一个引用的变量进行了null检查,但是在这之前,程序中已经对该变量进行了null检查,或是对该变量进行了一些可能会引发null异常的操作(取值、转换)。或者程序对变量进行了null检查但是并未返回或者抛出异常,后续使用该变量仍然可能引发null异常。
**例1**:如下代码先调用字符串string的`length()`方法,此时如果string如果为null就已经会抛出空指针的异常,所以之后对string是否为null的判断完全是多余的。
System.out.println(string.length());
if(string!=null){
...
}
**例2**:如下代码先对string进行null校验,但是并未返回或者抛出异常。后续继续调用字符串string的`length()`方法,此时如果string如果为null就已经会抛出空指针的异常,所以之前对string是否为null的判断完全是多余的。
if(string == null){
...
}
System.out.println(string.length());
修复建议:
检查程序逻辑,删除不必要的null检查代码。
低危:使用equals()来判断字符串是否为空
程序中使用equals方法来判断字符串是否为空,这样会降低系统性能。
修复建议:
使用判断字符串长度的方法,判断字符串是否为空,这样会提升系统性能。
低危:循环中拼接字符串
字符串对象是不可改变的,拼接和修改字符串对象,最后都会创建一个新的字符串对象。而在循环中拼接字符串将会产生很多的对象,浪费系统运行时间和空间。
**例如**:在下面代码片段中,在循环中拼接字符串。
String string = ""
for (int i < 0;i < 10;i++) {
string = string + i;
}
在上面的循环中产生了10个StringBuilder对象,每次都会调用两次`append`方法分别将string和i的值追加至StringBuilder对象中,最后调用toStirng方法将StringBuilder对象内容转化为字符串并赋值给string变量。
修复建议:
在循环语句结构中,需要保证线程安全可以使用StringBuffer代替String进行拼接,不需要时可以使用StringBuilder代替String进行拼接。
**例如**:在下面代码片段中,在循环中使用StringBuilder拼接字符串。
StringBuilder stringBuilder = new StringBuilder();
for (int i < 0;i < 10;i++) {
stringBuilder.append(i);
}
String string = stringBuilder.toString();
低危:创建Boolean对象
程序中采用`new Boolean(boolean expression)`或`new Boolean(String expression)`创建对象,该方法产生的额外对象将占用更多空间、降低性能。
修复建议:
使用`Boolean.valueOf(boolean expression)`或`Boolean.valueOf(String expression)`或`Boolean.TRUE、Boolean.FALSE`代替`new Boolean`方法。
低危:byte数组转String时未指定编码
在将字节数组的数据转换为String时如果未设定转换编码,可能会导致数据丢失。
**例如**:下面代码片段中,将数据转换为字符串,以便创建hash值。
byte[] byteArray = byte[BUFSIZE];
FileInputStream fileInputStream = new FileInputStream("fileName");
int count = fileInputStream.read(byteArray);
String fileString = new String(byteArray);
String fileSHA256Hex = DigestUtils.sha256Hex(fileString);
当文件的大小小于字节数组容量SIZE时,只要文件myFile中的信息已编码为与默认字符集相同,此方式正确。但是,如果使用不同的编码方式,或者为二进制文件,则信息将会丢失。进而导致生成的SHA散列值的可靠性降低。
修复建议:
应避免将可能包含非字符数据的byte数组转换为String对象,如果必须将byte数组转String,应对其进行编码。
**例1**:下面代码片段中,避免了将可能包含非字符数据的byte数组转换为String对象。
byte[] byteArray = byte[BUFSIZE];
FileInputStream fileInputStream = new FileInputStream("fileName");
int count = fileInputStream.read(byteArr);
byte[] fileSHA256 = DigestUtils.sha256(byteArray);
**例2**:下面代码片段中,byte数组转String时指定了编码。
byte[] byteArray = byte[BUFSIZE];
FileInputStream fileInputStream = new FileInputStream("fileName");
int count = fileInputStream.read(byteArray);
String fileString = new String(byteArray,"utf-8");
String fileSHA256Hex = DigestUtils.sha256Hex(fileString);
低危:侵犯隐私
程序对敏感信息(如用户密码或身份证号等个人信息)处理不当可能导致用户的个人信息泄露,这是一种非法行为。
**例如**:以下代码可读取存储在Android WebView上的给定站点的用户名和密码,并广播给所有注册的接收者。
webView.setWebViewClient(new WebViewClient(){
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler httphandler, String url, String field) {
...
Intent intent = new Intent();
String[] credentials= view.getHttpAuthUsernamePassword(url, field);
String name = credentials[0];
String password = credentials[1];
intent.putExtra("username", name);
intent.putExtra("password", password);
intent.setAction("SEND_CREDENTIALS");
...
view.getContext().sendBroadcast(intent);
}
});
修复建议:
当安全和隐私的需要发生矛盾时,通常应优先考虑隐私的需要。为满足这一要求,同时又保证信息安全的需要,应在退出程序前清除所有私人信息。
保护私人数据的最好做法就是最大程度地减少私人数据的暴露。不应允许应用程序、流程处理以及员工访问任何私人数据,除非是出于职责以内的工作需要。正如最小授权原则一样,不应该授予访问者超出其需求的权限,对私人数据的访问权限应严格限制在尽可能小的范围内。
对于移动应用程序,请确保它们从不与在设备上运行的其他应用程序进行任何敏感数据通信。存储私人数据时,通常都应加密。对于Android以及其他任何使用SQLite数据库的平台来说,SQLCipher是一个好选择——对SQLite数据库的扩展为数据库文件提供了透明的256位AES加密。因此,凭证可以存储在加密的数据库中。
**例1**:以下代码演示了在将所需的二进制码和存储凭证下载到数据库文件后,将SQLCipher集成到Android应用程序中的方法。
import net.sqlcipher.database.SQLiteDatabase;
...
SQLiteDatabase.loadLibs(this);
File dbFile = getDatabasePath("credentials.db");
dbFile.mkdirs();
dbFile.delete();
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbFile, "credentials", null);
db.execSQL("create table credentials(u, p)");
db.execSQL("insert into credentials(u, p) values(?, ?)", new Object[]{username, password});
请注意,对`android.database.sqlite.SQLiteDatabase`的引用可以使用`net.sqlcipher.database.SQLiteDatabase`代替。
要在WebView存储上启用加密,需要使用sqlcipher.so库重新编译WebKit。
**例2**:以下代码从Android WebView存储读取给定站点的用户名和密码,而不是将其广播到所有注册的接收器,它仅在内部广播,以便广播只能由同一应用程序的其他部分看到。
webview.setWebViewClient(new WebViewClient() {
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
String[] credentials = view.getHttpAuthUsernamePassword(host, realm);
String username = credentials[0];
String password = credentials[1];
Intent i = new Intent();
i.setAction("SEND_CREDENTIALS");
i.putExtra("username", username);
i.putExtra("password", password);
LocalBroadcastManager.getInstance(view.getContext()).sendBroadcast(i);
}
});
低危:switch语句中缺少default语句
程序中switch语句缺少default语句,而所有可能的变量值不一定都会被给定的case语句处理,导致一些情况未处理并产生问题。
修复建议:
检查程序逻辑,为switch语句添加所需的default语句
低危:可序列化类中存在可序列化的敏感信息
在可序列化类中存在敏感信息,当对象被序列化时,类中的敏感信息将会存储。攻击者可能会序列化该对象,并获取敏感信息。
**例如**:下列代码中类实现了序列化,且存在敏感信息`password`。攻击者获取该对象序列化的文件后,将会获取此用户密码。
class Person implements Serializable{
private String name;
private String password;
public Person(String name,String password) {
this.setName(name);
this.setPassword(password);
}
...
}
修复建议:
在存在敏感信息的可序列化类中,当序列化对象时,忽略,拒绝或者加密敏感信息。
**例1**:以下代码敏感信息使用transient关键字修饰,将不会参与序列化过程:
class Person implements Serializable{
private String name;
private transient String password;
public Person(String name,String password) {
this.setName(name);
this.setPassword(password);
}
...
}
**例2**:以下代码将会在序列化时拒绝序列化:
class Person implements Serializable {
private String name;
private String password;
public Person(String name,String password) {
this.setName(name);
this.setPassword(password);
}
...
private final void writeObject(ObjectOutputStream out) throws IOException {
throw new NotSerializableException();
}
private final void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
throw new NotSerializableException();
}
private final void readObjectNoData() throws ObjectStreamException {
throw new NotSerializableException();
}
}
```
低危:空的方法
程序中存在空的方法。
修复建议:
检测程序逻辑,是否存在未完成的代码。
低危:空的代码块
程序中存在空的代码块。
**例如**:下面代码片段中,程序中存在空的代码块,在程序中毫无意义。
public final class EmptyBlock{
{
//空的代码块
}
...
public void bad() {
{
//空的代码块
}
...
}
}
修复建议:
检测程序逻辑,是否存在未完成的代码。
低危:无用的分号
程序中存在无用的分号
修复建议:
检测程序逻辑,是否需要删除无用的分号。
低危:错误的参数顺序
调用方法时,使用错误的参数顺序可能导致应用程序以意想不到的方式运行。
**例如**:下面代码片段中,copy方法定义了src参数为第一个,而dest为第二个,最后调用时却将dest作为第一个参数。
public static void main(String[] args) throws Exception {
Object src = null;
Object dest = null;
...
copy(dest, src);
}
public static void copy(Object src,Object dest) {
...
}
修复建议:
检查程序逻辑,正确的进行方法调用,明确参数顺序。
低危:空的if代码块
程序中存在空的if代码块,可能是程序员的逻辑出现问题。
**例如**:
if(flag){}
修复建议:
检测程序逻辑,是否存在未完成的代码。
低危:隐藏的表单字段
隐藏字段是不安全的参数,攻击者能够在一个post请求中改变隐藏字段的值。
**例如**:
<input type="hidden">
修复建议:
将隐藏字段视为不可信赖的输入,不要在隐藏字段中存储敏感信息。
低危:侵犯隐私:自动补全
在启用自动补全功能的情况下,一些浏览器会在整个会话中保留用户输入,这将允许在初始化用户之后使用计算机来查看先前提交的信息。
**例如**:
<form name="form02" action="" method="post" autocomplete="on">
密码: <input type="password" >
<input type="submit" name="" value="提交" >
</form>
修复建议:
在使用HTML5时建议不要打开autocomplete属性。
**例1**:在表单中,将autocomplete的属性值显示设置为off,会禁用所有的input的自动补全功能。
<form name="form02" action="" method="post" autocomplete="off">
密码: <input type="password" >
<input type="submit" name="" value="提交" >
</form>
**例2**:或者在相应的input标签上将autocomplete的属性值显示设置为off,禁用指定的input的自动补全功能。
<form name="form02" action="" method="post">
密码: <input type="password" autocomplete="off">
<input type="submit" name="" value="提交" >
</form>
低危:遗留的调试代码
应用程序中的测试代码会建立一些意想不到的入口,这些入口可能会被攻击者作为“后门”进行利。有的测试代码本身并不会带来危害,但它表明了程序未进行严格的清理,攻击者很可能会查找到另外一些可利用的测试代码。
**例如**:
console.log(message);
修复建议:
应用程序在发布之前应删除测试代码。
异常处理
低危:捕获NullPointerException
应用程序中不应该捕获`NullPointerException`或者任何它的基类。因为捕获`NullPointerException`只可能掩盖潜在的空引用,降低应用性能,并造成难以理解和维护的代码。
**例如**:如下代码对空指针异常进行了捕获
String name=null;
try {
name=request.getParameter("name");
if(name.equals("admin")){
...
}
} catch(NullPointerException e) { //程序捕获NullPointerException
...
}
修复建议:
检查程序逻辑,删除捕获`NullPointerException`的代码。
低危:finally代码块中抛出异常
在finally代码块中抛出异常时会消除从try或catch程序段抛出的任何异常,并影响后续代码的执行
**例如**:下面代码中在finally代码块中抛出了异常
public static void doOperation throws IOException{
FileInputStream fis = null;
try{
fis = new FileInputStream("Reader.txt");
...
} finally {
fis.close();
...
}
}
当Reader.txt不存在,且程序并未对异常进行捕获时,程序只会抛出finally代码块中产生的`NullPointerException`,try代码块中产生的`FileNotFoundException`将不会显示,后续代码将不会执行。
修复建议:
不应在finally代码块中抛出异常,应直接处理掉可能发生的异常
**例如**:下面代码中在finally代码块中处理了异常,其后续代码的执行不会被影响
public static void doOperation throws IOException{
FileInputStream fis = null;
try{
fis = new FileInputStream("Reader.txt");
...
} finally {
if(fis !=null) {
try {
fis.close();
} catch (IOException e) {
...
}
...
}
...
}
低危:空的catch代码块(1)
忽略或消除程序异常会导致不一致的程序状态,**例如**:在try程序段中,不会执行在异常抛出之后的表达式或语句。
**例如**:下面这段不符合规则的代码示例捕获并消除了`InterruptedException`。
class Foo implements Runnable{
public void run(){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
//Ignore
}
}
}
上面程序中,妨碍了`run()`方法的调用者判断是否发生一个中断异常。
修复建议:
程序异常中断了预期中的程序流程,应恰当地进行异常处理,要么从异常中恢复,将异常重新抛出,由下一个和try语句对应的catch段来处理,要么根据catch程序段的上下文抛出另一个合适的异常。
低危:空的catch代码块(2)
忽略或消除程序异常会导致不一致的程序状态,例如在try程序段中,不会执行在异常抛出之后的表达式或语句。
**例如**:
try{
var a = 1;
} catch(e){
}
修复建议:
程序异常中断了预期中的程序流程,应恰当地进行异常处理,要么从异常中恢复,将异常重新抛出,由下一个和try语句对应的catch段来处理,要么根据catch程序段的上下文抛出另一个合适的异常。
跨站脚本
高危:反射型XSS[Java]
反射型XSS是指应用程序通过Web请求获取不可信赖的数据,并在未检验数据是否存在恶意代码的情况下,将其发送给用户。反射型XSS一般可以由攻击者构造带有恶意代码参数的URL来实现,在构造的URL地址被打开后,其中包含的恶意代码参数被浏览器解析和执行。这种攻击的特点是非持久化,必须用户点击包含恶意代码参数的链接时才会触发。
**例如**:下面JSP代码片段的功能是从HTTP请求中读取输入的用户名(username)并显示到页面。
```java
<%
String name= request.getParameter("username"); %>
...
姓名: <%= name%>
...
```
如果name里有包含恶意代码,那么Web浏览器就会像显示HTTP响应那样执行该代码,应用程序将受到反射型XSS攻击。
修复建议
为了避免反射型XSS攻击,建议采用以下方式进行防御:
1.对用户的输入进行合理验证(如年龄只能是数字),对特殊字符(如`<、>、'、"`以及`<script>、javascript`等进行过滤。
2.根据数据将要置于HTML上下文中的不同位置(HTML标签、HTML属性、JavaScript脚本、CSS、URL),对所有不可信数据进行恰当的输出编码。
**例如**:采用OWASP ESAPI对数据输出HTML上下文中不同位置,编码方法如下。
```java
//HTML encode
ESAPI.encoder().encodeForHTML(inputData);
//HTML attribute encode
ESAPI.encoder().encodeForHTMLAttribute(inputData);
//JavaScript encode
ESAPI.encoder().encodeForJavaScript(inputData);
//CSS encode
ESAPI.encoder().encodeForCSS(inputData);
//URL encode
ESAPI.encoder().encodeForURL(inputData);
```
3.设置HttpOnly属性,避免攻击者利用跨站脚本漏洞进行Cookie劫持攻击。在Java EE中,给Cookie添加HttpOnly的代码如下:
```java
...
response.setHeader("Set-Cookie","cookiename=cookievalue; path=/; Domain=domainvaule; Max-age=seconds; HttpOnly");
...
```
高危:存储型XSS[Java]
存储型XSS是指应用程序通过Web请求获取不可信赖的数据,并且在未检验数据是否存在XSS代码的情况下,将其存入数据库。当程序下一次从数据库中获取该数据时,致使页面再次执行XSS代码。存储型XSS可以持续攻击用户,在用户提交了包含XSS代码的数据存储到数据库后,每当用户在浏览网页查询对应数据库中的数据时,那些包含XSS代码的数据就会在服务器解析并加载,当浏览器读到XSS代码后,会当做正常的HTML和JS解析并执行,于是发生存储型XSS攻击。
**例如**:下面JSP代码片段的功能是根据一个已知用户雇员ID(id)从数据库中查询出该用户的地址,并显示在JSP页面上。
```java
<%
...
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from users where id =" + id);
String address = null;
if (rs != null) {
rs.next();
address = rs.getString("address");
}
%>
家庭地址: <%= address %>
```
如果address的值是由用户提供的,且存入数据库时没有进行合理的校验,那么攻击者就可以利用上面的代码进行存储型XSS攻击。
修复建议
为了避免存储型XSS攻击,建议采用以下方式进行防御:
1.对从数据库或其它后端数据存储获取不可信赖的数据进行合理验证(如年龄只能是数字),对特殊字符(如`<、>、'、"`以及`<script>、javascript`等进行过滤。
2.根据数据将要置于HTML上下文中的不同位置(HTML标签、HTML属性、JavaScript脚本、CSS、URL),对所有不可信数据进行恰当的输出编码。
**例如**:采用OWASP ESAPI对数据输出HTML上下文中不同位置,编码方法如下。
```java
//HTML encode
ESAPI.encoder().encodeForHTML(inputData);
//HTML attribute encode
ESAPI.encoder().encodeForHTMLAttribute(inputData);
//JavaScript encode
ESAPI.encoder().encodeForJavaScript(inputData);
//CSS encode
ESAPI.encoder().encodeForCSS(inputData);
//URL encode
ESAPI.encoder().encodeForURL(inputData);
```
3.设置HttpOnly属性,避免攻击者利用跨站脚本漏洞进行Cookie劫持攻击。在Java EE中,给Cookie添加HttpOnly的代码如下:
```java
...
response.setHeader("Set-Cookie","cookiename=cookievalue; path=/; Domain=domainvaule; Max-age=seconds; HttpOnly");
...
```
低危:跨站请求伪造
跨站请求伪造(CSRF)是伪造客户端请求的一种攻击。应用程序允许用户提交不包含任何nonce(与用户Session关联的加密随机值)的请求,将可能导致CSRF攻击。
**例如**:以下代码片段用于银行转账功能,对于该重要敏感的操作没有进行相应防护,将易于导致跨站请求伪造攻击。
var req = new XMLHttpRequest();
req.open("POST", "/transferFunds", true);
body = "to_account=Bill&amount=10000";
req.send(body);
**例2**:下面是通过表单发送请求。
<form method="GET" action="/transferFunds ">
cash: <input type="text" name="cash">
to: <input type=" text " name=“to">
<input type="submit" name="action" value="TransferFunds">
</form>
修复建议:
防止跨站请求伪造攻击的方法如下:
防御策略
1、验证HTTP Refer字段
2、在请求地址中添加token验证
3、在http头中自定义属性并验证
4、Chrome浏览器端启用 SameSite cookie
防御手段
1、尽量使用POST, 限制GET
2、调整浏览器Cookie策略
3、加验证码,强制用户必须与应用进行交互,才能完成最终请求。在通常情况下,验证码能很好遏制CSRF攻击。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案
4、Referer Check在Web最常见的应用就是“防止图片盗链”。
同理,Referer Check也可以被用于检查请求是否来自合法的“源”(Referer值是否是指定页面,或者网站的域),如果都不是,那么就可能是CSRF攻击
5、Anti CSRF token 业界推荐
Anti CSRF token
1、用户访问某个表单页面。
2、服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。
3、在页面表单附带上Token参数。
4、用户提交请求后, 服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致,一致为合法请求,不是则非法请求。
这个Token的值必须是随机的,不可预测的。由于Token的存在,攻击者无法再构造一个带有合法Token的请求实施CSRF攻击。
另外使用Token时应注意Token的保密性,尽量把敏感操作由GET改为POST,以form或AJAX形式提交,避免Token泄露。
**例如**:下面代码片段中,在表单中增加了一个Token。
req.open("POST", "/transferFunds", true);
body = "to_account=Bill&amount=10000&token=" + token;
//token为与会话相关的一次性随机数。
req.send(body);
这样,服务器端程序响应用户请求前先验证Token,判断请求的合法性。对于Token,越难被猜出攻击者攻击成功的概率就越低。
配置管理
中危:JavaEE配置错误:特定HTTP方法[Java]
Web应用程序指定 HTTP方法的安全限制可能得不到预期的效果,攻击者可以通过篡改HTTP命令来绕过应用程序的安全限制。
**例如**:下面配置文件片段中, 以下安全限制可应用于 HTTP GET方法,但不能应用于其他 HTTP 命令:
```xml
...
<security-constraint>
<web-resource-collection>
<web-resource-name>Protected Area</web-resource-name>
<url-pattern>/myWeb/create</url-pattern>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>Create</role-name>
</auth-constraint>
</security-constraint>
...
```
修复建议
Web应用程序应该在web.xml中不要指定 HTTP方法的安全限制,防止攻击者篡改HTTP命令来绕过应用程序的安全限制。
**例如**:下面配置文件片段中,以下安全限制将应用于所有HTTP方法。
```xml
...
<security-constraint>
<web-resource-collection>
<web-resource-name>Protected Area</web-resource-name>
<url-pattern>/myWeb/create</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>Create</role-name>
</auth-constraint>
</security-constraint>
...
```
参考链接:https://blog.csdn.net/qq_39560975/article/details/131956264
高等级缺陷
代码质量:邮件服务器建立未加密的连接
代码注入:XML外部实体注入
代码注入:动态解析代码
跨站脚本:存储型XSS
跨站脚本:反射型XSS
输入验证:基于DOM的XSS
输入验证:路径遍历
输入验证:重定向
输入验证:重定向
中等级缺陷
API误用:不安全的框架绑定
API误用:依赖未经验证和完整性检查的cookie
代码质量:Cookie:未经过SSL加密
代码质量:null引用
代码质量:webpack源码泄露
代码质量:比较Locale相关的数据未指定适当的Locale
代码质量:不安全的信息传输
代码质量:非静态内部类实现序列化接口
代码质量:请求参数自动填充数据库实体
代码质量:未设置httpOnly
代码质量:系统信息泄露:Session传递
代码质量:系统信息泄露:外部
代码质量:硬编码IP
代码质量:硬编码手机号码
代码质量:硬编码邮箱地址
代码注入:HTTP响应截断
代码注入:HTTP响应截断:Cookies
代码注入:JavaScript劫持
代码注入:XML实体扩展注入
代码注入:同源方法执行
代码注入:资源注入
密码管理:不安全的随机数
密码管理:配置文件中的明文密码
密码管理:硬编码密码
配置管理:JavaEE配置错误:特定HTTP方法
输入验证:HTTP参数污染
输入验证:服务器端请求伪造
输入验证:拒绝服务:正则表达式
输入验证:路径遍历:ZIP条目覆盖
输入验证:日志伪造
输入验证:文件上传
资源管理:单例成员变量
资源管理:反射型文件下载
资源管理:格式化缺陷
资源管理:路径访问控制
资源管理:使用不安全的targetblank
资源管理:数据库访问控制
资源管理:资源未释放:Sockets
资源管理:资源未释放:流
低等级缺陷
API误用:忽略返回值
API误用:缺少对方法返回值的null检查
API误用:使用不必要的线程安全类
API误用:误用String.toString()
代码质量:byte数组转String时未指定编码
代码质量:Cookie:持久
代码质量:Cookie:未经过SSL加密
代码质量:HTML中包含硬编码域名
代码质量:img中缺少alt属性
代码质量:JavaEE程序:遗留的调试代码
代码质量:switch语句中缺少default语句
代码质量:switch语句中缺少返回语句
代码质量:变量和方法同名
代码质量:表达式永远为false
代码质量:表达式永远为true
代码质量:不应重复编译相同的正则表达式
代码质量:创建String对象
代码质量:方法返回参数类型为泛型通配符类型
代码质量:浮点数的字符串比较
代码质量:构造方法中调用可覆写的方法
代码质量:过于宽松的CORS策略
代码质量:可序列化类中存在可序列化的敏感信息
代码质量:空的if代码块
代码质量:空的if代码块
代码质量:空的方法
代码质量:列表标签误用
代码质量:没有使用正则表达式时应该使用replace方法而不是replaceAll方法
代码质量:侵犯隐私
代码质量:侵犯隐私:String对象
代码质量:侵犯隐私:自动补全
代码质量:日志记录:非staticfinal的日志对象
代码质量:日志记录:使用系统输出流
代码质量:冗余的null检查
代码质量:冗余的初始化
代码质量:使用==或!=比较字符串
代码质量:使用equals()来判断字符串是否为空
代码质量:使用单个字符字符串获取索引位置
代码质量:使用已过时的属性或标签
代码质量:未使用的值
代码质量:未使用的字段
代码质量:无用的分号
代码质量:系统信息泄露:Debug日志程序
代码质量:系统信息泄露:标准错误流
代码质量:系统信息泄露:过于宽泛的Debug日志记录
代码质量:系统信息泄露:内部
代码质量:循环中拼接字符串
代码质量:遗留的调试代码
代码质量:隐藏的表单字段
代码质量:硬编码URL地址
代码质量:硬编码文件分隔符
代码质量:注释中的银行卡号
代码质量:注释中的邮箱
代码注入:JavaScript劫持:易受攻击的框架
代码注入:JSON注入
代码注入:有风险的SQL查询:Hibernate
跨站脚本:跨站请求伪造
密码管理:不安全的哈希算法
密码管理:不安全的哈希算法
密码管理:不安全的随机数
密码管理:明文密码
密码管理:硬编码加密密钥
密码管理:注释中的密码
配置管理:构建配置错误:Maven仓库外部依赖
输入验证:操纵设置
输入验证:拒绝服务:StringBuilder
输入验证:拒绝服务:解析Double类型数据
输入验证:数据跨越信任边界
输入验证:文件上传
输入验证:有风险的资源使用
异常处理:泛化的捕获异常
异常处理:泛化的抛出异常
异常处理:空的catch代码块
异常处理:空的catch代码块
资源管理:有风险的数据库访问控制
基于参考文档进行的补充
参考文档:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)