JAVA Web应用常见漏洞与修复建议

最近负责的项目参与了甲方要求的代码审计,扫出来不少问题,46w+行代码扫出来81种漏洞,涉及1w+行代码,不良代码率高达2.93%,也确实反应了不少问题,这里贴出来供大家参考

目录


目录

跨站脚本

高危:存储型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

应用程序的客户端代码从

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或脚本中执行。如果不能避免,则应进行严格的过滤。

高危:重定向

应用程序允许未验证的用户输入控制重定向中的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");

中危:拒绝服务:正则表达式

正则表达式引擎分成两类:一类称为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函数,在处理图片的同时,破坏图片中可能包含的脚本代码。

代码注入

中危:HTTP响应截断

程序从一个不可信赖的数据源获取数据,未进行验证就置于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);
}

中危:有风险的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误用

中危:不安全的框架绑定

目前大部分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的方式提交表单。

资源管理

中危:数据库访问控制

程序未进行恰当的访问控制,执行了一个包含用户控制主键的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();

中危:资源未释放:流 

程序创建或分配流资源后,不进行合理释放,将会降低系统性能。攻击者可能会通过耗尽资源池的方式发起拒绝服务攻击。

**例如**:在下面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的缓存机制,所以如果基本类型的包装类是一个整数且在-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);
}

中危:系统信息泄露: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程序段的上下文抛出另一个合适的异常。 

跨站脚本

低危:跨站请求伪造

跨站请求伪造(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,越难被猜出攻击者攻击成功的概率就越低。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年12月22日
下一篇 2023年12月22日

相关推荐