5个以上参数的方法
摘要
函数声明中参数超过5个,将导致程序逻辑过于复杂。
缺陷描述
方法函数应当尽量简洁,一个函数只处理一个功能,本规则检测函数的参数超过5个的情形。
示例1 函数中包含参数过多。
public class test {
public String getUser(int a, int b, int c, int d, int e, int f){
...
}
}
修复建议:
尽量不要设计参数超过5个的方法,一个函数只处理一个功能,防止方法逻辑过于复杂。
隐私违规
摘要
对机密信息(如客户密码或社会保障号码)处理不当会危及用户的个人隐私,这是一种非法行为。
缺陷描述
Privacy Violation 会在以下情况下发生:
1. 用户私人信息进入了程序。
2. 数据被写到了一个外部介质,例如控制台、file system 或网络。
示例1 以下代码包含了一个日志记录语句,该语句通过在日志文件中存储记录信息跟踪添加到数据库中的各条记录信息。在存储的其他数值中,getPassword() 函数可以返回一个由用户提供的、与用户帐号相关的明文密码。
pass = getPassword();
...
dbmsLog.println(id+":"+pass+":"+type+":"+tstamp);
在以上示例中,代码采用日志的形式将明文密码记录到了文件系统中。虽然许多开发人员认为文件系统是存储数据的安全位置,但这不是绝对的,特别是在涉及到隐私问题时。
在移动世界中隐私是最令人担心的问题之一,其原因有以下两点。一是设备丢失的几率较高。第二点与移动应用程序之间的进程间通信有关。移动平台的本质是从各种来源下载并在相同设备上运行的应用程序。因为恶意软件在银行应用程序附近运行的可能性很高,所以应用程序的作者需要注意消息所包含的信息,这些消息将会发送给在设备上运行的其他应用程序。移动应用程序之间的进程间通信不应包含敏感信息。
示例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);
view.getContext().sendBroadcast(i);
}
});
...
此示例存在多个问题。首先,WebView 凭证以明文的形式存储且不经过 hash 处理。因此,如果用户拥有 root 设备(或使用仿真器),他/她就能读取存储的给定站点的密码。其次,明文凭证将被广播给所有注册的接收者,这就意味着任何使用 SEND_CREDENTIALS 收听的注册接收者都将收到消息。即使权限限制接收者人数,广播也不会受到保护;既然这样,我们也不建议将权限作为修复方式使用。
可以通过多种方式将私人数据输入到程序中:
— 以密码或个人信息的形式直接从用户处获取
— 由应用程序访问数据库或者其他数据存储形式
— 间接地从合作者或者第三方处获取
通常情况下,在移动世界的背景下,该私人信息将包括(以及密码、SSN 和其他一般个人信息):
- 位置
- 手机号码
- 序列号和设备 ID
- 网络运营商信息
- 语音信箱信息
有时,某些数据并没有贴上私人数据标签,但在特定的上下文中也有可能成为私人信息。比如,通常认为学生的学号不是私人信息,因为学号中并没有明确而公开的信息用以定位特定学生的个人信息。但是,如果学校用学生的社会保障号码生成学号,那么这时学号应被视为私人信息。
安全和隐私似乎一直是一对矛盾。从安全的角度看,您应该记录所有重要的操作,以便日后可以鉴定那些非法的操作。然而,当其中牵涉到私人数据时,这种做法就存在一定风险了。
虽然不安全地处理私人数据有多种形式,但是常见的风险来自于盲目的信任。程序员通常会信任程序运行的操作系统,因此认为将私人信息存放在 file system、注册表或者获得局部控制的资源中是值得信任的。尽管已经限制了某些资源的访问权限,但仍无法保证所有访问这些资源的个体都是值得信任的。例如,2004 年,一个不道德的 AOL 员工把大约 9200 万个客户的私人电子邮件地址卖给了一个通过垃圾邮件进行营销的赌博网站 [1]。
鉴于此类备受瞩目的信息盗取事件,私人信息的收集与管理正日益规范化。要求各个组织应根据其经营地点、所从事的业务类型及其处理的私人数据性质,遵守下列一个或若干个联邦和州的规定:
- Safe Harbor Privacy Framework [3]
- Gramm-Leach Bliley Act (GLBA) [4]
- Health Insurance Portability and Accountability Act (HIPAA) [5]
- California SB-1386 [6]
尽管制定了这些规范,Privacy Violation 漏洞仍时有发生。
修复建议:
当安全和隐私的需要发生矛盾时,通常应优先考虑隐私的需要。为满足这一要求,同时又保证信息安全的需要,应在退出程序前清除所有私人信息。
为加强隐私信息的管理,应不断改进保护内部隐私的原则,并严格地加以执行。这一原则应具体说明应用程序应该如何处理各种私人数据。在贵组织受到联邦或者州法律的制约时,应确保您的隐私保护原则尽量与这些法律法规保持一致。即使没有针对贵组织的相应法规,您也应当保护好客户的私人信息,以免失去客户的信任。
保护私人数据的最好做法就是最大程度地减少私人数据的暴露。不应允许应用程序、流程处理以及员工访问任何私人数据,除非是出于职责以内的工作需要。正如最小授权原则一样,不应该授予访问者超出其需求的权限,对私人数据的访问权限应严格限制在尽可能小的范围内。
对于移动应用程序,请确保它们从不与在设备上运行的其他应用程序进行任何敏感数据通信。存储私人数据时,通常都应加密。对于 Android 以及其他任何使用 SQLite 数据库的平台来说,SQLCipher 是一个好选择 -- 对 SQLite 数据库的扩展为数据库文件提供了透明的 256 位 AES 加密。因此,凭证可以存储在加密的数据库中。
示例3 以下代码演示了在将所需的二进制码和存储凭证下载到数据库文件后,将 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。
示例4 以下代码从 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);
}
});
...
注意事项:
- 要彻底审计所有的 Privacy Violation 漏洞,措施之一就是确保自定义的规则可以识别所有进入程序的私人或敏感信息。无法自动识别多数私人数据信息。若不使用自定义规则,您执行的 Privacy Violation 漏洞检查可能是不完整的。
框架及场景参考
(一)Android: Side Channel Data Leakage
在android中使用Intent传输密码等隐私数据时如果使用隐式跳转的方式,可以被攻击者进行模拟action的方式进行数据截取,如果数据没有进行加密的话可以直接被攻击者截取到明文数据。
示例5
Intent intent = new Intent();
intent.putExtra("username",useredit.getText().toString());
intent.putExtra("password",pwdedit.getText().toString());
intent.setAction("edu.com.SendIntent.LoginActivity");
intent.addCategory(Intent.CATEGORY_DEFAULT);
startActivity(intent);
规范要求:
在android中使用Intent传输密码等隐私数据时应:
1)在Intent跳转方式上使用显式跳转;
2)不要将用户隐私数据明文输出到日志等位置。
3)对传输的数据参数进行加密处理。
示例6 Activity组件通信的传递的密码等隐私数据应进行加密处理。
Intent intent = new Intent(this,MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Bundle bun = new Bundle();
//加密账号字符串
String encryptionUserName = encryption(useredit.getText().toString());
//加密密码字符串
String encryptionPwd = encryption(pswedit.getText().toString());
bun.putString("Username",encryptionUserName );
bun.putString("Password", encryptionPwd);
intent.setClass(this, IntentReciever.class);
intent.putExtras(bun);
MainActivity.this.startActivity(intent);
Cookie中的明文存储
摘要
在cookie中存储的未加密的敏感数据比加密的更容易访问,这大大的降低了攻击者的解密难度。
缺陷描述
HTTPS会话中的敏感cookie的安全属性没有被设置,这可能会导致用户代理通过HTTP会话以明文形式发送这些cookie。
示例1 下面的代码片断,来自于servlet doPost()方法,在没有调用setSecure(真)的情况下设置一个accountID cookie(敏感)。
Cookie c = new Cookie(ACCOUNT_ID, acctID);
response.addCookie(c);
修复建议:
如果Cookie只通过HTTPS传送,那么每次都要设置安全属性。
ESAPI禁用的API使用
摘要
ESAPI 提供了此方法的更安全的版本。
缺陷描述
ESAPI 安全编码指南包含一个禁止的 API 列表。对于这些 API,ESAPI 中具有更为安全的备选组件。
禁止的 API 与替代性 API 的列表:
Banned 001 System.out.println()
Banned 002 Throwable.printStackTrace()
Banned 003 Runtime.exec()
Banned 004 Session.getId()
Banned 005 ServletRequest.getUserPrincipal()
Banned 006 ServletRequest.isUserInRole()
Banned 007 Session.invalidate()
Banned 008 Math.Random.*
Banned 009 File.createTempFile()
Banned 010 ServletResponse.setContentType()
Banned 011 ServletResponse.sendRedirect()
Banned 012 RequestDispatcher.forward()
Banned 013 ServletResponse.addHeader()
Banned 014 ServletResponse.addCookie()
Banned 015 ServletRequest.isSecure()
Banned 016 Properties.*
Banned 017 ServletContext.log()
Banned 018 java.security and javax.crypto
Banned 019 java.net.URLEncoder/Decoder
Banned 021 ServletResponse.encodeURL
Banned 022 ServletResponse.encodeRedirectURL
Banned 023 javax.servlet.ServletInputStream.readLine
修复建议:
确定禁用API函数的列表,并禁止开发人员使用这些功能,提供更安全的替代品。在某些情况下,自动代码分析工具或编译器可以指示当场使用禁止的功能,如从Microsoft's SDL引入“banned.h”。[ r.676.1 ] [ r.676.2 ]。
HTTPS Session创建Cookie时未将secure标记为true
摘要
创建了cookie,但未将 secure 标记设置为true。
缺陷描述
现今的 Web 浏览器支持每个 cookie 的 secure 标记。如果设置了该标记,那么浏览器只会通过 HTTPS 发送 cookie。通过未加密的通道发送 cookie 将使其受到网络截取攻击,因此安全标记有助于保护 cookie 值的保密性。如果 cookie 包含私人数据或带有会话标识符,那么该标记尤其重要。
示例1 在下面的示例中,在未设置secure标记的情况下将cookie添加到响应中。
Cookie cookie = new Cookie("emailCookie", email);
response.addCookie(cookie);
如果应用程序同时使用 HTTPS 和 HTTP,但没有设置 secure 标记,那么在 HTTPS 请求过程中发送的 cookie 也会在随后的 HTTP 请求过程中被发送。通过未加密的无线连接截取网络信息流对攻击者而言十分简单,因此通过 HTTP 发送 cookie(特别是具有会话 ID 的 cookie)可能会危及应用程序安全。
修复建议:
对所有新 cookie 设置 Secure 标记,指示浏览器不要以明文形式发送这些 cookie。可通过调用 setSecure(true) 完成此配置。
Cookie cookie = new Cookie("emailCookie", email);
cookie.setSecure(true);
response.addCookie(cookie);
If…Else语句缺少Else语句
摘要
程序源码在处理if…else if代码块中缺少else块。
缺陷描述
类似switch语句缺少default块,if…else if语句没有以else结束,可能没有处理所有可能情况。
示例1 缺少else语句。
if(condition1){
...
}else if(condition2){
...
}
修复建议:
记得在if…else if代码块中增加else语句。
示例2 “示例1”代码修改如下
if(condition1){
...
}else if(condition2){
...
}else{ //处理其他所有额外情况
...
}
Long型数据未以L结尾
摘要
检测代码中使用长整型数据常量没有正确以L结尾。
缺陷描述
良好的代码规范要求我们准确编写代码,同时需要考虑代码的可维护性。其中,在定义长整型数据常量时,不要用小写字母”l”和阿拉伯数字”1”误写为结尾修饰符。
示例1 下面代码片段分别以小写字母”l”和阿拉伯数字”1”结尾。
...
Long maxCount = 6001;
...
Long maxSize = 6000l;
...
修复建议:
定义长整型数据常量应当正确以L结尾。
示例2
...
long maxCount = 6001L;
...
SQL语句查询所有结果
摘要
SELECT查询语句使用 * 查找所有表中所有字段。
缺陷描述
通常,我们没必要将表中所有字段查询出来,增加没必要的开销。
示例1 :
String sql = “SELECT * FROM table a WHERE a.field1 = ?”;
PreparedStatement ps = conn.preparedStatement(sql);
修复建议:
根据实际需要,查找具体字段,避免使用 * 进行遍历。
Switch语句缺少Default条件
摘要
该代码在switch语句中不含有默认的分支语句,这可能导致复杂的逻辑错误,并由此产生漏洞。
缺陷描述
示例1 下面示例中在security_check函数返回a-1值出现错误时,没有能够正确检查返回代码。如果攻击者可以提供能够调用错误的数据,那么他就可以绕过安全检查。
...
#define FAILED 0
#define PASSED 1
int result;
...
result = security_check(data);
switch (result) {
case FAILED:
printf("Security check failed!\n");
exit(-1);
case PASSED:
printf("Security check passed.\n");
break;
}
// program execution continues...
相反,在未标明情况下,就使用默认的标签:
#define FAILED 0
#define PASSED 1
int result;
result = security_check(data);
switch (result) {
case FAILED:
printf("Security check failed!\n");
exit(-1);
case PASSED:
printf("Security check passed.\n");
break;
default:
printf("Unknown error (%d), exiting...\n",result);
exit(-1);
}
因为无法假设对所有的可能情况都进行标明,所以使用此标签。一个好的做法是保留默认情况用于进行错误处理。
修复建议:
在根据某一特定变量值对流或值进行调整时,应确保所有情况都已标明。在switch语句中,可以通过使用默认标签来实现标明。
注意事项:
该漏洞是软件开发中的一个常见问题,出现在并非一个变量的所有可能的值都被特定的过程考虑到或处理情况下。正因为如此,如果是根据不良信息所进行的进一步决策,就会导致层层失败,继而可能产生任何安全问题,并构成系统中的重大失败。在switch式语句中,如果操作正确的话,创建默认情况这一非常简单的做法就可以缓解这个局面。但是通常,默认情况只用于代表一个假设的选择,而不是进行完整性检查。我们不建议这种做法,在某些情况下,这甚至跟完全忽略默认情况一样糟糕。
Switch语句中缺少Break语句
摘要
程序省略了switch或类似结构中的break语句,造成与多个条件相关的代码被运行。如果程序员只想运行与一个条件相关的代码时,这样操作就可能产生问题。
缺陷描述
这可能导致关键代码在本不该运行的情况中被运行。
示例1
...
int month = 8;
switch (month) {
case 1: print("January");
case 2: print("February");
case 3: print("March");
case 4: print("April");
case 5: print("May");
case 6: print("June");
case 7: print("July");
case 8: print("August");
case 9: print("September");
case 10: print("October");
case 11: print("November");
case 12: print("December");
}
println(" is a great month");
...
现在有人也许会认为,如果测试过12个情况的case语句后,每个结果都会显示:月份+is a great month。但,如果测试的是11,那结果会显示"November December is a great month"。
修复建议:
为了使一个语句通过而省略break语句经常容易被认为是一个错误,因此不要这样使用。如果您需要使用fall-through能力,请确保您已清楚地在switch语句中记录这一点了,并确保您已检查了所有的逻辑可能性。
使用if语句可能会澄清省略break语句的功能。而且,这种方法更安全。
安全决策依赖于Cookie
摘要
应用程序使用依赖于cookie的存在或值的保护机制,但它不能确保cookie对相关用户有效。
缺陷描述
攻击者在浏览器内或通过实现浏览器之外的客户端代码,可以很容易地修改cookie。攻击者可以绕过保护机制,如授权和身份验证,修改cookie,以包含预期值。
使用Cookie设置用户权限很危险。cookie可以被操作来要求较高的授权级别,或者声称成功的身份验证已经发生。
示例1 下面的代码摘录从浏览器读取的cookie值,以确定用户的角色。
Cookie[] cookies = request.getCookies();
for (int i =0; i< cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("role")) {
userRole = c.getValue();
}
}
示例2 下列代码可用于医疗记录应用程序,它通过检查cookie是否已被执行身份验证。
$auth = $_COOKIES['authenticated'];
if (! $auth) {
if (AuthenticateUser($_POST['user'], $_POST['password']) == "success") {
// save the cookie to send out in future responses
setcookie("authenticated", "1", time()+60*60*2);
}
else {
ShowLoginScreen();
die("\n");
}
}
DisplayMedicalHistory($_POST['patient_ID']);
程序员期望AuthenticateUser()方法的check将被应用,“authenticated”cookie只有当认证成功才会进行设置。程序员甚至特别指定该Cookie的有效期为2小时。
可是,攻击者可以将“authenticated”cookie设置为非零值,比如1。因此,$auth变量为1,从而AuthenticateUser()甚至不进行检查,即攻击者绕过了认证。
示例3 在下面的示例中,authentication标记从浏览器cookie读取,从而允许对用户状态数据进行外部控制。
Cookie[] cookies = request.getCookies();
for (int i =0; i< cookies.length; i++) {
Cookie c = cookies[i];
if (c.getName().equals("authenticated") && Boolean.TRUE.equals(c.getValue())) {
authenticated = true;
}
}
修复建议:
若您打算使用Cookie作为安全决策,请在Cookie数据中执行彻底的输入验证(即:服务器端验证)。
安全决策依赖于不受信任的输入
摘要
攻击者可以欺骗 DNS 条目。勿将 DNS 名称作为安全性的依据。
缺陷描述
许多 DNS 服务器都很容易被攻击者欺骗,所以应考虑到某天软件有可能会在有问题的 DNS 服务器环境下运行。如果允许攻击者进行 DNS 更新(有时称为 DNS 缓存中毒),则他们会通过自己的机器路由您的网络流量,或者让他们的 IP 地址看上去就在您的域中。勿将系统安全寄托在 DNS 名称上。
示例1 以下代码使用 DNS 查找,以确定输入请求是否来自可信赖的主机。如果攻击者可以攻击 DNS 缓存,那么他们就会获得信任。
String ip = request.getRemoteAddr();
InetAddress addr = InetAddress.getByName(ip);
if (addr.getCanonicalHostName().endsWith("trustme.com")) {
trusted = true;
}
IP 地址相比 DNS 名称而言更为可靠,但也还是可以被欺骗的。攻击者可以轻易修改要发送的数据包的源 IP 地址,但是响应数据包会返回到修改后的 IP 地址。为了看到响应的数据包,攻击者需要在受害者机器与修改的 IP 地址之间截取网络数据流。为实现这个目的,攻击者通常会尝试把自己的机器和受害者的机器部署在同一子网内。攻击者可能会巧妙地采取源地址路由的方法来回避这一要求,但是在今天的互联网上通常会禁止源地址路由。总而言之,核实 IP 地址是一种有用的 authentication 方式,但不应仅使用这一种方法进行 authentication。
修复建议:
理解可以进入您的软件的所有潜在的区域:参数,cookies,任何从网络、环境变量、反向DNS查询,查询结果,请求头、URL组件,电子邮件,文件,文件名,数据库以及任何外部系统读取的数据。请记住,这些输入可以通过API调用间接获得。
识别所有用于安全决策的输入,并确定是否可以修改设计,以便您不必依赖提交的输入。例如,您可以在服务器端保存有关用户会话的关键信息,而不是将其记录在外部数据中。
暴露系统数据
摘要
暴露系统数据可能通过如下几种方式
缺陷描述
暴露系统数据可能通过如下几种方式
1. J2EE Misconfiguration: Direct JSP Access
2. Java Server Pages 可能导致 System Information Leak、暴露源代码,甚至执行任意代码。
3. Web 框架(例如,使用 action 或 servlet 将请求转发给 JSP 的 Struts 和 Spring 框架)构建的应用程序中直接访问 Java Server Pages (JSP),可能导致出现未处理的异常和系统信息泄露。使用经特殊技术处理的请求(例如 http://host/page.jsp%00 或http://host/page.js%2570),会导致执行不力或配置不佳的应用程序服务器泄露源代码详细信息。更糟的是,如果应用程序允许用户上传任意文件,攻击者可能利用此机制通过 JSP 的形式上传恶意代码并要求上传的页面在服务器上执行恶意代码。
示例1 以下示例显示了构造不佳的安全限制,其中明确允许使用带有 "*" 的角色名称来直接访问 JSP,这表明所有用户均可以访问相应的 Web 资源。
...
<security-constraint>
<web-resource-collection>
<web-resource-name>JSP Access for Everyone!</web-resource-name>
<description>Allow any user/role access to JSP</description>
<url-pattern>*.jsp</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
</security-constrain>
...
系统数据或调试信息有助于攻击者了解系统并制定攻击计划。
统数据或调试信息通过输出流或者日志功能流出程序时,就会发生信息泄漏。
示例2 以下代码针对标准的错误流输出了一个异常:
...
try {
...
} catch (Exception e) {
e.printStackTrace();
}
...
依据这一系统配置,该信息可转储到控制台,写成日志文件,或者显示给远程用户。例如,凭借脚本机制,可以轻松将输出信息从“标准错误”或“标准输出”重定向至文件或其他程序。或者,运行程序的系统可能具有将日志发送至远程设备的远程日志记录系统,例如 "syslog" 服务器。在开发过程中,您将无法知道此信息最终可能显示的位置。
在某些情况下,该错误消息恰好可以告诉攻击者入侵这一系统的可能性究竟有多大。例如,一个数据库错误消息可以揭示应用程序容易受到 SQL Injection 攻击。其他的错误消息可以揭示有关该系统的更多间接线索。在上述例子中,泄露的信息可能会暗示操作系统的类型、系统上安装了哪些应用程序,以及管理员在配置应用程序时做了哪些方面的努力。
修复建议:
生产环境中的应用程序不应该使用生成内部细节(例如堆栈跟踪和错误消息)的方法,除非这些信息直接提交到最终用户无法查看的日志中。所有错误消息文本应该是HTML实体编码之前写入日志文件,以防止潜在的跨站脚本攻击对日志进行查看。
表达式永假
摘要
此表达式(或部分表达式)的计算结果始终为 false。
缺陷描述
此表达式(或部分表达式)的计算结果始终为 false;程序可以按一种更为简单的方式重写。而其附近代码的出现可能是出于调试目的,或者可能没有与程序中的其他代码一同进行维护。该表达式还可能为我们指出方法中的错误所在。
示例1 以下方法将变量 secondCall 初始化为 false 后,将不会再对该变量进行设置。(变量 firstCall 被错误地使用了两次。)其结果是,表达式 firstCall && secondCall 的计算结果始终为 false,所以永远不会调用 setUpDualCall()。
...
public void setUpCalls() {
boolean firstCall = false;
boolean secondCall = false;
if (fCall > 0) {
setUpFCall();
firstCall = true;
}
if (sCall > 0) {
setUpSCall();
firstCall = true;
}
if (firstCall && secondCall) {
setUpDualCall();
}
}
...
示例2 以下方法不会再将变量 firstCall 设为 true。(在首个条件语句后,变量 firstCall 被错误地设为 false。)其结果是,表达式 firstCall && secondCall 的第一部分的计算结果始终为 false。
...
public void setUpCalls() {
boolean firstCall = false;
boolean secondCall = false;
if (fCall > 0) {
setUpFCall();
firstCall = false;
}
if (sCall > 0) {
setUpSCall();
secondCall = true;
}
if (firstCall || secondCall) {
setUpForCall();
}
}...
修复建议:
总之,您应该去修改或是删除未使用的代码。它不仅不能实现任何程序功能,还会带来额外的麻烦和维修负担。
表达式永真
摘要
此表达式(或部分表达式)的计算结果始终为 true。
缺陷描述
此表达式(或部分表达式)的计算结果始终为 true;程序可以按一种更为简单的方式重写。而其附近代码的出现可能是出于调试目的,或者可能没有与程序中的其他代码一同进行维护。该表达式还可能为我们指出方法中的错误所在。
示例1 以下方法将变量 secondCall 初始化为 true 后,将不会再对该变量进行设置。(变量 firstCall 被错误地使用了两次。)其结果是,表达式 firstCall || secondCall 的计算结果始终为 true,所以始终会调用 setUpForCall()。
...
public void setUpCalls() {
boolean firstCall = true;
boolean secondCall = true;
if (fCall < 0) {
cancelFCall();
firstCall = false;
}
if (sCall < 0) {
cancelSCall();
firstCall = false;
}
if (firstCall || secondCall) {
setUpForCall();
}
}
...
示例2 以下方法尝试检查变量 firstCall 和 secondCall。(变量 firstCall 被错误地设为 true,而不是对其进行检查。)其结果是,表达式 firstCall = true && secondCall == true 的第一部分的计算结果始终为 true。
public void setUpCalls() {
boolean firstCall = false;
boolean secondCall = false;
if (fCall > 0) {
setUpFCall();
firstCall = true;
}
if (sCall > 0) {
setUpSCall();
secondCall = true;
}
if (firstCall = true && secondCall == true) {
setUpDualCall();
}
}
修复建议:
总之,您应该去修改或是删除未使用的代码。它不仅不能实现任何程序功能,还会带来额外的麻烦和维修负担。
捕获过于泛化异常的声明
摘要
捕捉过于泛化的异常更可能会导致包含安全漏洞的代码错误处理起来更复杂。
缺陷描述
多catch可能会使情况棘手而且产生重复,但通过捕捉像Exception这样的高级别类来“压缩”catch块可以掩盖需要特殊处理或者在程序这一点上不该被捕捉到的异常。捕捉过于泛化的异常会从根本上毁掉Java的输入异常目的,而且程序增大的情况下可能会变得非常危险,并开始抛出新类型的异常。新的异常类型不会收到任何注意。
示例1示例代码在同一个catch块中处理所有开发人员预期异常。
try {
doExchange();
}
catch (Exception e) {
logger.error("doExchange failed", e);
}
但是,如果对doExchange()进行修改来抛出一种本该以不同方式处理的新的异常,那么泛化的catch块就会阻止编译人员指明情况。此外,新的catch块现在也会处理来自RuntimeException的异常,如ClassCastException 和NullPointerException,这些都不是程序员本意要进行处理的情况。
修复建议:
不要在在同一个catch块中处理所有异常。
示例2 下面的代码引用以同样方式处理三种类型的异常。
try {
doExchange();
}
catch (IOException e) {
logger.error("doExchange failed", e);
}
catch (InvocationTargetException e) {
logger.error("doExchange failed", e);
}
catch (SQLException e) {
logger.error("doExchange failed", e);
}
不合理的变量名
摘要
变量命名不规范。
缺陷描述
本规则 检测变量名没有以骆驼法则命名(testName),比如test_name。
示例1 错误的写法。
...
String user_name="name";
修复建议:
按照骆驼法则对变量名称进行命名。
示例2 “示例1”可改写为:
...
String userName="name";
不合理的方法名
CWE ID: 200011
摘要
方法命名不规范。
缺陷描述
本规则检测方法名是否正确以小写字母开头。
示例1 将检测出的错误写法。
public class Test {
public static String GetUser(String abc, String pass){
...
}
}
修复建议:
示例2 正确的写法。
public class Test {
public static String getUser(String abc, String pass){
...
}
}
表达式永假
摘要
此表达式(或部分表达式)的计算结果始终为 false。
缺陷描述
此表达式(或部分表达式)的计算结果始终为 false;程序可以按一种更为简单的方式重写。而其附近代码的出现可能是出于调试目的,或者可能没有与程序中的其他代码一同进行维护。该表达式还可能为我们指出方法中的错误所在。
示例1 以下方法将变量 secondCall 初始化为 false 后,将不会再对该变量进行设置。(变量 firstCall 被错误地使用了两次。)其结果是,表达式 firstCall && secondCall 的计算结果始终为 false,所以永远不会调用 setUpDualCall()。
...
public void setUpCalls() {
boolean firstCall = false;
boolean secondCall = false;
if (fCall > 0) {
setUpFCall();
firstCall = true;
}
if (sCall > 0) {
setUpSCall();
firstCall = true;
}
if (firstCall && secondCall) {
setUpDualCall();
}
}
...
示例2 以下方法不会再将变量 firstCall 设为 true。(在首个条件语句后,变量 firstCall 被错误地设为 false。)其结果是,表达式 firstCall && secondCall 的第一部分的计算结果始终为 false。
...
public void setUpCalls() {
boolean firstCall = false;
boolean secondCall = false;
if (fCall > 0) {
setUpFCall();
firstCall = false;
}
if (sCall > 0) {
setUpSCall();
secondCall = true;
}
if (firstCall || secondCall) {
setUpForCall();
}
}...
修复建议:
总之,您应该去修改或是删除未使用的代码。它不仅不能实现任何程序功能,还会带来额外的麻烦和维修负担。
表达式永真
摘要
此表达式(或部分表达式)的计算结果始终为 true。
缺陷描述
此表达式(或部分表达式)的计算结果始终为 true;程序可以按一种更为简单的方式重写。而其附近代码的出现可能是出于调试目的,或者可能没有与程序中的其他代码一同进行维护。该表达式还可能为我们指出方法中的错误所在。
示例1 以下方法将变量 secondCall 初始化为 true 后,将不会再对该变量进行设置。(变量 firstCall 被错误地使用了两次。)其结果是,表达式 firstCall || secondCall 的计算结果始终为 true,所以始终会调用 setUpForCall()。
...
public void setUpCalls() {
boolean firstCall = true;
boolean secondCall = true;
if (fCall < 0) {
cancelFCall();
firstCall = false;
}
if (sCall < 0) {
cancelSCall();
firstCall = false;
}
if (firstCall || secondCall) {
setUpForCall();
}
}
...
示例2 以下方法尝试检查变量 firstCall 和 secondCall。(变量 firstCall 被错误地设为 true,而不是对其进行检查。)其结果是,表达式 firstCall = true && secondCall == true 的第一部分的计算结果始终为 true。
public void setUpCalls() {
boolean firstCall = false;
boolean secondCall = false;
if (fCall > 0) {
setUpFCall();
firstCall = true;
}
if (sCall > 0) {
setUpSCall();
secondCall = true;
}
if (firstCall = true && secondCall == true) {
setUpDualCall();
}
}
修复建议:
总之,您应该去修改或是删除未使用的代码。它不仅不能实现任何程序功能,还会带来额外的麻烦和维修负担。
捕获过于泛化异常的声明
摘要
捕捉过于泛化的异常更可能会导致包含安全漏洞的代码错误处理起来更复杂。
缺陷描述
多catch可能会使情况棘手而且产生重复,但通过捕捉像Exception这样的高级别类来“压缩”catch块可以掩盖需要特殊处理或者在程序这一点上不该被捕捉到的异常。捕捉过于泛化的异常会从根本上毁掉Java的输入异常目的,而且程序增大的情况下可能会变得非常危险,并开始抛出新类型的异常。新的异常类型不会收到任何注意。
示例1示例代码在同一个catch块中处理所有开发人员预期异常。
try {
doExchange();
}
catch (Exception e) {
logger.error("doExchange failed", e);
}
但是,如果对doExchange()进行修改来抛出一种本该以不同方式处理的新的异常,那么泛化的catch块就会阻止编译人员指明情况。此外,新的catch块现在也会处理来自RuntimeException的异常,如ClassCastException 和NullPointerException,这些都不是程序员本意要进行处理的情况。
修复建议:
不要在在同一个catch块中处理所有异常。
示例2 下面的代码引用以同样方式处理三种类型的异常。
try {
doExchange();
}
catch (IOException e) {
logger.error("doExchange failed", e);
}
catch (InvocationTargetException e) {
logger.error("doExchange failed", e);
}
catch (SQLException e) {
logger.error("doExchange failed", e);
}
不合理的变量名
摘要
变量命名不规范。
缺陷描述
本规则 检测变量名没有以骆驼法则命名(testName),比如test_name。
示例1 错误的写法。
...
String user_name="name";
修复建议:
按照骆驼法则对变量名称进行命名。
示例2 “示例1”可改写为:
...
String userName="name";
不合理的方法名
摘要
方法命名不规范。
缺陷描述
本规则检测方法名是否正确以小写字母开头。
示例1 将检测出的错误写法。
public class Test {
public static String GetUser(String abc, String pass){
...
}
}
修复建议:
示例2 正确的写法。
public class Test {
public static String getUser(String abc, String pass){
...
}
}
不合理的静态常量名
摘要
静态常量设置不正确。
缺陷描述
本规则检测静态常量没有全部大写。
示例1 错误的写法。
public static final string User_Type="USER_TYPE";
修复建议:
静态常量全部大写。
示例2 “示例1”可改写为:
public static final string USER_TYPE="USER_TYPE";
不可控的递归
摘要
该产品没有妥善控制递归的发生数量,消耗过多的已分配内存或程序堆栈等资源。
缺陷描述
CPU、内存和栈内存等资源,可能会被迅速消耗或耗尽,最终导致退出或崩溃。
在某些情况下,应用程序的解释器可能会杀死似乎在消耗过多资源的进程或线程,如PHP的memory_limit设置。当解释器杀死该进程/线程时,就可能会报告其中包含有应用程序安装路径之类详细信息的错误。
修复建议:
限制对合理数量进行递归调用的数目。
不正确的格式化字符串数量
摘要
格式化字符串参数数量与实际传递的参数值不匹配,将导致Format Exception。
缺陷描述
DMSCA检测格式字符串中格式符与参数数量不相等的错误。
与C语言的sprintf()方法类似,Java中String类的format()方法用于创建格式化的字符串以及连接多个字符串对象,它有两种重载形式:
① format(String format, Object... args) 新字符串使用本地语言环境,制定字符串格式和参数生成格式化的新字符串。
② format(Locale locale, String format, Object... args) 使用指定的语言环境,制定字符串格式和参数生成格式化的字符串。
以下显示不同转换符实现不同数据类型到字符串的转换:
转 换 符 说 明 示 例
%s 字符串类型 "mingrisoft"
%c 字符类型 'm'
%b 布尔类型 TRUE
%d 整数类型(十进制) 99
%x 整数类型(十六进制) FF
%o 整数类型(八进制) 77
%f 浮点类型 99.99
%a 十六进制浮点类型 FF.35AE
%e 指数类型 9.38E+05
%g 通用浮点类型(f和e类型中较短的)
%h 散列码
%% 百分比类型 %
%n 换行符
%tx 日期与时间类型(x代表不同的日期与时间转换符)
修复建议:
确保格式化字符串中格式符与参数数量保持一致。
信任边界违规
摘要
在同一数据结构中将可信赖数据和不可信赖数据混合在一起会导致程序员错误地信赖未验证的数据。
缺陷描述
信任边界可以理解为在程序中划分的分界线。分界线的一边是不可信赖的数据。分界线的另一边则是被认定为是可信赖的数据。验证逻辑的用途是允许数据安全地跨越信任边界 — 从不可信赖的一边移动到可信赖的另一边。
当程序使可信赖和不可信赖的分界线模糊不清时,就会发生 Trust Boundary Violation 漏洞。发生这种错误的最普遍方式是允许可信赖的数据和不可信赖的数据共同混合在同一数据结构中。
示例1 以下 Java 代码接受了一个 HTTP 请求。在 HTTP 会话对象中存储了 usrname 参数以后,再进行检查,以确保该用户已经过了验证。
...
usrname = request.getParameter("usrname");
if (session.getAttribute(ATTR_USR) == null) {
session.setAttribute(ATTR_USR, usrname);
}
...
若不对信任边界进行合理构建及良好维护,则程序员不可避免地会混淆哪些数据已经过验证,哪些尚未经过验证。这种混淆最终会导致某些数据未经验证就加以使用了。
修复建议:
对信任边界进行合理构建及良好维护。
错误的块划分
摘要
代码没有明确地定义一个包含2个或更多语句的块,从而导致逻辑错误。
缺陷描述
在某些语言中,括号(或其他符号)对块是可选的。省略分隔符时,可能会插入一个逻辑错误,其中一个语句被认为是在一个块中,但实际上不是。在某些情况下,逻辑错误可能具有安全含义。
这是个一般性逻辑错误,往往会导致明显的不正确的行为,也很容发现和修复。在轻量测试或未经测试的代码中,此错误可能会被引入到生产环境中,并通过创建导致应用程序中的意外状态的控制流路径提供额外的攻击向量。后果将取决于被错误执行的行为类型。
示例1 在这个例子中,程序员陈述了有意调用Do_X()和Do_Y(),似乎意图是仅仅当条件为真时才调用这些方法。然而,由于没有括号来表示块,Do_Y()总是会被执行,即使条件为假。
if (condition==true)
Do_X();
Do_Y();
这可能不是程序员的真正意图。当条件涉及至关重要的安全,如在作出安全决策或检测一个关键错误,这可能会产生一个漏洞。
示例2 在这个例子中,程序员有缩进的Do_Y() 声明如果意图的功能应与前一条件相关,仅仅当条件为真时。然而,因为do_x()被称为在同一行的条件和没有括号表示分组,do_y()总是会被执行,即使条件为假。
if (condition==true) Do_X();
Do_Y();
这可能不是程序员的意图。当条件是至关重要的安全,如在作出安全决策或检测一个关键错误,这可能会产生一个漏洞。
修复建议:
始终使用明确的块界限,并使用静态分析技术来执行这一做法。
错误消息导致的信息泄露
摘要
揭示系统数据或调试信息有助于攻击者了解系统并制定攻击计划。
缺陷描述
当系统数据或调试信息通过输出流或者日志功能流出程序时,就会发生信息泄漏。
示例1 以下代码会将异常打印到标准错误流:
try {
...
} catch (Exception e) {
e.printStackTrace();
}
依据这一系统配置,该信息可转储到控制台,写成日志文件,或者显示给远程用户。例如,凭借脚本机制,可以轻松将输出信息从“标准错误”或“标准输出”重定向至文件或其他程序。或者,运行程序的系统可能具有将日志发送至远程设备的远程日志记录系统,例如 "syslog" 服务器。在开发过程中,您将无法知道此信息最终可能显示的位置。
在某些情况下,该错误消息恰好可以告诉攻击者入侵这一系统的可能性究竟有多大。例如,一个数据库错误消息可以揭示应用程序容易受到 SQL Injection 攻击。其他的错误消息可以揭示有关该系统的更多间接线索。在上述例子中,泄露的信息可能会暗示操作系统的类型、系统上安装了哪些应用程序,以及管理员在配置应用程序时做了哪些方面的努力。
这是另一种情况,特定于移动世界。大多数移动设备现在执行的是“近场通信”(NFC) 协议,以便使用无线电通信在设备之间快速共享信息。它在设备极为贴近或互相接触时有效。即使 NFC 的通信范围仅局限于几厘米,也可能发生窃听、修改数据以及各种其他类型的攻击情况,因为 NFC 本身并不能确保通信安全。
示例2 Android 平台提供对 NFC 的支持。以下代码将创建一条消息,该消息会被发送给所在范围内的其他设备。
...
public static final String TAG = "NfcActivity";
private static final String DATA_SPLITTER = "__:DATA:__";
private static final String MIME_TYPE = "application/my.applications.mimetype";
...
public NdefMessage createNdefMessage(NfcEvent event) {
TelephonyManager tm = (TelephonyManager)Context.getSystemService(Context.TELEPHONY_SERVICE);
String VERSION = tm.getDeviceSoftwareVersion();
String text = TAG + DATA_SPLITTER + VERSION;
NdefRecord record = new NdefRecord(NdefRecord.TNF_MIME_MEDIA,
MIME_TYPE.getBytes(), new byte[0], text.getBytes());
NdefRecord[] records = { record };
NdefMessage msg = new NdefMessage(records);
return msg;
}
...
修复建议:
编写错误消息时,始终要牢记安全性。在编码的过程中,尽量避免使用繁复的消息,提倡使用简短的错误消息。限制生成与存储繁复的输出数据将有助于管理员和程序员诊断问题的所在。此外,还要留意有关调试的跟踪信息,有时它可能出现在不明显的位置(例如嵌入在错误页 HTML 代码的注释行中)。
即便是并未揭示栈踪迹或数据库转储的简短错误消息,也有可能帮助攻击者发起攻击。例如,“Access Denied”(拒绝访问)消息可以揭示系统中存在一个文件或用户。
如果您担心 Android 设备上的系统数据会通过 NFC 泄露,那么您可以采取以下三种措施之一。不把系统数据包括在发送到范围内其他设备的消息中,或加密消息负载,或在更高层中建立安全通信通道。
注意事项:
- 不要依赖于封装器脚本、组织内部的 IT 策略或是思维敏捷的系统管理员来避免 System Information Leak 漏洞。编写安全的软件才是关键。
- 这类漏洞并不适用于所有类型的程序。例如,如果您在一个客户机上执行应用程序,而攻击者已经获取了该客户机上的系统信息,或者如果您仅把系统信息打印到一个可信赖的日志文件中,就可以使用 AuditGuide 来过滤这一类别。
对关键资源授予不正确的权限
摘要
该软件指定的安全关键资源的权限的方式,允许该资源任意人员读取或修改。
缺陷描述
当某资源开放权限设置,提供比实际需要更广泛的访问范围时,它可能会导致敏感信息的曝光,或资源被各方随意修改。当资源与程序配置、执行或敏感的用户数据相关时,这种举动尤其危险。
示例1 下面C语言代码示例在创建文件并写入“Hello World”之前,将进程的unmask设置为0。
#define OUTFILE "hello.out"
umask(0);
FILE *out;
/* Ignore CWE-59 (link following) for brevity */
out = fopen(OUTFILE, "w");
if (out) {
fprintf(out, "hello world!\n");
fclose(out);
}
在UNIX系统上运行此程序后,运行“ls”命令可能返回以下输出:
-rw-rw-rw- 1 username 13 Nov 24 17:58 hello.out
“rw-rw-rw-”字符串表示其所有者、所在组和外界(所有用户)可以读取文件并写入它。
示例2 下面的PHP语言代码示例为新用户创建主目录,并将该用户定义为目录的所有者。如果新目录不能由用户拥有,则目录将被删除。
function createUserDir($username){
$path = '/home/'.$username;
if(!mkdir($path)){
return false;
}
if(!chown($path,$username)){
rmdir($path);
return false;
}
return true;
}
目录的默认权限0777将新用户设置为目录所有者,不显式更改目录的权限,并使其处于默认状态。此默认允许任何用户对目录进行读写,允许对用户文件进行攻击。代码也未能更改目录的所有者组,这可能导致意外组访问。
修复建议:
使用关键资源如配置文件时,检查是否有不安全的资源权限(如可被任何普通用户修改)[ r.732.1 ],如果该资源有可能被未授权方进行修改,生成一个错误甚至退出软件。
访问说明符操纵
摘要
方法调用可更改访问说明符。
缺陷描述
AccessibleObject API 允许程序员绕过由 Java 访问说明符提供的 access control 检查,从而绕过由Java访问说明符提供的接入控制检查,导致程序私有字段、私有方法和行为被反向调用,这些通常情况下都是不允许的,可能产生未授权的命令执行等严重安全隐患。
修复建议:
对访问说明符进行合理的权限限制:使用攻击者无法设置的参数,通过有权限的类更改访问说明符;所有出现的访问说明符都应仔细检查。
检测错误条件但未采取操作
摘要
忽略异常会导致程序无法发现意外状况和情况。
缺陷描述
几乎每一个对软件系统的严重攻击都是从违反程序员的假设开始的。攻击后,程序员的假设看起来既脆弱又拙劣,但攻击前,许多程序员会在午休时间为自己的种种假设做很好的辩护。
在代码中,很容易发现两个令人怀疑的假设:“一是这个函数调用不可能出错;二是即使出错了,也不会对系统造成什么重要影响。”因此当程序员忽略异常时,这其实表明了他们是基于上述假设进行操作。
示例1 下面摘录的代码会忽略一个由 DoExchange() 抛出的罕见异常。
try {
DoExchange();
}
catch (RareException e) {
// this can never happen
}
如果抛出 RareException 异常,程序会继续执行,就像什么都没有发生一样。程序不会记录任何有关这一特殊情况的依据,因而事后再查找这一异常就可能很困难。
修复建议:
至少,应该记录抛出异常的事实,以便于稍后查询及预知对程序运行所造成的影响。然而最好是中止当前操作。如果忽略某个异常的原因是因为调用者无法正确处理该异常,而程序上下文使调用者不便或不可能声明程序会抛出这一异常,那么可以考虑抛出 RuntimeException 或 Error 异常,两者均是未检查的异常。在 JDK 1.4 中,RuntimeException 有一个构造函数,可以方便地用来封装另其他异常。
示例2 “示例1”中的代码应该用以下方式重写。
try {
doExchange();
}
catch (RareException e) {
throw RuntimeException("This can never happen", e);
}
注意事项:
1.只有极少的异常类型可以在特定的上下文中丢弃。例如,Thread.sleep() 会抛出 InterruptedException 异常,并且在大多数情况下,程序应该以相同的方式决定线程是否唤醒过早。
try {
Thread.sleep(1000);
}
catch (InterruptedException e){
// The thread has been woken up prematurely, but its
// behavior should be the same either way.
}
交易处理不当
摘要
产品没有清空它的状态或者在异常抛出时没有正确的清除它的状态,从而导致不期待的状态或控制流。
缺陷描述
代码可以被留在一个不好的状态。
示例1 在这个例子中,你可能会导致一个线程意外的被锁定。
...
public class foo {
public static final void main( String args[] ) {
boolean returnValue;
returnValue=doStuff();
}
public static final boolean doStuff( ) {
boolean threadLock;
boolean truthvalue=true;
try {
while(//check some condition) {
threadLock=true; //do some stuff to truthvalue
threadLock=false;
}
} catch (Exception e){
System.err.println("You did something bad");
if (something) return truthvalue;
}
return truthvalue;
}
}
...
修复建议:
如果要通过抛出一个异常来跳出一个循环或者方法,确保要做清洁或者结束程序。不要过多的使用抛出异常。
注意事项:
当方法或者循环变得很复杂时,经常需要从开头到结尾对一些层级进行清除。由于异常可以打乱代码流,常常把一个代码块留在一个不好的状态。
竞态条件
摘要
该程序包含与其他代码同时运行的代码序列,代码序列需要对共享资源进行临时的、独占的访问,但存在时间窗,其中共享资源可以由另一个并发运行的代码序列修改。
缺陷描述
Race Condition发生在并发环境中,并有效地成为代码序列的属性。根据上下文,一个编码序列可以作为函数调用的形式,也可以作为少量的指令,一系列的程序调用等。
以Class Initialization Cycle为例,为新对象分配静态字段会调用构造函数,即使其依赖于其他变量初始化也是如此,这将导致为对象错误地执行初始化。
在对 Java 类执行初始化后,会先为类中声明的静态字段调用初始值设定项,然后再调用类的构造函数。这意味着,向其分配的构造函数会先于其他代码被调用,如果此构造函数依赖于其他要初始化的字段或变量,这可能会导致对象仅部分完成初始化,或使用错误的值为对象执行初始化。
示例1 以下类声明了静态字段,并将其分配至新对象。
...
public class Box{
public int area;
public static final int width = 10;
public static final Box box = new Box();
public static final int height = (int) (Math.random() * 100);
public Box(){
area = width * height;
}
...
}
...
在以上代码中,因为 width 等于 10,所以开发人员可能会希望 box.area 是 10 的随机整数倍。但在现实情况中,它可能始终具有硬编码值 0。首先会对已声明具有编译时常量的静态最终字段执行初始化,然后再依次执行各代码。这意味着:由于 height 并非编译时常量,因此在声明 box 后才会对其进行声明,从而造成在调用构造函数后对 height 执行初始化。
示例2 以下类声明了互相依赖的静态字段。
...
class Foo{
public static final int f = Bar.b - 1;
...
}
...
class Bar{
public static final int b = Foo.f + 1;
...
}
//This example is perhaps easier to identify, but would be dependent on which class is loaded first //by the JVM. In this example Foo.f could be either -1 or 0, and Bar.b could be either 0 or 1.
再来看,java.text.Format 中的 parse() 和 format() 方法包含一个可导致用户看到其他用户数据的 race condition。
示例3 以下代码显示了此设计缺陷如何暴露自己。
public class Common {
private static SimpleDateFormat dateFormat;
...
public String format(Date date) {
return dateFormat.format(date);
}
...
final OtherClass dateFormatAccess=new OtherClass();
...
public void function_running_in_thread1(){
System.out.println("Time in thread 1 should be 12/31/69 4:00 PM, found: "+ dateFormatAccess.format(new Date(0)));
}
public void function_running_in_thread2(){
System.out.println("Time in thread 2 should be around 12/29/09 6:26 AM, found: "+ dateFormatAccess.format(new Date(System.currentTimeMillis())));
}
}
尽管此代码可在单一用户环境中正常运行,但如果两个线程同时运行此代码,则会生成以下输出内容:
Time in thread 1 should be 12/31/69 4:00 PM, found:12/31/69 4:00 PM
Time in thread 2 should be around 12/29/09 6:26 AM, found:12/31/69 4:00 PM
在这种情况下,第一个线程中的数据显示在了第二个线程的输出中,原因是实施的 format() 中存在 race condition。
再来看,Servlet 成员字段可能允许一个用户查看其他用户的数据。
许多 Servlet 开发人员都不了解 Servlet 为单例模式。Servlet 只有一个实例,并通过使用和重复使用该单个实例来处理需要由不同线程同时处理的多个请求。
这种误解的共同后果是,开发者使用 Servlet 成员字段的这种方式会导致某个用户可能在无意中看到其他用户的数据。换言之,即把用户数据存储在 Servlet 成员字段中会引发数据访问的 race condition。
示例4 以下 Servlet 把请求参数值存储在成员字段中,然后将参数值返回给响应输出流。
public class GuestBook extends HttpServlet {
String name;
protected void doPost (HttpServletRequest req, HttpServletResponse res) {
name = req.getParameter("name");
...
out.println(name + ", thanks for visiting!");
}
}
当该代码在单一用户环境中正常运行时,如果有两个用户几乎同时访问 Servlet,可能会导致这两个请求以如下方式处理线程的插入:
线程 1: 将“Dick”分配给 name
线程 2: 将“Dick”分配给 name
线程 1: print“Jane, thanks for visiting!”
线程 2: print“Jane, thanks for visiting!”
因此会向第一个用户显示第二个用户的用户名。
还有一种情形也值得注意,存储在静态字段中的数据库连接会被不同的线程共享。
对于与事务相关联的资源对象(比如数据库连接),一次只能与一个事务相关联。出于这个原因,一个连接不应该被多个线程共享,并且不应该存储在静态字段中。要获取更多详细信息,请参见 J2EE 规范中的第 4.2.3 节。
示例5
public class ConnectionManager {
private static Connection conn = initDbConn();
...
}
修复建议:
使用多线程和共享变量操作时,只使用线程安全函数。
在共享变量上使用原子操作。警惕无辜的前瞻性结构,如“X”。这可能出现在代码层的原子,但它实际上是非原子的指令层,因为它涉及到一个读操作,其次是一个计算操作,紧接一个写操作。
如果使用一个互斥锁,但一定要避免相关的弱点如cwe-412。
尝试避免同步的开销,避免双重检查锁定(cwe-609)和其他执行错误出现。
禁用中断或信号的关键部分的代码,但也要确保代码不进入一个大的或无限的循环。
为关键变量使用volatile类型修饰符以避免意外的编译器优化或重新排序。这并不一定解决同步问题,但它可以提供帮助。
使用过期函数
摘要
使用不推荐的或过时的函数可能表示这是一段被忽略的代码。
缺陷描述
随着编程语言的发展,有时会弃用些一些方法,原因是:
1. 为了改进该编程语言。
2. 对如何有效、安全地执行操作有了更深一步的了解。
3. 某些操作的管理规则发生了变化。
在某一种编程语言中,人们通常会放弃使用某些方法,转而采用更新的方法。在执行同样的任务时,新方法会采用不同的处理方式,这种方式往往比原有的方法更合理。
示例1 以下代码使用一个字节数组和一个指定每个 16 位 Unicode 字符中前 8 位的值来构造一个字符串对象。
...
String name = new String(nameBytes, highByte);
...
在这个例子中,构造函数可能无法正确地将字节转换成字符,这取决于由 nameBytes 表述的编码字符串所使用的字符集。随着用于编码字符串的字符集的不断发展,这个构造函数已经过时,取而代之的是新的构造函数。新构造函数使用了名为 charset 的参数,用以对字节进行编码,从而实现字节的转换。
并非所有函数都会因为存在安全漏洞而被弃用或被取代。然而,出现被弃用的函数通常表示周围代码已经不起作用了,有可能处于不受维护的状况。在过去很长一段时间内,人们并没有将软件安全放在首位,甚至都未曾考虑过。如果程序使用了不推荐的或过时的函数,在其附近就会潜伏着安全问题。
修复建议:
请不要使用不推荐的或过时的函数。不管是否会对安全产生直接影响,都要使用最新的函数来代替这些过时的函数。当您遇到过时的函数时,应意识到它的出现可能会给周围的代码带来安全隐患。请考虑进行应用程序开发时所依据的有关安全方面的各种假设。它们是否仍然有效?使用特定的过时函数是否会带来更大的维护问题?
使用不够随机的伪随机数
摘要
不安全的随机数Insecure Randomness,标准的伪随机数值生成器不能抵挡各种加密攻击。
缺陷描述
在对安全性要求较高的环境中,使用一个能产生可预测数值的函数作为随机数据源,会产生 Insecure Randomness 错误。
电脑是一种具有确定性的机器,因此不可能产生真正的随机性。伪随机数生成器 (PRNG) 近似于随机算法,始于一个能计算后续数值的种子。
PRNG 包括两种类型:统计学的 PRNG 和密码学的 PRNG。统计学的 PRNG 可提供有用的统计资料,但其输出结果很容易预测,因此数据流容易复制。若安全性取决于生成数值的不可预测性,则此类型不适用。密码学的 PRNG 通过可产生较难预测的输出结果来应对这一问题。为了使加密数值更为安全,必须使攻击者根本无法、或极不可能将它与真实的随机数加以区分。通常情况下,如果并未声明 PRNG 算法带有加密保护,那么它有可能就是一个统计学的 PRNG,不应在对安全性要求较高的环境中使用,其中随着它的使用可能会导致严重的漏洞(如易于猜测的密码、可预测的加密密钥、会话劫持攻击和 DNS 欺骗)。
示例1 下面的代码可利用统计学的 PRNG 为购买产品后仍在有效期内的收据创建一个 URL。
String GenerateReceiptURL(String baseUrl) {
Random ranGen = new Random();
ranGen.setSeed((new Date()).getTime());
return (baseUrl + ranGen.nextInt(400000000) + ".html");
}
这段代码使用 Random.nextInt() 函数为它所生成的收据页面生成独特的标识符。因为 Random.nextInt() 是一个统计学的 PRNG,攻击者很容易猜到由它所生成的字符串。尽管收据系统的底层设计也存在错误,但如果使用了一个不生成可预测收据标识符的随机数生成器(如密码学的 PRNG),会更安全一些。
修复建议:
当不可预测性至关重要时,如大多数对安全性要求较高的环境都采用随机性,这时可以使用密码学的 PRNG。不管选择了哪一种 PRNG,都要始终使用带有充足熵的数值作为该算法的种子。(诸如当前时间之类的数值只提供很小的熵,因此不应该使用。)
Java 语言在 java.security.SecureRandom 中提供了一个加密 PRNG。就像 java.security 中其他以算法为基础的类那样,SecureRandom 提供了与某个特定算法集合相关的包,该包可以独立实现。当使用 SecureRandom.getInstance() 请求一个 SecureRandom 实例时,您可以申请实现某个特定的算法。如果算法可行,那么您可以将它作为 SecureRandom 的对象使用。如果算法不可行,或者您没有为算法明确特定的实现方法,那么会由系统为您选择 SecureRandom 的实现方法。
Sun 在名为 SHA1PRNG 的 Java 版本中提供了一种单独实现 SecureRandom 的方式,Sun 将其描述为计算:
“SHA-1 可以计算一个真实的随机种子参数的散列值,同时,该种子参数带有一个 64 比特的计算器,会在每一次操作后加 1。在 160 比特的 SHA-1 输出中,只能使用 64 比特的输出 [1]。”
然而,文档中有关 Sun 的 SHA1PRNG 算法实现细节的相关记录很少,人们无法了解算法实现中使用的熵的来源,因此也并不清楚输出中到底存在多少真实的随机数值。尽管有关 Sun 的实现方法网络上有各种各样的猜测,但是有一点无庸置疑,即算法具有很强的加密性,可以在对安全性极为敏感的各种内容中安全地使用。
客户端不充分的点击劫持保护
摘要
脆弱的点击劫持攻击的根本原因,是应用程序的Web页可以加载到另一个网站的框架。应用程序没有实现正确的框架破坏脚本,这将阻止页面加载到另一个框架中。值得注意的是,有许多类型的简单的重定向脚本,还使应用程序很容易受到劫持技术,而不应该用。
此外,还要注意的是,这种攻击可以最好的保护设置适当的值在HTTP响应头在服务器端,如内容安全政策(CSP)或“X-Frame-Options”。如果服务器正确设置了这些头,则可以忽略此结果。
缺陷描述
点击劫持攻击允许攻击者劫持用户的点击一个网页的鼠标,通过无形的框架中的应用,并将在一个虚假的网站前台。当用户确信点击虚假网站,例如在链接或按钮上时,用户的鼠标实际上是点击目标网页,尽管是看不见的。
这可能允许攻击者造成用户执行在脆弱的应用程序的任何不良行为,如使用户的摄像头,删除所有用户的记录,改变用户的设置,或造成clickfraud。
示例1 Clickjackable Webpage。
<html>
<body>
<button onclick="clicked();">
Click here if you love ducks
</button>
</body>
</html>
修复建议:
实现对客户适当的framebuster脚本,特别是,该framebuster脚本应该包括CSS默认禁用的用户界面,和一个默认禁用JavaScript framebuster。
示例2 Bustable Framebuster。
<html>
<head>
<script>
if ( window.self.location != window.top.location ) {
window.top.location = window.self.location;
}
</script>
</head>
<body>
<button onclick="clicked();">
Click here if you love ducks
</button>
</body>
</html>
示例3 Proper Framebusterbusterbusting。
<html>
<head>
<style> html {display : none; } </style>
<script>
if ( self == top ) {
document.documentElement.style.display = 'block';
}
else {
top.location = self.location;
}
</script>
</head>
<body>
<button onclick="clicked();">
Click here if you love ducks
</button>
</body>
|</html>
空if语句块
摘要
检测空的if语句块(分支)。
缺陷描述
空的if语句块作为冗余代码,不利于维护。
示例1
if($booleanExpression1){
...
} else if($booleanExpression2){
...
} else{
}
修复建议:
按照标准代码规范,补充空代码块逻辑,或者直接删除空的代码块。
示例2
if($booleanExpression1){
...
} else if($booleanExpression2){
...
}
空代码块
摘要
检测空的代码块。
缺陷描述
空的代码块作为冗余代码,不利于维护。
示例1
try{
...
}catch(Exception e){
}
修复建议:
按照标准代码规范,补充空代码块逻辑,或者直接删除空的代码块。
示例2
try{
...
}catch(Exception e){
...
e.printStackTrace();
}
违背最佳编码实践
摘要
开发人员没有按照良好的编码规范进行编码,有可能引入不必要的麻烦或安全问题。
缺陷描述
DMCA(端玛源代码安全静态扫描工具)会侦测出如下缺陷或漏洞:
- Class Does Not Implement Equals
- Comparison of Boxed Primitive Types
- Empty Methods
- Invalid Call to Object.equals()
- Multiple Stream Commits
- Not Static Final Logger
- Use of System Output Stream
- Throw Inside Finally
① Class Does Not Implement Equals
指在没有实现 equals() 的对象上调用了 equals() 方法。
当比较对象时,开发人员通常希望比较对象的属性。然而,在没有明确实现 equals() 的类(或任何超类/接口)上调用 equals() 会导致调用继承自 java.lang.Object 的 equals() 方法。Object.equals() 将比较两个对象实例,查看它们是否相同,而不是比较对象成员字段或其他属性。尽管可以合法地使用 Object.equals(),但这通常表示存在错误代码。
示例1
public class AccountGroup
{
private int gid;
public int getGid(){
return gid;
}
public void setGid(int newGid){
gid = newGid;
}
}
...
public class CompareGroup {
public boolean compareGroups(AccountGroup group1, AccountGroup group2) {
return group1.equals(group2); //equals() is not implemented in AccountGroup
}
}
② Comparison of Boxed Primitive Types
指如果使用相等运算符而非其 equals() 方法比较框式基元,可能会导致意外行为。
在处理框式基元时,如果需要比较相等性,则应调用框式基元的 equals() 方法,而非使用运算符 == 和 !=。Java 规范具有关于框式转换的如下说明:
"如果框式值 p 是一个整数文本(例如 -128 和 127 之间的整数),或是布尔文本 true 或 false,或者是 '\u0000' 和 '\u007f' 之间的字符文本,则使 a 和 b 作为 p 的任意两个框式转换值的结果。结果始终会是 a == b。"
这意味着,如果使用了框式基元(并非 Boolean 或 Byte),则仅会缓存或记住一个值区间。对于值的子集,使用 == 或 != 会返回正确的值,而对于此子集外的所有其他值,将返回对象地址的比较结果。
示例2 以下示例对框式基元使用相等运算符。
...
Integer mask0 = 100;
Integer mask1 = 100;
...
if (file0.readWriteAllPerms){
mask0 = 777;
}
if (file1.readWriteAllPerms){
mask1 = 777;
}
...
if (mask0 == mask1){
//assume file0 and file1 have same permissions
...
}
...
以上代码使用了框式基元 Integer 以尝试比较两个 int 值。如果 mask0 和 mask1 都等于 100,则 mask0 == mask1 将返回 true。但是,如果 mask0 和 mask1 都等于 777,则 mask0 == maske1 将返回 false,因为这些值不在这些框式基元的缓存值范围内。
③ Empty Methods
指方法为空,要么不会被调用,或者仅仅通过其他 dead code 进行调用。
示例3 在下面这个类中,doWork() 方法将永远不会被调用,或者在某段dead code处被调用。
public class Dead {
private void doWork() {
//nothing exists here
}
public static void main(String[] args) {
System.out.println("running Dead");
}
}
存在潜在的隐患。
④ Invalid Call to Object.equals()
指程序会调用数组上的 Object.equals(),而非 java.util.Arrays.equals()。
由于调用数组上的 Object.equals() 会检查数组地址是否相同而非检查数组元素是否相同,因此在大多数情况下这是一个错误调用,通常应将该代码替换为 java.util.Arrays.equals()。
示例4 以下示例尝试使用 Object.equals() 函数检查两个数组。
...
int[] arr1 = new int[10];
int[] arr2 = new int[10];
...
if (arr1.equals(arr2)){
//treat arrays as if identical elements
}
...
除非在某个点将一个数组分配至另一个数组,否则可能会始终生成一个从未执行的代码。
⑤ Multiple Stream Commits
指提交 servlet 的输出流之后,重置流缓冲区或执行重新提交该数据流的其他任何操作都是错误的做法。与之类似,在调用 getOutputStream 之后调用 getWriter() 也是错误的做法,反之亦然。
转发 HttpServletRequest、重定向 HttpServletResponse 或者刷新 servlet 的输出流缓冲区会导致提交相关的数据流。后续执行的任何缓冲区重置或数据流提交操作,例如额外的刷新或重定向,将会导致出现 IllegalStateException。
此外,Java servlets 允许使用 ServletOutputStream 或 PrintWriter(但不能同时使用)将数据写入响应数据流。调用getOutputStream() 之后调用 getWriter() 或者反向调用,也会导致出现 IllegalStateException。
在运行时,IllegalStateException 会阻止响应处理程序完成运行,轻易地使其中断响应。这会导致服务器不稳定,也间接表明 servlet 实现不正确。
示例5 以下代码会在 servlet 的输出流缓冲区刷新之后重定向其响应。
public class RedirectServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
...
OutputStream out = res.getOutputStream();
...
// flushes, and thereby commits, the output stream
out.flush();
out.close(); // redirecting the response causes an IllegalStateException
res.sendRedirect("http://www.acme.com");
}
}
示例6 相反,以下代码在请求转发之后会尝试写入并刷新 PrintWriter 的缓冲区。
public class FlushServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
...
// forwards the request, implicitly committing the stream
getServletConfig().getServletContext().getRequestDispatcher("/jsp/boom.jsp").forward(req, res);
...
// IllegalStateException; cannot redirect after forwarding
res.sendRedirect("http://www.acme.com/jsp/boomboom.jsp");
PrintWriter out = res.getWriter();
// writing to an already-committed stream will not cause an exception,
// but will not apply these changes to the final output, either
out.print("Writing here does nothing");
// IllegalStateException; cannot flush a response's buffer after forwarding the request
out.flush();
out.close();
}
}
⑥ Not Static Final Logger
指没有用static final对日志记录器进行修饰。
在某个特定类中的所有实例中,共享一个单一的记录器对象,并在程序的执行过程中使用同一个记录器,这是一个很好的编程习惯。
示例7 以下指令错误地声明了一个非固定日志记录器。
private final Logger logger =
Logger.getLogger(MyClass.class);
⑦ Use of System Output Stream
指使用 System.out 或 System.err 而不是专门的日志记录工具,会导致难以监控程序的运行状况。
示例8 开发人员学习编写的第一个 Java 程序通常都是如下所示的样子:
public class MyClass
public static void main(String[] args) {
System.out.println("hello world");
}
}
多数程序员深入了解 Java 的许多精妙之处后,有一部分人仍会依赖于这一基础知识,始终使用 System.out.println() 编写进行标准输出的消息。
这里的问题是,直接在标准输出流或标准错误流中写入信息通常会作为一种非结构化日志记录形式使用。结构化日志记录系统提供了各种要素,如日志级别、统一的格式、日志标示符、次数统计,而且,可能最重要的是,将日志信息指向正确位置的功能。当系统输出流的使用与正确使用日志记录功能的代码混合在一起时,得出的结果往往是一个保存良好但缺少重要信息的日志。
开发者普遍认为需要使用结构化日志记录,但是很多人在“产前”的软件开发中仍使用系统输出流功能。如果您正在检查的代码是在开发阶段的初期生成的,那么对 System.out 或 System.err 的使用可能会在转向结构化日志记录系统的过程中导致漏洞。
⑧ Throw Inside Finally
指使用 finally 块中的 throw 语句会通过 try-catch-finally 破坏逻辑进度。
在 Java 中,finally 块通常在其相应的 try-catch 块之后执行,该块通常用于自由分配的资源,例如文件句柄或数据库指针。由于破坏了正常的程序执行,在 finally 块中抛出异常会绕过关键的清除代码。
示例9 在下列代码中,抛出 FileNotFoundException 时,将绕过对 stmt.close() 的调用。
public void processTransaction(Connection conn) throws FileNotFoundException
{
FileInputStream fis = null;
Statement stmt = null;
try{
stmt = conn.createStatement();
fis = new FileInputStream("badFile.txt");
...
}catch (FileNotFoundException fe){
log("File not found.");
}
catch (SQLException se){
//handle error
}
finally{
if (fis == null)
{
throw new FileNotFoundException();
}
if (stmt != null)
{
try{
stmt.close();
}catch (SQLException e){
log(e);
}
}
}
}
修复建议:
遵循良好的编码规范,永远值得开发人员关注,不但可以提高维护的效率,而且也将降低风险引入的可能。
以“Use of System Output Stream”为例,使用 Java 日志记录工具而不是 System.out 或 System.err。
示例10 代码“示例8”可以使用 log4j 重新写成以下形式:
import org.apache.log4j.Logger;
import org.apache.log4j.BasicConfigurator;
public class MyClass {
private final static Logger logger =
Logger.getLogger(MyClass.class);
public static void main(String[] args) {
BasicConfigurator.configure();
logger.info("hello world");
}
}
空指针引用
摘要
该程序可能会间接引用一个 null 指针,从而引起 null 指针异常。
缺陷描述
Null Pointer空指针的引用,对于空指针的错误引用往往是由于在引用之前没有对空指针做判断,就直接使用空指针,还有可能把空指针作为一个对象来使用,间接使用对象中的属性或是方法,而引起程序崩溃。
当违反程序员的一个或多个假设时,通常会出现 null 指针异常。如果程序明确将对象设置为 null,但稍后却间接引用该对象,则将出现 dereference-after-store 错误。此错误通常是因为程序员在声明变量时将变量初始化为 null。
大部分空指针问题只会引起一般的软件可靠性问题,但如果攻击者能够故意触发空指针间接引用,攻击者就有可能利用引发的异常绕过安全逻辑,或致使应用程序泄漏调试信息,这些信息对于规划随后的攻击十分有用。
示例1 在下列代码中,程序员将变量 foo 明确设置为 null。稍后,程序员间接引用 foo,而未检查对象是否为 null 值。
...
Foo foo = null;
...
foo.setBar(val);
...
当违反程序员的一个或多个假设时,通常会出现 null 指针异常。如果程序未检查该对象是否为 null 便间接引用该对象(该对象可能为null),则会出现 check-after-dereference 错误。
大部分空指针问题只会引起一般的软件可靠性问题,但如果攻击者能够故意触发空指针间接引用,攻击者就有可能利用引发的异常绕过安全逻辑,或致使应用程序泄漏调试信息,这些信息对于规划随后的攻击十分有用。
示例2 在下列代码中,程序员假设变量 foo 不是 null,并通过间接引用该对象来确认此假设。但是,稍后检查 foo 是否为 null 时,程序员就会发现事实与该假设相反。如果在 if 指令中检查时发现 foo 可能是 null,则在间接引用时也将为 null,并会引起 null 指针异常。间接引用不安全,或者不必进行后续检查。
...
foo.setBar(val)
...
if (foo != null) {
...
}
...
修复建议:
在间接引用可能为 null 值的对象之前,请务必仔细检查。如有可能,在处理资源的代码周围的包装器中纳入 null 检查,确保在所有情况下均会执行 null 检查,并最大限度地减少出错的地方。
注意事项:
空指针(NullPointer)引用导致的错误,依靠代码审计工具很难发现其中的错误,因为空指针的引用一般不会发生在出现空指针然后直接使用空指针情况。往往是由于代码逻辑比较复杂空指针引用的位置会比较远,不容易发现;并且在正常情况下不会触发,只有在某一个特定输入条件下才会引发空指针引用。对于排查此类错误也就更加困难。
因此,本规则会存在一定量误报的可能。
框架及场景参考
(一)Null Pointer Reference: Without Check Input
检测外部输入数据没有进行判空,就对其成员方法进行访问导致控制指针异常。
零除错误
摘要
产品使用0去除一个值。
缺陷描述
通常在向该产品提供一个意外值,或错误发生时没有正确检测到的时候就会出现该漏洞。它经常出现在涉及尺寸、长度、宽度和高度的物理尺寸计算中。
示例1 示例中包含有一个计算平均值的函数,但该函数不对分母不为0时所使用的输入值进行 验证。这将生成一个异常试图用0来除。如果没有使用Java异常处理方法来处理此错误,那么就可能会出现意外的结果。
public int computeAverageResponseTime (int totalTime, int numRequests) {
return totalTime / numRequests;
}
修复建议:
在除法计算前,对用作分母的输入值进行验证,确保被零除的错误不会导致意外的结果。
示例2 修改“示例1”代码通过对用作分母的输入值进行验证,并对可能被零除的错误抛出异常。
public int computeAverageResponseTime (int totalTime, int numRequests)
throws ArithmeticException {
if (numRequests == 0) {
System.out.println("Division by zero attempted!");
throw ArithmeticException;
}
return totalTime / numRequests;
}
命名混淆
摘要
类成员与其封装类同名。
缺陷描述
Java 允许类成员与封装类使用同一个名称,但是这通常只会造成混乱和错误的代码。
示例1 以下类中存在一些错误。请尝试一下如何调试这些错误。
public class Name {
private Name Name;
public Name getName() {
return Name.Name;
}
}
示例2 以下代码摘自 WebGoat,向我们展示了一个由 confusing naming 问题产生的 bug [1]。
public class CreateDB
{
public void CreateDB() { }
...
}
作者试图为 CreateDB 类创建一个构造函数,但却在不经意间定义了返回值的类型(void),这就相当于他创建了一个常规方法而已。
修复建议:
严格遵守Java编码规范编写代码。
魔鬼数字
摘要
在代码中使用魔鬼数字(没有具体含义的数字、字符串等)将会导致代码难以理解。
缺陷描述
将数字定义为常量的最终目的是为了使代码更容易理解,所以并不是只要将数字定义为常量就不是魔鬼数字了。如果常量的名称没有意义,无法帮助理解代码,同样是一种魔鬼数字。
示例1 魔鬼数字代码示例。
public void addProduct(ProductServiceStruct product)
{
// 魔鬼数字,无法理解3具体代表产品的什么状态
if(product.getProduct().getProductStatus() != 3)
{
thrownewPMSException(PMSErrorCode.Product.ADD_ERROR);
}
BooleanResult result =checkTariff(product.getTariffs());
if(!result.getResult())
{
thrownew PMSException(PMSErrorCode.Product.ADD_ERROR);
}
}
修复建议:
应该将数字定义为名称有意义的常量。
示例2 “示例1”代码修改如下
/**
*产品未激活状态
*/
private static final int UNACTIVATED = 0;
/**
*产品已激活状态
*/
private static final int ACTIVATED = 1;
public voidaddProduct2(ProductServiceStruct product)
{
if(product.getProduct().getProductStatus() != ACTIVATED)
{
thrownewPMSException(PMSErrorCode.Product.ADD_ERROR);
}
BooleanResult result =checkTariff(product.getTariffs());
if(!result.getResult())
{
thrownewPMSException(PMSErrorCode.Product.ADD_ERROR);
}
}
抛出过于泛化异常的声明
摘要
该方法抛出了一个过于笼统异常,从而使调用者很难处理和修复发生的错误。
缺陷描述
声明一种可以抛出 Exception 或 Throwable 异常的方法,从而使调用者很难处理和修复发生的错误。Java 异常机制的设置是:调用者可以方便地预计有可能发生的各种错误,并为每种异常情况编写处理代码。同时声明:一个方法抛出一个过于笼统的异常违反该系统。
示例1 以下方法抛出了三种类型的异常。
public void doExchange()throws IOException,
InvocationTargetException, SQLException {
...
}
示例2这样写看上去会显得更加紧凑。
public void doExchange()throws Exception {
...
}
这样做会防碍调用者理解和处理所发生的异常。此外,如果 doExchange() 因为变更了代码,而引入了一个需要不同于之前异常处理方式的新型异常,则不能使用简单的方式来处理该要求。
修复建议:
不要声明抛出 Exception 或 Throwable 异常的方法。如果方法抛出的异常无法恢复,或者通常不能被调用者捕获,那么可以考虑抛出未检查的异常,而不是已检查的异常。这可以通过实现一个继承自 RuntimeException 或Error 的类来代替 Exception,或者还可以在方法中加入 try/catch 块将已检查的异常转换为未检查异常。
对象劫持
摘要
一个类有clonealbe()方法,但没有申明成final,导致允许一个对象被创建而没有调用构造函数。这可以导致处在一个不可预期的状态。
缺陷描述
示例1 在这个例子中,一个公有类BackAccount通过申明Object clone(string accountnumber)实现了cloneable()方法。
...
public class BankAccount implements Cloneable{
public Object clone(String accountnumber) throws CloneNotSupportedException {
Object returnMe = new BankAccount(account number);
...
}
}
...
修复建议:
定义类中的cloneable方法为final。
SQL注入
摘要
以用户或者外部不可信来源的输入动态构造SQL查询的命令,将可能改变SQL查询语句本来的语义,从而导致执行任意的SQL命令。
缺陷描述
SQL injection 错误在以下情况下发生:
1. 数据从一个不可信赖的数据源进入程序。
- 在这种情况下, 静态扫描工具无法确定数据源是否可信赖。
2. 数据用于动态地构造一个 SQL 查询。
示例1 以下代码动态地构造并执行了一个 SQL 查询,该查询可以搜索与指定名称相匹配的项。该查询仅会显示条目所有者与被授予权限的当前用户一致的条目。
...
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName");
String query = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ itemName + "'";
ResultSet rs = stmt.execute(query);
...
这一代码所执行的查询遵循如下方式:
SELECT * FROM items
WHERE owner = <userName>
AND itemname = <itemName>;
但是,由于这个查询是动态构造的,由一个不变的基查询字符串和一个用户输入字符串连接而成,因此只有在 itemName 不包含单引号字符时,才会正确执行这一查询。如果一个用户名为 wiley 的攻击者为 itemName 输入字符串“name' OR 'a'='a”,那么查询就会变成:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name' OR 'a'='a';
附加条件 OR 'a'='a' 会使 where 从句永远评估为 true,因此该查询在逻辑上将等同于一个更为简化的查询:
SELECT * FROM items;
这种查询的简化会使攻击者绕过查询只返回经过验证的用户所拥有的条目的要求;而现在的查询则会直接返回所有储存在 items 表中的条目,不论它们的所有者是谁。
示例2 这个例子指出了将不同的恶意数值传递给在“示例1”中构造和执行的查询时所带来的各种影响。如果一个用户名为 wiley 在 itemName中输入字符串“name'; DELETE FROM items; --”,则该查询将会变为以下两个:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name';
DELETE FROM items;
众多数据库服务器,其中包括 Microsoft(R) SQL Server 2000,都可以一次性执行多条用分号分隔的 SQL 指令。对于那些不允许运行用分号分隔的批量指令的数据库服务器,比如 Oracle 和其他数据库服务器,攻击者输入的这个字符串只会导致错误;但是在那些支持这种操作的数据库服务器上,攻击者可能会通过执行多条指令而在数据库上执行任意命令。
注意成对的连字符 (--);这在大多数数据库服务器上都表示下面的语句将作为注释使用,而不能加以执行 [4]。在这种情况下,注释字符的作用就是删除修改的查询指令中遗留的最后一个单引号。对于那些不允许按照这种方式使用注释的数据库,通常攻击者会按“示例1” 的方式进行攻击。如果攻击者输入字符串“name'); DELETE FROM items; SELECT * FROM items WHERE 'a'='a”,则会创建以下三个有效指令:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name'
DELETE FROM items;
SELECT * FROM items WHERE 'a'='a';
修复建议:
造成 SQL injection 攻击的根本原因在于攻击者可以改变 SQL 查询的上下文,使程序员原本要作为数据解析的数值,被篡改为命令了。当构造一个 SQL 查询时,程序员应当清楚,哪些输入的数据将会成为命令的一部分,而哪些仅仅是作为数据。参数化 SQL 指令可以防止直接窜改上下文,避免几乎所有的 SQL injection 攻击。参数化 SQL 指令是用常规的 SQL 字符串构造的,但是当需要加入用户输入的数据时,它们就需要使用捆绑参数,这些捆绑参数是一些占位符,用来存放随后插入的数据。换言之,捆绑参数可以使程序员清楚地分辨数据库中的数据,即其中有哪些输入可以看作命令的一部分,哪些输入可以看作数据。这样,当程序准备执行某个指令时,它可以详细地告知数据库,每一个捆绑参数所使用的运行时的值,而不会被解析成对该命令的修改。
可以将“示例1” 改写成使用参数化 SQL 指令(替代用户输入连续的字符串),如下所示:
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName");
String query = "SELECT * FROM items WHERE itemname=? AND owner=?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, itemName);
stmt.setString(2, userName);
ResultSet results = stmt.execute();
...
更加复杂的情况常常出现在报表生成代码中,因为这时需要通过用户输入来改变 SQL 指令的命令结构,比如在 WHERE 条件子句中加入动态的约束条件。不要因为这一需求,就无条件地接受连续的用户输入,从而创建查询语句字符串。当必须要根据用户输入来改变命令结构时,可以使用间接的方法来防止 SQL injection 攻击:创建一个合法的字符串集合,使其对应于可能要加入到 SQL 指令中的不同元素。在构造一个指令时,可使用来自用户的输入,以便从应用程序控制的值集合中进行选择。
注意事项:
- 使用参数化 SQL 指令的一个常见错误是使用由用户控制的字符串来构造 SQL 指令。这显然背离了使用参数化 SQL 指令的初衷。如果不能确定用来构造参数化指令的字符串是否由应用程序控制,请不要因为它们不会直接作为 SQL 指令执行,就假定它们是安全的。务必彻底地检查 SQL 指令中使用的所有由用户控制的字符串,确保它们不会修改查询的含意。
- 防范 SQL injection 攻击的另外一种常用方式是使用存储过程。虽然存储过程可以阻止某些类型的 SQL injection 攻击,但是对于绝大多数攻击仍无能为力。存储过程有助于避免 SQL injection 的常用方式是限制可作为参数传入的指令类型。但是,有许多方法都可以绕过这一限制,许多危险的表达式仍可以传入存储过程。所以再次强调,存储过程在某些情况下可以避免这种攻击,但是并不能完全保护您的应用系统抵御 SQL injection 的攻击。
框架及场景参考
(一)SQL Injection: Client Side Injection
有些人认为在移动世界中,典型的 Web 应用程序漏洞(如 SQL injection)是无意义的 -- 为什么用户要攻击自己?但是,谨记移动平台的本质是从各种来源下载并在相同设备上运行的应用程序。恶意软件在银行应用程序附近运行的可能性很高,它们会强制扩展移动应用程序的攻击面(包括跨进程通信)。
示例3 以下代码对示“示例1” 进行调整,使其适用于 Android 平台。
...
PasswordAuthentication pa = authenticator.getPasswordAuthentication();
String userName = pa.getUserName();
String itemName = this.getIntent().getExtras().getString("itemName");
String query = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ itemName + "'";
SQLiteDatabase db = this.openOrCreateDatabase("DB", MODE_PRIVATE, null);
Cursor c = db.rawQuery(query, null);
...
一样将对Android平台产生SQL注入影响,所不同的是,输入来源稍有变化。在这种情况下,我们同样需要采取措施防止SQL注入的产生。如下:
...
PasswordAuthentication pa = authenticator.getPasswordAuthentication();
String userName = pa.getUserName();
String itemName = this.getIntent().getExtras().getString("itemName");
String query = "SELECT * FROM items WHERE itemname=? AND owner=?";
SQLiteDatabase db = this.openOrCreateDatabase("DB", MODE_PRIVATE, null);
Cursor c = db.rawQuery(query, new Object[]{itemName, userName});
...
(二)SQL Injection: Hibernate
Hibernate是一种ORMapping 框架,内部可以使用原生SQL还有HQL语言进行SQL操作。
所谓的HQL注入,就是指在Hibernate中没有对数据进行有效的验证导致恶意数据进入应用程序中造成的。
请看这段代码:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException{
response.setContentType(“text/html”);
Configuration configuration = new Configuration();
configuration.configure();
SessionFactory sessionFactory = configuration.buildSessionFactory();
String input = request.getParameter(“sqlin”);
String hqlString = “from classes where name = ‘” + input + “’”;
System.out.println(hqlString);
Session session = sessionFactory.openSession();
List<Classes> classesLise = session.createQuery(hqlString).list();
for(Classes classes : classesList){
response.getWriter().write(classes.getName());
response.getWriter().print(“<br/>”);
}
session.close();
}
Input参数即可造成注入。
不过在Hibernate中,一般都是在createQuery中使用PDO,使用setString填充占位符进行sql语句的拼接,如果是这样的话,自然就不存在SQL注入,但是不排除有人像上面的图片中的写法。
此外,还需注意HQL注入对万能密码、知道表名列名的情况下进行盲注等情况的发生。
(三)SQL Injection: iBatis Data Map
使用 iBatis Data Map 可以指定 SQL 指令中的动态参数,通常使用 # 字符来定义它们,如:
<select id="getItems" parameterClass="MyClass" resultClass="items">
SELECT * FROM items WHERE owner = #userName#
</select>
变量名称周围的 # 字符表示 iBatis 将使用 userName 变量创建参数化查询。但是,iBatis 还允许使用 $ 字符将变量直接连接到 SQL 指令,使其易受 SQL injection 攻击。
示例4 以下代码动态地构造并执行了一个 SQL 查询,该查询可以搜索与指定名称相匹配的项。该查询仅会显示条目所有者与被授予权限的当前用户一致的条目。
<select id="getItems" parameterClass="MyClass" resultClass="items">
SELECT * FROM items WHERE owner = #userName# AND itemname = '$itemName$'
</select>
但是,由于这个查询是动态构造的,由一个不变的基查询字符串和一个用户输入字符串连接而成,因此只有在 itemName 不包含单引号字符时,才会正确执行这一查询。如果一个用户名为 wiley 的攻击者为 itemName 输入字符串“name' OR 'a'='a”,那么查询就会变成:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name' OR 'a'='a';
附加条件 OR 'a'='a' 会使 where 从句永远评估为 true,因此该查询在逻辑上将等同于一个更为简化的查询:
SELECT * FROM items;
这种查询的简化会使攻击者绕过查询只返回经过验证的用户所拥有的条目的要求;而现在的查询则会直接返回所有储存在 items 表中的条目,不论它们的所有者是谁。
(四)SQL Injection: JDO
使用 Java 数据对象 (JDO) 执行通过不可信来源的输入构建的动态 SQL 或 JDOQL 指令,会让攻击者有机会篡改指令的含义或者执行任意 SQL 命令。
示例5 以下代码动态地构造并执行了一个 SQL 查询,该查询可以搜索与指定名称相匹配的项。该查询仅会显示条目所有者与被授予权限的当前用户一致的条目。
...
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName");
String sql = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ itemName + "'";
Query query = pm.newQuery(Query.SQL, sql);
query.setClass(Person.class);
List people = (List)query.execute();
...
这一代码所执行的查询遵循如下方式:
SELECT * FROM items
WHERE owner = <userName>
AND itemname = <itemName>;
但是,由于这个查询是动态构造的,由一个不变的基查询字符串和一个用户输入字符串连接而成,因此只有在 itemName 不包含单引号字符时,才会正确执行这一查询。如果一个用户名为 wiley 的攻击者为 itemName 输入字符串“name' OR 'a'='a”,那么查询就会变成:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name' OR 'a'='a';
附加条件 OR 'a'='a' 会使 where 从句永远评估为 true,因此该查询在逻辑上将等同于一个更为简化的查询:
SELECT * FROM items;
这种查询的简化会使攻击者绕过查询只返回经过验证的用户所拥有的条目的要求;而现在的查询则会直接返回所有储存在 items 表中的条目,不论它们的所有者是谁。
(五)SQL Injection: MyBatis Mapper
使用 MyBatis Mapper XML 文件可以指定 SQL 指令中的动态参数,通常使用 # 字符来定义它们,如:
<select id="getItems" parameterType="domain.company.MyParamClass" resultType="MyResultMap">
SELECT *
FROM items
WHERE owner = #{userName}
</select>
变量名称周围带有括号的 # 字符表示 MyBatis 将使用 userName 变量创建参数化查询。但是,MyBatis 还允许使用 $ 字符将变量直接连接到 SQL 指令,使其易受 SQL injection 攻击。
示例6 以下代码动态地构造并执行了一个 SQL 查询,该查询可以搜索与指定名称相匹配的项。该查询仅会显示条目所有者与被授予权限的当前用户一致的条目。
<select id="getItems" parameterType="domain.company.MyParamClass" resultType="MyResultMap">
SELECT *
FROM items
WHERE owner = #{userName}
AND itemname = ${itemName}
</select>
但是,由于这个查询是动态构造的,由一个不变的基查询字符串和一个用户输入字符串连接而成,因此只有在 itemName 不包含单引号字符时,才会正确执行这一查询。如果一个用户名为 wiley 的攻击者为 itemName 输入字符串“name' OR 'a'='a”,那么查询就会变成:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name' OR 'a'='a';
附加条件 OR 'a'='a' 会使 where 从句永远评估为 true,因此该查询在逻辑上将等同于一个更为简化的查询:
SELECT * FROM items;
这种查询的简化会使攻击者绕过查询只返回经过验证的用户所拥有的条目的要求;而现在的查询则会直接返回所有储存在 items 表中的条目,不论它们的所有者是谁。
(六)SQL Injection: Persistence
使用 Java J2EE Persistence API 来执行通过不可信来源的输入构建的动态 SQL 语句会使得攻击者能够篡改语句的含义或者执行任意 SQL 命令。
示例7 以下代码动态地构造并执行了一个 SQL 查询,该查询可以搜索与指定名称相匹配的项。该查询仅会显示条目所有者与被授予权限的当前用户一致的条目。
...
String userName = ctx.getAuthenticatedUserName();
String itemName = request.getParameter("itemName");
String query = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ itemName + "'";
List items = entManager.createQuery(query).getResultList();
...
这一代码所执行的查询遵循如下方式:
SELECT * FROM items
WHERE owner = <userName>
AND itemname = <itemName>;
但是,由于这个查询是动态构造的,由一个不变的基查询字符串和一个用户输入字符串连接而成,因此只有在 itemName 不包含单引号字符时,才会正确执行这一查询。如果一个用户名为 wiley 的攻击者为 itemName 输入字符串“name' OR 'a'='a”,那么查询就会变成:
SELECT * FROM items
WHERE owner = 'wiley'
AND itemname = 'name' OR 'a'='a';
附加条件 OR 'a'='a' 会使 where 从句永远评估为 true,因此该查询在逻辑上将等同于一个更为简化的查询:
SELECT * FROM items;
这种查询的简化会使攻击者绕过查询只返回经过验证的用户所拥有的条目的要求;而现在的查询则会直接返回所有储存在 items 表中的条目,不论它们的所有者是谁。
跨站脚本攻击
摘要
向Web 浏览器发送未经验证的数据,将导致web浏览器执行恶意的代码,进而导致客户端的用户的敏感信息被偷盗,或者其它危及客户端用户的安全。
缺陷描述
Cross-Site Scripting (XSS)攻击根据输入来源,将可分成如下三种情况:
反射型:当Web应用程序没有事先消毒就将HTTP request反射回用户时,会出现反射型跨站脚本漏洞。这往往发生在恶意代码被包含在GET或POST参数的情况下。攻击者为了能够利用反射型XSS漏洞,必须引诱被攻击者从他自己的浏览器发出request,例如,通过点击电子邮件中的一个恶意链接。
持久型:当Web应用程序存储用户生成的数据,后来又将该数据显示给应用程序的其他用户时,就会出现持久型跨站脚本漏洞。很多Web应用程序都有这种漏洞,如维基、网上论坛和社交网站。如果在客户端浏览器被显示之前,这个数据没有进行过正确消毒,那么该应用程序的所有用户都可能成为受害者。持久型跨站脚本漏洞要比反射型的更危险,因为攻击者不必引诱Web应用程序的其他用户进行任何可疑操作。
以DOM为基础的XSS:以DOM为基础的跨站脚本漏洞通常会影响使用JavaScript或VBScript来对用户输入执行客户端处理的应用程序。如今许多应用程序都依靠包含有能够动态生成HTML内容的客户端脚本的网页。这些网页根据某些用户输入来修改其HTML,其间与服务器之间没有任何交互作用。如果攻击者通过在不向服务器提交任何数据的情况下通过这样一个网页注入恶意脚本,那么就会出现以DOM为基础的跨站脚本。与其他类型的跨站脚本不同的是,在这种情况下,是客户端脚本,而不是服务器,要负责对用户输入进行正确消毒。虽然这种漏洞目前不太常见,但随着越来越多的应用程序将其处理逻辑推到客户端浏览器,从而减少HTTP流量,这些类型的跨站脚本漏洞也就将越来越频繁地出现。
反射型跨站(Reflected_XSS_All_Clients)、存储跨站(Stored_XSS)和基于DOM跨站在以下情况下发生:
1. 数据通过一个不可信赖的数据源进入 Web 应用程序。对于 Reflected XSS,不可信赖的源通常为 Web 请求,而对于 Persisted(也称为 Stored)XSS,该源通常为数据库或其他后端数据存储。
2. 未检验包含在动态内容中的数据,便将其传送给了 Web 用户。对于基于 DOM 的 XSS,任何时候当受害人的浏览器解析 HTML 页面时,恶意内容都将作为 DOM(文档对象模型)创建的一部分执行。
传送到 Web 浏览器的恶意内容通常采用 JavaScript 代码片段的形式,但也可能会包含一些 HTML、Flash 或者其他任意一种可以被浏览器执行的代码。基于 XSS 的攻击手段花样百出,几乎是无穷无尽的,但通常它们都会包含传输给攻击者的私人数据(如 Cookie 或者其他会话信息)。在攻击者的控制下,指引受害者进入恶意的网络内容;或者利用易受攻击的站点,对用户的机器进行其他恶意操作。
示例1 以下 JSP 代码片段可从 HTTP 请求中读取雇员 ID eid,并将其显示给用户。
<% String eid = request.getParameter("eid"); %>
...
Employee ID: <%= eid %>
如果 text 只包含标准的字母或数字文本,这个例子中的代码就能正确运行。如果 eid 里有包含元字符或源代码中的值,那么 Web 浏览器就会像显示 HTTP 响应那样执行代码。
起初,这个例子似乎是不会轻易遭受攻击的。毕竟,有谁会输入导致恶意代码的 URL,并且还在自己的电脑上运行呢?真正的危险在于攻击者会创建恶意的 URL,然后采用电子邮件或者社会工程的欺骗手段诱使受害者访问此 URL 的链接。当受害者单击这个链接时,他们不知不觉地通过易受攻击的网络应用程序,将恶意内容带到了自己的电脑中。这种对易受攻击的 Web 应用程序进行盗取的机制通常被称为反射式 XSS。
示例2 以下 JSP 代码片段可在数据库中查询具有给定 ID 的雇员,并输出相应雇员姓名。
<%...
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from emp where id="+eid);
if (rs != null) {
rs.next();
String name = rs.getString("name");
}
%>
Employee Name: <%= name %>
如同“示例1”,如果对 name 的值处理得当,该代码就能正常地执行各种功能;如若处理不当,就会对代码的盗取行为无能为力。同样,这段代码暴露出的危险较小,因为 name 的值是从数据库中读取的,而且显然这些内容是由应用程序管理的。然而,如果 name 的值是由用户提供的数据产生,数据库就会成为恶意内容沟通的通道。如果不对数据库中存储的所有数据进行恰当的输入验证,那么攻击者就可以在用户的 Web 浏览器中执行恶意命令。这种类型的 Persistent XSS(也称为 Stored XSS)盗取极其阴险狡猾,因为数据存储导致的间接性使得辨别威胁的难度增大,而且还提高了一个攻击影响多个用户的可能性。XSS 盗取会从访问提供留言簿 (guestbook) 的网站开始。攻击者会在这些留言簿的条目中嵌入 JavaScript,接下来所有访问该留言簿的用户都会执行这些恶意代码。
有些人认为在移动世界中,典型的 Web 应用程序漏洞(如 cross-site scripting)是无意义的 -- 为什么用户要攻击自己?但是,谨记移动平台的本质是从各种来源下载并在相同设备上运行的应用程序。恶意软件在银行应用程序附近运行的可能性很高,它们会强制扩展移动应用程序的攻击面(包括跨进程通信)。
示例3 以下代码在 Android WebView 中启用了 JavaScript(默认情况下,JavaScript 为禁用状态),并根据从 Android Intent 接收到的值加载页面。
...
WebView webview = (WebView) findViewById(R.id.webview); webview.getSettings().setJavaScriptEnabled(true);
String url = this.getIntent().getExtras().getString("url");
webview.loadUrl(url);
...
如果 url 的值以 javascript: 开头,则接下来的 JavaScript 代码将在 WebView 中的 Web 页面上下文内部执行。
正如例子中所显示的,XSS 漏洞是由于 HTTP 响应中包含了未经验证的数据代码而引起的。受害者遭受 XSS 攻击的途径有三种:
- 如“示例1” 所述,系统从 HTTP 请求中直接读取数据,并在 HTTP 响应中返回数据。当攻击者诱使用户为易受攻击的 Web 应用程序提供危险内容,而这些危险内容随后会反馈给用户并在 Web 浏览器中执行,就会发生反射式 XSS 盗取。发送恶意内容最常用的方法是,把恶意内容作为一个参数包含在公开发表的 URL 中,或者通过电子邮件直接发送给受害者。以这种手段构造的 URL 构成了多种“网络钓鱼”(phishing) 阴谋的核心,攻击者借此诱骗受害者访问指向易受攻击站点的 URL。站点将攻击者的内容反馈给受害者以后,便会执行这些内容,接下来会把用户计算机中的各种私密信息(比如包含会话信息的 cookie)传送给攻击者,或者执行其他恶意活动。
- 如“示例2” 所述,应用程序将危险数据储存在一个数据库或其他可信赖的数据存储器中。这些危险数据随后会被回写到应用程序中,并包含在动态内容中。Persistent XSS 盗取发生在如下情况:攻击者将危险内容注入到数据存储器中,且该存储器之后会被读取并包含在动态内容中。从攻击者的角度看,注入恶意内容的最佳位置莫过于一个面向许多用户,尤其是相关用户显示的区域。相关用户通常在应用程序中具备较高的特权,或相互之间交换敏感数据,这些数据对攻击者来说有利用价值。如果某一个用户执行了恶意内容,攻击者就有可能以该用户的名义执行某些需要特权的操作,或者获得该用户个人所有的敏感数据的访问权限。
— 正如“示例3” 所示,应用程序之外的数据源将危险数据储存在一个数据库或其他数据存储器中,随后这些危险数据被当作可信赖的数据回写到应用程序中,并存储在动态内容中。
许多现代 Web 框架都提供对用户输入执行验证的机制。其中包括 Struts 和 Spring MVC。为了突出显示未经验证的输入源,该规则包会降低(静态代码分析器)报告的问题被利用的可能性,并在使用框架验证机制时提供相应的依据,以动态重新调整问题优先级。我们将这种功能称之为上下文敏感排序。为了进一步帮助用户执行审计过程,端码科技软件安全研究团队提供了数据验证项目模板,该模板会根据应用于输入源的验证机制,将问题分组到多个文件夹中
修复建议:
在对未经验证的数据传送给web页面处理前,对数据进行清洁处理,可以使用输入验证的白名单或者黑名单的方法去验证,也可以使用一些成熟的输出encoding的方法把一些web浏览器易解析成命令的特殊字符的SCAII码转换成HTML的编码。比如:
Character HTML value
< <
> >
& &
" "
public String encodeHtml(String input)
{
StringBuffer out = new StringBuffer();
for (int i = 0; i < input.length(); i++)
{
char c = input.charAt(i);
if (c == '<')
{
out.append("<");
}
else if (c == '>')
{
out.append(">");
}
else if (c == '\"')
{
out.append(""");
}
else if (c == '\'')
{
out.append(""");
}
else if (c == '&')
{
out.append("&");
}
else if (c > 0x20 && c < 0x126)
{
out.append(c);
}
else
{
out.append("&#" + (int)c + ";");
}
}
return out.toString();
}
同时也可以借用一些已有的函数再输出之前对字符串作处理 。比如: HtmlEncoder.encode ,ServletResponse.encode 或者自定义验证处理。使用成熟的清洁库来删除不可信数据中的恶意代码
参考XSS修复的库:
OWASP ESAPI Encoding Library
OWASP ESAPI项目已经创建了Encoding Library,来降低XSS漏洞带来的风险。执行以下步骤,来使用ESAPI Encoding Library
- 下载并添加对OWASP ESAPI jar文件的引用。
- 导入org.owasp.esapi.ESAPI。
- 使用适当的编码环境Encoding嵌入到Web响应中的不可信数据。
OWASP Java Encoding Library
OWASP Java Encoding Library项目提供了高性能库,其拥有更多Encoding方法来帮助开发人员缓解XSS漏洞带来的风险。
执行以下步骤,来使用Java编码器项目:
- 下载并添加对Java编码器项目jar文件的引用。
- 导入org.owasp.encoder.Encode。
- 编码嵌入到Web响应中的不可信数据。
OWASP AntiSamy项目
OWASP AntiSamy项目是一种结合策略的Java HTML清洁程序。任何策略不允许的HTML元素都会自动被过滤。
执行以下步骤,来使用AntiSamy项目:
- 下载并添加对AntiSamy jar文件的引用。
- 导入来自于org.owasp.validator.html.*的引用。
- 创建AntiSamy XML策略文件。
- 使用AntiSamy扫描器与策略文件清洁不可信数据。
OWASP Java HTML sanitizer项目
OWASP Java HTML sanitizer项目为Java1.5及更新版本提供更快的净化库,并且比AntiSamy更易配置。
执行以下任务,来使用Java HTML净化项目:
- 下载并添加对Java HTML sanitizer项目与相关性引用。
- 导入org.owasp.html.*。
- 创建策略对象。
- 净化不可信数据。
注意事项:
常见XSS漏洞修复误区:
• 尝试使用黑名单来检测跨站脚本攻击。
• 创建您自己的encoding库。
• 依赖单层防御。
• 选择错误的encoding方法或上下文环境。
序列化类包含敏感数据
摘要
代码包含一个有隐私数据的类,但是类没有显式的拒绝序列化。这些类可能被其它的类序列化。数据可以通过其它类序列化来访问。
缺陷描述
可序列化的类实际上是公开的类,因为里面的数据不能隐藏。类没有显示的拒绝序列化可以被另外的类序列化,从而使用存储在里面的数据。
攻击者可以把类输出成字节流,然后从里面提取重要的数据。
示例1
class Teacher {
private String name;
private String clas;
public Teacher(String name,Stringclas) {
//...
//Check the database for the name and address
this.SetName() = name;
this.Setclas() = clas;
}
}
修复建议:
在java里,显示地定义final writeObject()来防止序列化。这是推荐的解决方案。定义writeObject()方法去抛出一个异常,显式地拒绝序列化。
确保防止对你的对象进行序列化。
拒绝服务
摘要
攻击者可以造成程序崩溃或使合法用户无法进行使用。
缺陷描述
Denial of Service 攻击试图让目标用户无法访问计算机或网络资源。如果应用程序存在 DoS 漏洞,攻击者就能阻止合法用户访问由该应用程序提供的服务。攻击者可能通过对应用程序发送大量请求,而使它拒绝对合法用户的服务,但是这种攻击形式经常会在网络层就被排除掉了。更加严重的是那些只需要使用少量请求就可以使得攻击者让应用程序过载的 bug。这种 bug 允许攻击者去指定请求使用系统资源的数量,或者是持续使用这些系统资源的时间。
示例1 以下代码允许用户指定线程进入休眠状态的时间。通过指定一个较大的时间,攻击者便可以无限期地阻碍线程。因此,只需少量的请求,攻击者就能耗尽应用程序的线程池。
int usrSleepTime = Integer.parseInt(usrInput);
Thread.sleep(usrSleepTime);
示例2 以下代码从一个 zip 文件中读取字符串。因为它使用 readLine() 方法,所以可以读取一批极大量的输入。攻击者能够利用该代码引发一个 OutOfMemoryException 异常,或者消耗大量的内存,从而致使程序需要更多的时间去执行垃圾信息的收集,或在随后的操作过程中用完内存资源。
InputStream zipInput = zipFile.getInputStream(zipEntry);
Reader zipReader = new InputStreamReader(zipInput);
BufferedReader br = new BufferedReader(zipReader);
String line = br.readLine();
修复建议:
校验用户输入以确保它不会引起不适当的资源利用。
示例3 以下代码允许用户指定线程进入休眠的时间,如“示例1” 所述,但仅当数值在合理的范围之内时才会有效。
int usrSleepTime = Integer.parseInt(usrInput);
if (usrSleepTime >= SLEEP_MIN &&
usrSleepTime <= SLEEP_MAX) {
Thread.sleep(usrSleepTime);
} else {
throw new Exception("Invalid sleep duration");
}
示例4 以下代码从一个 zip 文件中读取字符串,如“示例2” 所述,但它读取的最大字符串长度为 MAX_STR_LEN 字符。
InputStream zipInput = zipFile.getInputStream(zipEntry);
Reader zipReader = new InputStreamReader(zipInput);
BufferedReader br = new BufferedReader(zipReader);
StringBuffer sb = new StringBuffer();
int intC;
while ((intC = br.read()) != -1) {
char c = (char) intC;
if (c == '\n') {
break;
}
if (sb.length() >= MAX_STR_LEN) {
throw new Exception("input too long");
}
sb.append(c);
}
String line = sb.toString();
注意事项:
- 即使消耗的系统资源总量或持续使用这些系统资源的时间未被黑客控制或至少未被直接控制,仍有可能发生 Denial of service。相反,程序员可能选择不安全的常量指定这些参数。端玛科技安全编码规则包会将此类情况作为潜在 Denial of Service 漏洞报告。
框架及场景参考
(一)Denial of Service: Format String
如果允许用户输入控制格式参数,则攻击者能够借此造成异常抛出或信息泄露。
攻击者可以修改格式字符串参数,以造成异常抛出。如果未能捕获此异常,则可能导致应用程序崩溃。或者,如果其他参数中使用了敏感信息,攻击者可能会更改格式字符串以泄露此信息。
示例5 用户可通过以下代码指定 Formatter.format() 的格式字符串参数。
...
Formatter formatter = new Formatter(Locale.US);
String format = "The customer: %s %s has the balance %4$." + userInput + "f";
formatter.format(format, firstName, lastName, accountNo, balance);
...
此程序的最初设计旨在让用户指定所显示余额的小数点。但在现实情况中,对此并没有限制。如果用户可以指定所有内容,则可能会导致抛出如 java.util.MissingFormatArgumentException 等异常。此外,由于它不在 try 块中,因此可能会造成应用程序故障。
针对此示例,更严重的情况是:如果攻击者可以指定用户输入 "2f %3$s %4$.2",则格式字符串可能会是 "The customer: %s %s has the balance %4$.2f %3$s %4$.2"。这可能会导致敏感的 accountNo 包含在生成的字符串中。
修复建议:
不管什么时候,只要可以,请将静态的 format string 传递到那些接受 format string 参数的函数。如果必须动态构造 format string,则可定义一组有效的 format string,并从这组安全的 format string 进行选择。最后,请始终要进行如下校验:在所选 format string 中的格式化指示的数量符合即将进行格式化的参数的数量。
(二)Denial of Service: Parse Double
程序会调用解析 double 类型的方法,这会导致线程被挂起。
在实施 java.lang.Double.parseDouble() 及相关方法时出现漏洞,会导致在解析 [2^(-1022) - 2^(-1075) : 2^(-1022) - 2^(-1076)]范围内的任意数字时挂起线程。此缺陷可被攻击者用于执行拒绝服务 (DoS) 攻击。
示例6 即使程序未直接使用 double 类型,也容易受到攻击。
protected void processLocale(HttpServletRequest request, HttpServletResponse response)
{
Locale locale = request.getLocale();
if (locale != null)
{
session.setAttribute(Globals.LOCALE_KEY, locale);
}
}
在 Apache Tomcat 中实现 HttpServletRequest 时使用 parseDouble 验证“接受语言”标题的区域设置,会导致对 getLocale() 的任何调用都具有危险性。
该漏洞在 Java 版本 6 Update 23 及更早版本中存在。Java 版本 6 Update 24 或更高版本不存在该漏洞。
修复建议:
如果可能,请应用由 Oracle 发布的修补程序。尽可能为类似 Apache Tomcat 的易受攻击的其他产品安装修补程序。如果不可能,请务必将您的 DMCA(端玛源代码安全静态扫描工具)安装配置为防范此攻击。
如果您的程序中具有易受攻击的方法调用,可以使用 Double 来避免遭受这种攻击。但这种方法无法保证能够消除此问题。对于类似 BigInteger 的其他数字类,可使用 parseDouble() 方法实现这些类。
注意事项:
- 无法通过更改源代码消除该漏洞,但如果您使用的是 Java 版本 6 Update 24 或更高版本,就可以安全修复该漏洞。
(三)Denial of Service: Regular Expression
不可信赖数据被传递至应用程序并作为正则表达式使用。这会导致线程过度使用 CPU 资源。
实施正则表达式评估程序及相关方法时存在漏洞,该漏洞会导致评估线程在处理嵌套和重复的正则表达式组的重复和交替重叠时挂起。此缺陷可被攻击者用于执行拒绝服务 (DoS) 攻击。
示例7
(e+)+
([a-zA-Z]+)*
已知的正则表达式实现方法均无法避免这种攻击。所有平台和语言都容易受到这种攻击。
嵌套超过4层
摘要
代码块嵌套层数过多,将导致程序可读性变差。
缺陷描述
本规则检测代码嵌套层次太多(>=4)。
示例1
public static void main(String[] args) {
...
for(i=0;i<size;i++){
if(condition1){
if(condition2){
if(condition3){//erro
...
}
}
}
}
}
修复建议:
拆解代码,减少代码的嵌套层数。
日志伪造
摘要
将未经验证的用户输入写入日志文件可致使攻击者伪造日志条目或将恶意信息内容注入日志。
缺陷描述
在以下情况下会发生 Log Forging 的漏洞:
1. 数据从一个不可信赖的数据源进入应用程序。
2. 数据写入到应用程序或系统日志文件中。
为了便于以后的审阅、统计数据收集或调试,应用程序通常使用日志文件来储存事件或事务的历史记录。根据应用程序自身的特性,审阅日志文件可在必要时手动执行,也可以自动执行,即利用工具自动挑选日志中的重要事件或带有某种倾向性的信息。
如果攻击者可以向随后会被逐字记录到日志文件的应用程序提供数据,则可能会妨碍或误导日志文件的解读。最理想的情况是,攻击者可能通过向应用程序提供包括适当字符的输入,在日志文件中插入错误的条目。如果日志文件是自动处理的,那么攻击者就可以通过破坏文件格式或注入意外的字符,从而使文件无法使用。更阴险的攻击可能会导致日志文件中的统计信息发生偏差。通过伪造或其他方式,受到破坏的日志文件可用于掩护攻击者的跟踪轨迹,甚至还可以牵连第三方来执行恶意行为 [1]。最糟糕的情况是,攻击者可能向日志文件注入代码或者其他命令,利用日志处理实用程序中的漏洞 [2]。
示例1 下列 Web 应用程序代码会尝试从一个请求对象中读取整数值。如果数值未被解析为整数,输入就会被记录到日志中,附带一条提示相关情况的错误消息。
...
String val = request.getParameter("val");
try {
int value = Integer.parseInt(val);
}catch (NumberFormatException nfe) {
log.info("Failed to parse val = " + val);
}
...
如果用户为“val”提交字符串“twenty-one”,则日志中会记录以下条目:
INFO: Failed to parse val=twenty-one
然而,如果攻击者提交字符串“twenty-one%0a%0aINFO:+User+logged+out%3dbadguy”,则日志中会记录以下条目:
INFO: Failed to parse val=twenty-one
INFO: User logged out=badguy
显然,攻击者可以使用同样的机制插入任意日志条目。
有些人认为在移动世界中,典型的 Web 应用程序漏洞(如 Log Forging)是无意义的 -- 为什么用户要攻击自己?但是,谨记移动平台的本质是从各种来源下载并在相同设备上运行的应用程序。恶意软件在银行应用程序附近运行的可能性很高,它们会强制扩展移动应用程序的攻击面(包括跨进程通信)。
示例2 以下代码将“示例1” 改编为适用于 Android 平台。
...
String val = this.getIntent().getExtras().getString("val");
try {
int value = Integer.parseInt();
}catch (NumberFormatException nfe) {
Log.e(TAG, "Failed to parse val = " + val);
}
...
修复建议:
使用间接方法防止 Log Forging 攻击:创建一组与不同事件一一对应的合法日志条目,这些条目必须记录在日志中,并且仅记录该组条目。要捕获动态内容(如用户注销系统),请务必使用由服务器控制的数值,而非由用户提供的数据。这就确保了日志条目中绝不会直接使用由用户提供的输入。
可以按以下方式将“示例1” 重写为与 NumberFormatException 对应的预定义日志条目:
...
public static final String NFE = "Failed to parse val. The input is required to be an integer value."
...
String val = request.getParameter("val");
try {
int value = Integer.parseInt(val);
}
catch (NumberFormatException nfe) {
log.info(NFE);
}
...
下面是 Android 的等同内容:
...
public static final String NFE = "Failed to parse val. The input is required to be an integer value."
...
String val = this.getIntent().getExtras().getString("val");
try {
int value = Integer.parseInt();
}catch (NumberFormatException nfe) {
Log.e(TAG, NFE);
}
...
在某些情况下,这个方法有些不切实际,因为这样一组合法的日志条目实在太大或是太复杂了。这种情况下,开发者往往又会退而采用黑名单方法。在输入之前,黑名单会有选择地拒绝或避免潜在的危险字符。然而,不安全字符列表很快就会不完善或过时。更好的方法是创建一份白名单,允许其中的字符出现在日志条目中,并且只接受完全由这些经认可的字符组成的输入。在大多数 Log Forging 攻击中,最关键的字符是“\n”(换行符),该字符决不能出现在日志条目白名单中。
注意事项:
- 许多日志功能只是为了在开发和测试过程中调试程序而创建的。根据我们的经验,当生产的某一阶段,会随机或出于某一目的进行调试。不要仅仅因为程序员说“我没有计划在生产中启动调试功能”,就容忍 Log Forging 漏洞。
冗余初始化
摘要
基于性能考虑,不建议对变量进行重复初始化。
缺陷描述
默认情况下,调用类的构造函数时,Java会把变量初始化成确定的值,所有的对象被设置成null,整数变量设置成0,float和double变量设置成0.0,逻辑值设置成false。当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键字创建一个对象时,构造函数链中的所有构造函数都会被自动调用。
修复建议:
给成员变量合理设置初始值,并避免重复进行初始化。
使用浮点数据精确计算
摘要
检测使用浮点型数据进行精确计算的问题。
缺陷描述
float和double只能用来做科学计算或者是工程计算,在商业计算等精确计算中,我们要用java.math.BigDecimal。
示例1
float a = 16777216.0f;
float b = 1.0f;
float c = a + b; // 错误; yields 1.6777216E7 not 1.6777217E7
double d = a + b; // 错误; addition is still between 2 floats
修复建议:
使用java.math.BigDecimal进行精确计算。
示例2
float a = 16777216.0f;
float b = 1.0f;
BigDecimal c = BigDecimal.valueOf(a).add(BigDecimal.valueOf(b));
double d = (double)a + (double)b
使用过时或有风险的加密算法
摘要
识别调用会使用无法保证敏感数据的保密性的弱加密算法。
缺陷描述
陈旧的加密算法(如 DES)再也不能为敏感数据提供足够的保护了。加密算法依赖于密钥大小,这是确保加密强度的主要方法之一。加密强度通常通过生成有效密钥所需的时间和计算能力来衡量。计算能力的提高使得能够在合理的时间内获得较小的加密密钥。例如,在二十世纪七十年代首次开发出该算法时,在 DES 中使用的 56 位密钥造成了巨大的计算障碍,但今天,使用常用设备能在不到一天的时间内破解 DES。
修复建议:
选用足够安全的加密算法,弃用已知不够安全的算法,勿自作聪明自己发明自以为很安全的加密算法。
使用伪随机数
摘要
不安全的随机数Insecure Randomness,标准的伪随机数值生成器不能抵挡各种加密攻击。
缺陷描述
在对安全性要求较高的环境中,使用一个能产生可预测数值的函数作为随机数据源,会产生 Insecure Randomness 错误。
电脑是一种具有确定性的机器,因此不可能产生真正的随机性。伪随机数生成器 (PRNG) 近似于随机算法,始于一个能计算后续数值的种子。
PRNG 包括两种类型:统计学的 PRNG 和密码学的 PRNG。统计学的 PRNG 可提供有用的统计资料,但其输出结果很容易预测,因此数据流容易复制。若安全性取决于生成数值的不可预测性,则此类型不适用。密码学的 PRNG 通过可产生较难预测的输出结果来应对这一问题。为了使加密数值更为安全,必须使攻击者根本无法、或极不可能将它与真实的随机数加以区分。通常情况下,如果并未声明 PRNG 算法带有加密保护,那么它有可能就是一个统计学的 PRNG,不应在对安全性要求较高的环境中使用,其中随着它的使用可能会导致严重的漏洞(如易于猜测的密码、可预测的加密密钥、会话劫持攻击和 DNS 欺骗)。
示例1 下面的代码可利用统计学的 PRNG 为购买产品后仍在有效期内的收据创建一个 URL。
String GenerateReceiptURL(String baseUrl) {
Random ranGen = new Random();
ranGen.setSeed((new Date()).getTime());
return (baseUrl + ranGen.nextInt(400000000) + ".html");
}
这段代码使用 Random.nextInt() 函数为它所生成的收据页面生成独特的标识符。因为 Random.nextInt() 是一个统计学的 PRNG,攻击者很容易猜到由它所生成的字符串。尽管收据系统的底层设计也存在错误,但如果使用了一个不生成可预测收据标识符的随机数生成器(如密码学的 PRNG),会更安全一些。
修复建议:
当不可预测性至关重要时,如大多数对安全性要求较高的环境都采用随机性,这时可以使用密码学的 PRNG。不管选择了哪一种 PRNG,都要始终使用带有充足熵的数值作为该算法的种子。(诸如当前时间之类的数值只提供很小的熵,因此不应该使用。)
Java 语言在 java.security.SecureRandom 中提供了一个加密 PRNG。就像 java.security 中其他以算法为基础的类那样,SecureRandom 提供了与某个特定算法集合相关的包,该包可以独立实现。当使用 SecureRandom.getInstance() 请求一个 SecureRandom 实例时,您可以申请实现某个特定的算法。如果算法可行,那么您可以将它作为 SecureRandom 的对象使用。如果算法不可行,或者您没有为算法明确特定的实现方法,那么会由系统为您选择 SecureRandom 的实现方法。
Sun 在名为 SHA1PRNG 的 Java 版本中提供了一种单独实现 SecureRandom 的方式,Sun 将其描述为计算:
“SHA-1 可以计算一个真实的随机种子参数的散列值,同时,该种子参数带有一个 64 比特的计算器,会在每一次操作后加 1。在 160 比特的 SHA-1 输出中,只能使用 64 比特的输出 [1]。”
然而,文档中有关 Sun 的 SHA1PRNG 算法实现细节的相关记录很少,人们无法了解算法实现中使用的熵的来源,因此也并不清楚输出中到底存在多少真实的随机数值。尽管有关 Sun 的实现方法网络上有各种各样的猜测,但是有一点无庸置疑,即算法具有很强的加密性,可以在对安全性极为敏感的各种内容中安全地使用。
使用未初始化变量
摘要
代码使用的变量尚未初始化,导致不可预知或意想不到的结果。
缺陷描述
在某些语言中,如C语言,未初始化的变量中包含有以前使用过的内存内容。有时攻击者可以控制或阅读这些内容。
初始的变量通常包含一些诸如垃圾等不好的东西,使其一致性遭到怀疑。这可能导致拒绝服务条件,或以意想不到的方式修改控制流量。在某些情况下,攻击者就可以使用以前的行为对变量进行“预先初始化”,从而使代码被执行。如果锁定变量检查在本不该通过的时候通过了,那么就这可能会导致竞争状态。
没有初始化的字符串是特别危险的,因为很多函数期望在字符串的结尾-仅在结尾-有空字符null。
示例1 下面的C语言示例中,Switch语句本想设置aN和bN的值,但在default 分支当中,程序员把aN的值设置了两次,结果bN的值将没有被指定。
switch (ctl) {
case -1:
aN = 0;
bN = 0;
break;
case 0:
aN = i;
bN = -i;
break;
case 1:
aN = i + NEXT_SZ;
bN = i - NEXT_SZ;
break;
default:
aN = -1;
aN = -1;
break;
}
repaint(aN, bN);
大多数未初始化变量问题都是导致软件可靠性问题,但是如果攻击者故意触发未初始化的变量,他将可能通过使程序崩溃来发布拒绝服务攻击。在正常的情况下,攻击者可能在调用函数前通过影响在堆栈上的值来控制未初始化变量的值。
示例2 下面Java演示示例中使用的int型变量foo未经初始化就用于条件判断。
int foo;
void bar() {
if (foo==0)
/.../
/../
}
修复建议:
• 为所有的变量都指定一个初始化的值。
• 如果编译警告选项打开,大多数编译器都会报道那些未初始化的变量。
• 尽可能使用一种语言它不会被这种问题所危及。
• 安全的字符串库和容器提取方法等消除技术可以被引入。
注意事项:
变量在初始化之前,通常包含有变量占用的那些遗留在内存里的垃圾数据。这些数据中很少有有用的,所以一般都建议将变量预先初始化或尽早就将其设置为初始值。如果C语言情况下忘记初始化,例如一个char * ,那么许多简单的字符串库就会因为期望在字符串库的末端获得零终止而往往可能返回不正确的结果。
C和C++中的堆栈变量的初始化不是默认的。其初始值由函数调用时候在堆栈中其位置所发生的任何情况决定。程序永远都不应该使用未经初始化的变量值。很少有程序员会使用在代码中或其它少见及异常情况中处理错误的未初始化的变量,未初始化变量所发出的警告有时可以表明代码中存在印刷或者排字的错误。
输入不规范
摘要
软件在进行规范化之前验证其输入数据,以防止软件在规范化步骤之后检测到数据无效。
缺陷描述
这可用于攻击者绕过验证,对本该阻止的暴露的弱点进行攻击,比如注入攻击等。
示例1 下面的代码试图验证一个给定的输入路径,根据白名单对其进行检测,然后返回合规的路径。在这种特定的情况下,如果它的路径以字符串“/safe_dir/”打头,将被视为合法路径。
String path = getInputPath();
if (path.startsWith("/safe_dir/"))
{
File f = new File(path);
return f.getCanonicalPath();
}
上述代码的问题是验证步骤在规范化进行之前发生。攻击者通过提供一个输入路径形如“/safe_dir/../”,可以绕过验证的步骤。然而,规范化过程将“..”视为上溯到父目录,因此路径规范化将仅仅变成“/”。
修复建议:
输入进行验证之前应当被解码和规范化为应用程序的当前的内部表示,确保您的应用程序不会对相同的输入解码两次,这类错误可能用来绕过白名单方案,比如在检测过后,引入危险的输入。
示例2 为了避免这个问题,验证应在规范化的发生。在这种情况下,规范化发生的文件对象的初始化过程中。下面的代码对“示例1”的问题进行了修正。
String path = getInputPath();
File f = new File(path);
if (f.getCanonicalPath().startsWith("/safe_dir/"))
{
return f.getCanonicalPath();
}
数值类型转换不正确
摘要
当将一种数据类型转换到另一种类型时,如将长数据转换为整数时,数据被省略或翻译的方式可能会生成意外的值。如果所得到的值被用于敏感的上下文中,那么就可能会发生危险行为。
缺陷描述
示例1 在下面的Java示例中,浮点数值被转换为整数,从而造成了精度损失。
int i = (int) 33457.8f;
修复建议:
避免数值类型之间的转换。请务必检查允许的范围。
死代码
摘要
永远不会被执行的类对象/方法/代码块。
缺陷描述
空 try 块是 dead code 或者表示存在调试代码。
空 try 块没有任何用处。事实上,当编译成字节代码时,优化操作会去除空 try 块,并且不会将其包含在完成的程序中。空 try 块可能表示被删除或注释掉的代码。
示例1 下列代码包含空 try 块。
try {
//rs = stmt.executeQuery(query);
}
catch(SQLException e) {
log(e);
}
Dead code 对 code quality 有负面影响,会使代码更难于读取、理解和维护。
修复建议:
去除程序中的所有dead code。
未闭合对象
摘要
软件没有在使用后完全的清除临时文件或者辅助的资源。
缺陷描述
示例1 Java应用程序中的流资源应该在finally块中释放,否则在在调用close()方法之前的一个异常将会导致未释放的I/O资源。在下面这个例子中,close()方法在try块中调用(不正确)。
...
try {
InputStream is = new FileInputStream(path);
byte b[] = new byte[is.available()];
is.read(b);
is.close();
} catch (Throwable t) {
log.error("Something bad happened: " + t.getMessage());
}
...
修复建议:
临时文件和辅助资源应该在它们不再需要时被删除和释放。
注意事项:
临时文件应该被尽快的删除。如果一个文件包含敏感信息,日志文件的存在可以让攻击者有更好的机会去访问它的内容。而且也有可能去溢出临时文件的个数从而创造一个拒绝服务的问题,因为通常文件夹有允许文件个数的限制。
未检测的错误条件
摘要
Poor Error Handling: Empty Catch Block - 忽略异常会导致程序无法发现意外状况和情况。
缺陷描述
几乎每一个对软件系统的严重攻击都是从违反程序员的假设开始的。攻击后,程序员的假设看起来既脆弱又拙劣,但攻击前,许多程序员会在午休时间为自己的种种假设做很好的辩护。
在代码中,很容易发现两个令人怀疑的假设:“一是这个方法调用不可能出错;二是即使出错了,也不会对系统造成什么重要影响。”因此当程序员忽略异常时,这其实就表明了他们是基于上述假设进行的操作。
示例1下面摘录的代码会忽略一个由 doExchange() 抛出的罕见异常。
try {
doExchange();
}
catch (RareException e) {
// this can never happen
}
如果抛出 RareException 异常,程序会继续执行,就像什么都没有发生一样。程序不会记录任何有关这一特殊情况的依据,因而事后再查找这一异常就可能很困难。
修复建议:
至少,应该记录抛出异常的事实,以便于稍后查询及预知对程序运行所造成的影响。然而最好是中止当前操作。如果忽略某个异常的原因是因为调用者无法正确处理该异常,而程序上下文使调用者不便或不可能声明程序会抛出这一异常,那么可以考虑抛出 RuntimeException 或 Error 异常,两者均是未检查的异常。在 JDK 1.4 中,RuntimeException 有一个构造函数,可以方便地用来封装另其他异常。
示例2 “示例1”中的代码应该用以下方式重写。
try {
doExchange();
}
catch (RareException e) {
throw RuntimeException("This can never happen", e);
}
注意事项:
1.只有极少的异常类型可以在特定的上下文中丢弃。例如,Thread.sleep() 会抛出 InterruptedException 异常,并且在大多数情况下,程序应该以相同的方式决定线程是否唤醒过早。
try {
Thread.sleep(1000);
}
catch (InterruptedException e){
// The thread has been woken up prematurely, but its
// behavior should be the same either way.
}
未检测的返回值
摘要
忽略方法的返回值会导致程序无法发现意外状况和情况。
缺陷描述
Java 程序员常常会误解包含在许多 java.io 类中的 read() 及相关方法。在 Java 结果中,将大部分错误和异常事件都作为异常抛出。(这是 Java 相对于 C 语言等编程语言的优势:各种异常更加便于程序员考虑是哪里出现了问题。)但是,如果只有少量的数据可用,stream 和 reader 类并不认为这是异常的情况。这些类只是将这些少量的数据添加到返回值缓冲区,并且将返回值设置为读取的字节或字符数。所以,并不能保证返回的数据量一定等于请求的数据量。
这样,程序员就需要检查 read() 和其他 IO 方法的返回值,以确保接收到期望的数据量。
示例1 下列代码会在一组用户中进行循环,读取每个用户的私人数据文件。程序员假设这些文件总是正好 1000 字节,从而忽略了检查read() 的返回值。如果攻击者能够创建一个较小的文件,程序就会重复利用前一个用户的剩余数据,并对这些数据进行处理,就像这些数据属于攻击者一样。
FileInputStream fis;
byte[] byteArray = new byte[1024];
for (Iterator i=users.iterator(); i.hasNext();) {
String userName = (String) i.next();
String pFileName = PFILE_ROOT + "/" + userName;
FileInputStream fis = new FileInputStream(pFileName);
fis.read(byteArray); // the file is always 1k bytes
fis.close();
processPFile(userName, byteArray);
}
修复建议:
示例2 上面“示例1”的代码可以修复为:
FileInputStream fis;
byte[] byteArray = new byte[1024];
for (Iterator i=users.iterator(); i.hasNext();) {
String userName = (String) i.next();
String pFileName = PFILE_ROOT + "/" + userName;
fis = new FileInputStream(pFileName);
int bRead = 0;
while (bRead < 1024) {
int rd = fis.read(byteArray, bRead, 1024 - bRead);
if (rd == -1) {
throw new IOException("file is unusually small");
}
bRead += rd;
}
// could add check to see if file is too large here
fis.close();
processPFile(userName, byteArray);
}
注:因为该问题的修复相当地复杂,您可能试图使用一个更简单的方法,例如在开始阅读前检查文件的大小。这种方法将导致应用程序容易受到文件系统 race condition 的攻击,凭借这个攻击者可以在文件大小检查和从文件调用读取数据之间使用恶意文件替换结构良好的文件。
未检测循环条件输入
摘要
攻击者可以输入一个非常高的值,可能导致拒绝服务(DOS)。
缺陷描述
应用程序在循环中执行一些重复任务,并根据用户输入定义执行循环的次数。一个非常高的值可能会导致应用程序卡在循环中,无法继续其他操作。
示例1 如果绝对必要,允许用户定义循环的参数,确保对输入进行验证和约束。
try
{
iLoopCount= Integer.parseInt(sLoopCountInput);
}
catch (NumberFormatException ex)
{
iLoopCount= MAX_LOOPS;
}
if ((iLoopCount > MAX_LOOPS)
...
修复建议:
理想情况下,不要基于用户提供的数据建立循环。如果有必要这样做,用户输入必须首先验证,其范围应该是有限的。
未使用的变量
摘要
变量值已被分配,但从未被使用,从而使其成为一个死存储。
缺陷描述
该变量可能不完整,也有可能是因为该未被使用过的变量指出了一个bug。
示例1 下列代码片段分配给变量r,但在重写值的时候却没有使用该变量。
ExampleLanguage:C
r=getName();
r=getNewBuffer(buf);
修复建议:
删除代码中未被使用的变量。
注意事项:
该变量值未被使用。分配后,或者是向该变量分配了其它值,或者是该变量超出了范围。
循环体中使用Try..Catch语句
摘要
在循环中抛出异常,可导致错误的结果。
缺陷描述
循环中使用try_catch block,执行中抛出异常会继续循环,如果操作的目的是计算结果的话,继续执行完循环得到的结果也是错误的,没有必要,且易导致性能问题。
除非操作是,一个服务器线程维持一直处理其它线程产生的数据,为了保证系统的稳定性,而必须将try_catch block写在循环内。
示例1
public void test()
{
while(true)
{
try
{
Thread.sleep(30*60*1000);
}
catch (InterruptedException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
修复建议:
除非业务场景需要,避免在循环体内构造try_catch块,因为每次进入、退出try_catch块都会消耗一定的CPU资源,将try_catch块放在循环体之外可以节省大量的执行时间。
示例2
public void test()
{
try
{
while(true)
{
Thread.sleep(30*60*1000);
}
}
catch (InterruptedException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
循环体中使用字符串拼接
摘要
应用程序在循环中直接使用“+”进行字符串拼接操作,将导致内存开销增加、时间消耗上升而造成性能下降。
缺陷描述
实现String字符串相加的方法有很多,常见的有直接相加”+”,StringBuilder.append(),StringBuffer.append()和String.format(),这三者的运行效率是有差异的,String是final类型的,每次相加都会new一个新的String对象,如果这种操作很多的话,很占用很大的内存。而StringBuilder.append()方法是在原对象上进行操作,如果长度不够就自行扩展。
在字符串拼接操作比较简单时,直接使用”+”操作符对性能影响很有限,可以忽略不计。
示例1 若在循环中直接使用”+”操作符,性能将下降得很厉害。
...
for(...){
String s = str1 + str2 + “abc”;
}
...
修复建议:
使用StringBuilder、StringBuffer进行字符串拼接操作,特别是在字符串拼接十分频繁的情形下。
示例2 上面“示例1”改用StringBuilder进行字符串拼接操作。
...
for(...){
String s = new StringBuilder().append(str1).append(str2).append( “abc”);
}
...
遗留调试代码
摘要
调试代码可以在部署的 web 应用程序中建立一些意想不到的入口点。
缺陷描述
开发过程中一般会为了调试和测试目的增加一些“后门”代码,这些代码不会随应用程序一起提供或部署。如果这类调试代码无意中被保留在应用程序中,则会导致应用程序向计划外的交互模式开放。这些后门入口点很容易产生安全隐患,因为它们不在当初的设计或者测试的考虑之内,并且不会出现在应用程序设计中的操作环境里。
遗忘调试代码中最常见例子出现在 web 应用程序中的 main() 方法。尽管这在产品的开发过程中是完全可以接受的,但是属于 J2EE 应用程序中的那部分类不应该定义 main()。
修复建议:
务必在部署应用程序的产品版之前删除调试代码。无论是否存在直接的安全威胁,一旦早期开发阶段结束,就没有任何理由将这样的代码保留在应用程序中。
注意事项:
出现方法 main() 可能预示着一个十分严重安全问题。在查找调用 main() 的操作时,请检查是否存在其他迹象,表明开发者编程时过于仓促,或者出于其他情况没能正常地结束工作
如果您审计一个非 J2EE Java 应用程序,J2EE Bad Practices 分类可能不适用于您的环境。在这种情况下,您可以使用 Audit工具来消除这些问题。
异常处理不当
摘要
catch 块可以处理的异常种类很多,但往往会由于过多的考虑被应该在此位置处理的各种问题或故障而困扰不已。
缺陷描述
多个 catch 块看上去既难看又繁琐,但使用一个“简约”的 catch 块捕获高级别的异常类(如 Exception),可能会混淆那些需要特殊处理的异常,或是捕获了不应在程序中这一点捕获的异常。本质上,捕获范围过大的异常与“Java 分类定义异常”这一目的是相违背的。随着程序的增加而抛出新异常时,这种做法会十分危险。而新发生的异常类型也不会被注意到。
示例1 以下代码使用了同一方式来处理三种不同的异常类型。
try {
doExchange();
}
catch (IOException e) {
logger.error("doExchange failed", e);
}
catch (InvocationTargetException e) {
logger.error("doExchange failed", e);
}
catch (SQLException e) {
logger.error("doExchange failed", e);
}
其实,与其这样,还不如使用一个单独的 catch 块来处理这三种异常,如下所示:
try {
doExchange();
}
catch (Exception e) {
logger.error("doExchange failed", e);
}
但是如果修改 doExchange(),以抛出需要以某种不同的方式处理的新异常类型,则范围过大的 catch 块会阻止编译器指出这一情况(有新的异常抛出)。此外,新 catch 块也将处理那些来自于 RuntimeException 的异常,比如 ClassCastException 和 NullPointerException,而这些异常的发生是不在程序员的计划之内的。
修复建议:
不要声明抛出 Exception 或 Throwable 异常的方法。如果方法抛出的异常无法恢复,或者通常不能被调用者捕获,那么可以考虑抛出未检查的异常,而不是已检查的异常。这可以通过实现一个继承自 RuntimeException 或Error 的类来代替 Exception,或者还可以在方法中加入 try/catch 块将已检查的异常转换为未检查异常。首先进行特定的Exception进行捕捉, 最后再用Exception异常类来处理其它可能出现的异常。
好的代码实践:
try{
doSomething();
}
catch(NullPointer Exception npe){
...
}
catch(FileNotFoundException fne){
...
}
catch(SocketException se){
...
}
catch(Exception e){
...
}
直接使用名称进行类比较
摘要
在没有实现 equals() 的对象上调用了 equals() 方法。
缺陷描述
当比较对象时,开发人员通常希望比较对象的属性。然而,在没有明确实现 equals() 的类(或任何超类/接口)上调用 equals() 会导致调用继承自 java.lang.Object 的 equals() 方法。Object.equals() 将比较两个对象实例,查看它们是否相同,而不是比较对象成员字段或其他属性。尽管可以合法地使用 Object.equals(),但这通常表示存在错误代码。
示例1
...
public class AccountGroup {
private int gid;
public int getGid() {
return gid;
}
public void setGid(int newGid) {
gid = newGid;
}
}
...
public class CompareGroup {
public boolean compareGroups(AccountGroup group1, AccountGroup group2) {
return group1.equals(group2);
//equals() is not implemented in AccountGroup
}
}
...
修复建议:
验证 Object.equals() 的使用确实是您要调用的方法。如果不是,那么可实现 equals() 方法,或者使用其他方法来比较对象。
示例2 以下代码将 equals() 方法添加到“示例1”部分中的示例。
public class AccountGroup
{
private int gid;
public int getGid()
{
return gid;
}
public void setGid(int newGid)
{
gid = newGid;
}
public boolean equals(Object o)
{
if (!(o instanceof AccountGroup))
return false;
AccountGroup other = (AccountGroup) o;
return (gid == other.getGid());
}
}
...
public class CompareGroup
{
public static boolean compareGroups(AccountGroup group1, AccountGroup group2)
{
return group1.equals(group2);
}
}
未经许可公开服务或资源
摘要
可以在Android设备上运行的任何其他应用程序直接访问导出的服务、活动或内容提供程序。如果特定的访问限制不明确,任何已安装的应用程序可以绕过安全沙盒和直接启动或绑定到该服务,执行未经授权的操作或访问允许用户数据。
缺陷描述
Android服务或活动既可以使用android:exported="true"
属性显式地导出,也可以通过定义服务的意图过滤器隐式地导出。android:permission
属性必须为每个要导出的服务或活动显式地声明,否则服务/活动在全局范围内无障碍。
示例1 下面Android代码示例演示了没有所需权限进行服务导出。
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service android:name="com.example.project.MyService">
<intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL" ></action>
</intent-filter>
</service>
</application>
</manifest>
示例2 用Permission属性导出服务。
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service android:name="com.example.project.MyService"
android:permission="android.permission.PROCESS_OUTGOING_CALLS">
<intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL" ></action>
</intent-filter>
</service>
<application>
</manifest>
修复建议:
• 不要导出服务、活动或内容提供程序,除非您的应用程序是专门打算这样做的。
• 不要为服务或活动定义意图过滤器,除非你允许它通过隐含的意图被调用。
• 如果您正在导出服务、活动等,请确保您设置了客户端必须具有的特定权限,以便启动服务/活动,进行绑定,否则让其对其意图进行响应。
框架及场景参考
(一)Android: Debuggable App
在AndroidManifest.xml中可定义android:debuggable属性,如果该属性设置为true,则表示应用程序允许被其他应用进行动态调试;app被恶意程序动态调试时,可能导致代码执行被跟踪;密码等用户隐私数据信息泄漏等问题。
示例3
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
</application>
规范要求:
发布版的程序应关闭Debuggable属性,即将AndroidManifest.xml中application的debuggable属性设置为false。
示例4 为防止程序被动态调试,应把AndroidManifest.xml中debuggable的值设置为false。
<application
android: debuggable="false"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
</application>
(二)Android: Exposure Of Resource To Other Applications
android:exported 是Android中的四大组件 Activity,Service,Provider,Receiver四大组件中都会有的一个属性,如果android:exported设置了true表示可对外进行访问或使用,如果android:exported设置了false表示不可对外进行访问或使用。
如果对内部组件进行设置android:exported值为true,则可能出现组件被外部APP进行恶意访问、非法操作等风险。
业务需求中需要对组件进行对外开放,则根据该开放性是针对于所有还是针对于个别,如果针对于所有则可设置android:exported为true即可,如果是针对于个别则通过自定义权限来进行访问安全控制。
Android、Service、Receiver的android:exported默认值会依据当前组件是否设置了intent-filter来定,如果组件设置了intent-filter时android:exported默认值为true,反之为false。
示例5
<Activity
android:name=".MyActivity">
<intent-filter>
<action android:name="com.test.targetactivity" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</Activity>
Provider的android:exported默认值会依据当前设置的android:targetSdkVersion值来定,程序中使用android:targetSdkVersion值高于(等于)17时android:exported默认值为false,反之android:exported默认值为true。
示例6
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="16" />
<provider
android:name=".testProvider"
android:authorities="com.ijiami.testProvider"
android:multiprocess="true"
android:permission="com.ijiami.permission.writeAndReadPermission">
</provider>
规范要求:
如果项目中对Android组件(Activity,Service,Provider,Receiver)没有可对外访问的业务需求,四大组件的android:exported值应设置为false。
如果项目中对Android组件(Activity,Service,Provider,Receiver)需要对外访问的业务的需求,四大组件的android:exported值应设置为true,并根据业务需求选择使用自定义权限进行安全控制。
示例7 程序内部使用使用了intent-filter的Activity组件时应将其android:exported设置为false(启动项Activity必须设置为true)。
<Activity
android:name=".MyActivity"
android:exported="false" >
<intent-filter>
<action android:name="com.test.targetactivity" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</Activity>
示例8 程序内部在android:targetSdkVersion="17"设置值低于17时,使用Content Provider组件时应将其android:exported设置为false。
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="16" />
<provider
android:name=".testProvider"
android:authorities="com.ijiami.testProvider"
android:multiprocess="true"
android:permission="com.ijiami.permission.writeAndReadPermission"
android:exported="false">
</provider>
(三)Java: Exposure of Resource to Wrong Sphere
当class文件不作严格限制直接将内部变量作为公共域暴露,此变量将不可预期地被修改,从而导致该class的外部调用者可为“原内部变量”设置任意、甚至不被允许的值,由此发生不可预期的行为。
示例9 下面Java代码将”price”声明为公共变量。
public class MyProduct {
// 此处price变量的值可被任意外部程序修改
public float price;
public MyProduct() {
this.price = ReadPriceFromDB("MyProduct");
}
}
示例10 下面Java代码将只读变量暴露到外部。
public class MyProduct {
//此处price变量的值可被程序外部读取,
// 但只能通过构造器进行修改
public final float price;
public MyProduct() {
this.price = ReadPriceFromDB("MyProduct");
}
}
规范要求:
避免将内部变量和特定实例作为公共域直接暴露到程序外部,可将数据作为属性暴露,并进行数据校验,根据需要进行限制,在暴露公共域时,通过使用final修饰符限定其值为只读。
示例11 将数据作为属性进行遮盖。
public class MyProduct {
// 此变量值只能被MyProduct class自身访问
private float price;
// 程序外部只能通过调用该属性的get方法读取其值
public float getPrice() {
return price;
}
public MyProduct() {
this.price = ReadPriceFromDB("MyProduct");
}
}
资源访问授权不当
摘要
查找数据库、文件等资源访问过程受外部影响,但没有进行严格的权限划分(Access Control)。
缺陷描述
当访问控制的一致性得不到保障,甚至压根没有进行访问控制时,用户可对原本没有权限进行操作或访问的资源进行操作和访问,这将导致很多问题,包括信息泄露、拒绝服务、任意代码调用等。
修复建议:
针对目标操作系统与服务器环境运用访问控制机制,并定义相应的访问控制列表。在定义这些访问控制列表时,采取“默认拒绝”的策略。
比较代替赋值
摘要
如果目的是要执行一项任务时,该代码就会使用一个运算符进行比较。
缺陷描述
在许多语言中,比较语句在外观上与赋值语句非常接近;经常被混淆。
示例1 该bug主要源于一个打字错误。
...
void called(int foo) {
foo==1;
if (foo==1) printf("foo\n");
}
int main() {
called(2);
return 0;
}
...
修复建议:
通过构建:许多集成开发环境(IDE)和静态分析产品都能检测到这个问题。
clone方法未调用super.clone()方法
摘要
方法 clone() 应调用 super.clone() 获取新的对象。
缺陷描述
在所有实现 clone() 的方法中,应通过调用 super.clone() 来获取新对象。如果类没有遵守该约定,那么子类的 clone() 方法将会返回一个错误的对象类型。
示例1 以下两个类显示了由于没有调用 super.clone() 而产生的 bug。由于 Kibitzer 实现 clone() 的方法的缘故,FancyKibitzer 的克隆方法将会返回类型为 Kibitzer 而非 FancyKibitzer 的对象。
...
public class Kibitzer implements Cloneable {
public Object clone() throws CloneNotSupportedException {
Object returnMe = new Kibitzer();
...
}
}
public class FancyKibitzer extends Kibitzer implements Cloneable {
public Object clone() throws CloneNotSupportedException {
Object returnMe = super.clone();
...
}
}
...
修复建议:
获取新对象时,在你的clone()方法中调用super.clone()。
在某些情形下,你可以不要clone方法,改用copy构造器。
凭证保护不足
摘要
这个漏洞是发生在当应用程序传输或者存储验证信息,使用易受未经认证的拦截和检索影响的不安全方法时。
缺陷描述
攻击者潜在可以绕过验证机制,劫持一个受害者的帐户,获取各种级别的帐户。
修复建议:
使用正确的安全机制去保护信用凭证。
使用恰当的加密法去保护信用凭证。
使用产业标准去保护信用凭证(例如,LDAP、keystore等)。