使用通过即时调用者的类加载器检查执行任务的 Java API 时应小心谨慎。这些 API 会绕过可确保已向执行链中的所有调用者授予了必需安全权限的 SecurityManager 检查。由于这些 API 可能会削弱系统安全性,因此不应在不可信认的代码上调用它们。
在这种情况下:
1. 可以通过公用函数访问封闭函数。
2. 当前应用程序存在 Process Control 和/或 Unsafe JNI (Java Native Interface) 漏洞。上述两个结果表明当前应用程序从不可信认的来源或不可信认的环境中加载库。不可信认的库可以使用 JNI访问此易受攻击的 API,从而获取对受限制程序包的访问权限并执行任意代码,导致应用程序容易受到远程攻击。
注:只有当应用程序配置有 SecurityManager 且代码包含在具有特定权限的类中时,此结果才适用。有关SecurityManager 和此漏洞的更多详细信息,请参见“Secure Coding Guidelines for the Java Programming Language”中的准则 9。
确保这些敏感的 API 无法通过不可信认的代码进行访问,并且无法在不可信认的代码上进行调用。此外,不得将此方法返回的对象传播回不可信认的代码。有关此漏洞的更多详细信息,请参见“Secure Coding Guidelines for the Java Programming Language”中的准则 9。
AccessibleObject API 允许程序员绕过由 Java 访问说明符提供的 access control 检查。特别是它让程序员能够允许反映对象绕过 Java access control,并反过来更改私有字段或调用私有方法、行为,这些通常情况下都是不允许的。
只能使用攻击者无法设置的参数,通过有权限的类更改访问说明符。所有出现的访问说明符都应仔细检查。
任何应用程序都能访问在显式定义中未明确分配访问权限的公共组件。Android 应用程序中活动、接收者和服务组件的 exported 属性的默认值取决于 intent-filter 是否存在。如果存在 intent-filter,说明该组件可供外部使用。因此应将 exported 属性设为 true。这样该组件就能被 Android 平台上的其他任何应用程序访问。
没有明确的访问权限的组件应该为异常。除非该组件确实需要被所有应用程序访问,否则开发人员应该通过在显式文件中明确定义访问权限来保护组件,以免其被恶意应用程序滥用。
当比较对象时,开发人员通常希望比较对象的属性。然而,在没有明确实现 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() 方法添加到“说明”部分中的示例。
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);
}
}
许多才智卓越的人都试图使用 double-checked locking 方法来提高性能,并为此付出了大量的时间,但是无一成功。
例 1:乍一看,下列代码似乎既能避免不必要的同步又能保证线程的安全性。
if (fitz == null) {
synchronized (this) {
if (fitz == null) {
fitz = new Fitzer();
}
}
}
return fitz;
程序员希望保证仅分配一个 Fitzer() 对象,但又不希望每次调用该代码时都进行一次同步。这就是所谓的double-checked locking 方法。令人遗憾的是,它并不起作用,并且可以分配多个 Fitzer() 对象。有关更多详细信息,请参见 The
"Double-Checked Locking is Broken" Declaration [1]。
其实同步所花费的代价比想象中的要少。许多情况下,最好的方法就是采用最简单的解决方法。
例 2: 例 1 中的代码应该用以下方式重写:
synchronized (this) {
if (fitz == null) {
fitz = new Fitzer();
}
return fitz;
}
在对安全性要求较高的环境中,使用一个能产生可预测数值的函数作为随机数据源,会产生 InsecureRandomness 错误。电脑是一种具有确定性的机器,因此不可能产生真正的随机性。伪随机数生成器 (PRNG) 近似于随机算法,始于一个能计算后续数值的种子。PRNG 包括两种类型:统计学的 PRNG 和密码学的 PRNG。统计学的 PRNG 可提供有用的统计资料,但其输出结果很容易预测,因此数据流容易复制。若安全性取决于生成数值的不可预测性,则此类型不适用。密码学的 PRNG 通过可产生较难预测的输出结果来应对这一问题。为了使加密数值更为安全,必须使攻击者根本无法、或极不可能将它与真实的随机数加以区分。通常情况下,如果并未声明 PRNG 算法带有加密保护,那么它有可能就是一个统计学的 PRNG,不应在对安全性要求较高的环境中使用,其中随着它的使用可能会导致严重的漏洞(如易于猜测的密码、可预测的加密密钥、会话劫持攻击和 DNS 欺骗)。
示例: 下面的代码可利用统计学的 PRNG 为购买产品后仍在有效期内的收据创建一个 URL。
function genReceiptURL (baseURL){
var randNum = Math.random();
var receiptURL = baseURL + randNum + ".html";
return receiptURL;
}
这段代码使用 Math.random() 函数为它所生成的收据页面生成独特的标识符。因为 Math.random() 是一个统计学的 PRNG,攻击者很容易猜到由它所生成的字符串。尽管收据系统的底层设计也存在错误,但如果使用了一个不生成可预测收据标识符的随机数生成器(如密码学的 PRNG),会更安全一些。
当不可预测性至关重要时,如大多数对安全性要求较高的环境都采用随机性,这时可以使用密码学的 PRNG。不管选择了哪一种 PRNG,都要始终使用带有充足熵的数值作为该算法的种子。(诸如当前时间之类的数值只提供很小的熵,因此不应该使用。)
在 JavaScript 中,常规的建议是使用 Mozilla API 中的 window.crypto.random() 函数。但这种方法在多种浏览器中都不起作用,包括 Mozilla Firefox 的最新版本。目前没有适用于功能强大的密码学 PRNG 的跨浏览器解决方案。此时应考虑在 JavaScript 之外处理任意 PRNG 功能。
Android 的备份服务可以使应用程序将永久性数据保存到一个远程云存储上,以便以后为应用程序数据提供一个恢复点。
通过将 allowBackup 属性设置为 true(默认值)并在 标签上定义 backupAgent 属性,可以为 Android应用程序配置该备份服务。但是,由于不同设备的云存储和传输方式不同,Android 并不保证您的数据在使用备份功能时的安全性。
如果您的应用程序包含敏感信息,请慎重使用备份功能存储敏感信息。此外,请谨记,由于 allowBackup属性的默认值为 true,因此有必要明确将此属性设置为 false 以禁用备份。
当违反程序员的一个或多个假设时,通常会出现 null 指针异常。如果程序明确将对象设置为 null,但稍后却间接引用该对象,则将出现 dereference-after-store 错误。此错误通常是因为程序员在声明变量时将变量初始化为 null。大部分空指针问题只会引起一般的软件可靠性问题,但如果攻击者能够故意触发空指针间接引用,攻击者就有可能利用引发的异常绕过安全逻辑,或致使应用程序泄漏调试信息,这些信息对于规划随后的攻击十分有用。
示例:在下列代码中,程序员将变量 foo 明确设置为 null。稍后,程序员间接引用 foo,而未检查对象是
否为 null 值。
Foo foo = null;
...
foo.setBar(val);
...
}
在间接引用可能为 null 值的对象之前,请务必仔细检查。如有可能,在处理资源的代码周围的包装器中纳入null 检查,确保在所有情况下均会执行 null 检查,并最大限度地减少出错的地方。
许多 DNS 服务器都很容易被攻击者欺骗,所以应考虑到某天软件有可能会在有问题的 DNS 服务器环境下运行。如果允许攻击者进行 DNS 更新(有时称为 DNS 缓存中毒),则他们会通过自己的机器路由您的网络流量,或者让他们的 IP 地址看上去就在您的域中。勿将系统安全寄托在 DNS 名称上。
示例:以下代码使用 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。
如果通过域名检查的方式可以确保主机接受和发送的 DNS 记录的一致性,您可以更加信任这一方式。攻击者如若不能控制目标域的域名服务器,就无法同时欺骗接受和发送的 DNS 记录。虽然这种方法并不简单,但是:攻击者也许可以说服域注册者把域移交给一个恶意的域名服务器。依赖于 DNS 记录的 authentication 是有风险的。
虽然没有十分简单的 authentication 机制,但是还有比基于主机的 authentication 更好的方法。密码系统提供了比较不错的安全性,但是这种安全性却易受密码选择不当、不安全的密码传送和 password management失误的影响。类似于 SSL 的方法值得考虑,但是通常这样的方法过于复杂,以至于使用时会有运行出错的风险,而关键资源也随时面临着被窃取的危险。在大多数情况下,包括一个物理标记的多重 authentication 可以在合理的代价范围内提供最大程度的安全保障。
可使用正则表达式对IP进行校验
ip = ipMatch(IpUtil.getIpAddress().getHostAddress());
private static String ipMatch(String ip){
if (Pattern.matches("[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?", ip)) {
return ip;
}
return "0";
}
或者使用其他获取IP的方法
多个 catch 块看上去既难看又繁琐,但使用一个“简约”的 catch 块捕获高级别的异常类(如 Exception),可能会混淆那些需要特殊处理的异常,或是捕获了不应在程序中这一点捕获的异常。本质上,捕获范围过大的异常与“Java 分类定义异常”这一目的是相违背的。随着程序的增加而抛出新异常时,这种做法会十分危险。而新发生的异常类型也不会被注意到。
示例:以下代码使用了同一方式来处理三种不同的异常类型。
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);
}
不要捕获范围过大的异常类,比如 Exception、Throwable、Error 或 ,除非是级别非常高的程序或线程。
声明一种可以抛出 Exception 或 Throwable 异常的方法,从而使调用者很难处理和修复发生的错误。Java 异常机制的设置是:调用者可以方便地预计有可能发生的各种错误,并为每种异常情况编写处理代码。
同时声明:一个方法抛出一个过于笼统的异常违反该系统。
示例:以下方法抛出了三种类型的异常。
public void doExchange() throws IOException, InvocationTargetException,SQLException {
...
}
不要声明抛出 Exception 或 Throwable 异常的方法。如果方法抛出的异常无法恢复,或者通常不能被调用者捕获,那么可以考虑抛出未检查的异常,而不是已检查的异常。这可以通过实现一个继承自RuntimeException 或Error 的类来代替 Exception,或者还可以在方法中加入 try/catch 块将已检查的异常转换为未检查异常。
与上一点(Poor Error Handling: Overly Broad Catch)相类似
finally 块中的返回指令会导致从 try 块中抛出的异常丢失。
例 1:在下列代码中,第二次调用 doMagic 方法,同时将参数 true 传递给该方法会导致抛出MagicException 异常,该异常将不会传递给调用者。finally 块中的返回指令会导致异常的丢弃。
public class MagicTrick {
public static class MagicException extends Exception { }
public static void main(String[] args) {
System.out.println("Watch as this magical code makes an " +"exception disappear before your very eyes!");
System.out.println("First, the kind of exception handling " +"you're used to:");
try {
doMagic(false);
} catch (MagicException e) {
// An exception will be caught here
e.printStackTrace();
}
System.out.println("Now, the magic:");
try {
doMagic(true);
} catch (MagicException e) {
// No exception caught here, the finally block ate it
e.printStackTrace();
}
System.out.println("tada!");
}
public static void doMagic(boolean returnFromFinally)
throws MagicException {
try {
throw new MagicException();
}
finally {
if (returnFromFinally) {
return;
}
}
}
}
将返回指令移到 finally 块之外。如果必须要 finally 块返回一个值,可以简单地将该返回值赋给一个本地变量,然后在 finally 块执行完毕后返回该变量。
通常情况下,您不会希望为外部类提供对象成员字段的直接访问路径,因为任意外部类都可以更改公共字段。面向对象的良好设计使用 Encapsulation 来防止将实现详细信息(例如成员字段)暴露给其他类。此外,如果系统假定此字段不可更改,则恶意代码可能能够逆向更改系统的行为。
示例 1:在以下代码中,字段 ERROR_CODE 已声明为公共、静态和非最终:
在这种情况下,恶意代码可能能够更改此错误代码并导致程序出现意外行为。
public class MyClass{
public static int ERROR_CODE = 100;
//...
}
如果您希望将字段作为常数值公开,则应将该字段声明为 public static final,否则就将该字段声明为 private。
public class MyClass{
public static final int ERROR_CODE = 123;
//...
}
如果需要其他类对其进行访问、修改,应该将该字段定义为private(私有)后通过gettrt、setter方法访问
Process control 漏洞主要表现为以下两种形式:
— 攻击者可以篡改程序加载的库的名称:攻击者直接地控制库所使用的名称。
— 攻击者能够篡改库加载的环境:攻击者间接控制库名称的含义。
这种情况下,我们主要考虑第二种情况,攻击者通过程序加载指定库的恶意版本,以此来控制环境的可能
性。
1. 攻击者为应用程序提供一个恶意的库。
2. 应用程序因为未指定一个绝对的路径,或未验证加载的文件而加载了恶意的库。
3. 通过在库中执行代码,应用程序授予攻击者在一般情况下无法获得的权限或能力。
示例:以下代码使用 System.loadLibrary() 从名为 library.dll 的本地库加载代码,它通常可以在标
准的系统目录中找到。
...
System.loadLibrary("library.dll");
...
这里的问题是,对于要加载的库来说,System.loadLibrary() 只会接受库的名称,而不接受库的路径。根据 Java 1.4.2 API 文档,这个函数会进行如下操作 [1]:可以从库文件能够正常获取的本地 file system 中加载含有本地代码的文件。这一过程的具体操作需在特定的条件下执行。从库名称到某一个特定文件名的映射是在特定的系统方式下完成的。在搜索顺序方面,如果攻击者将一份恶意的 library.dll 放在高于应用程序需要加载的文件的位置,那么应用程序就会加载该恶意代码,而不会选择加载最初需要的文件。由于应用程序的这一特性,它会以较高的权限运行,这就意味着攻击者的 library.dll 内容将以较高的权限运行,从而可能导致攻击者完全控制整个系统。
攻击者能够通过篡改环境间接地控制程序加载的库。我们不应当完全信赖环境,还需采取预防措施,防止攻击者利用某些控制环境的手段进行攻击。库名称应该尽可能由应用程序控制,而且使用一个传送到System.load() 的绝对路径进行加载。应该避免 System.loadLibrary(),因为它的行为不能独立实现。如果编译时不清楚具体的路径,应该在执行的过程中利用已知的数值构造一个绝对路径。应对照一系列定义有效参数的不变量对从环境中读取的库名称和路径进行仔细的检查。有时还可以执行其他校验,以检查环境是否已被恶意篡改。例如,如果一个配置文件为可写,程序可能会拒绝运行。如果事先已知有关要加载的库的信息,程序就会执行检测,以校验文件的有效性。如果一个库应始终归属于某一特定用户,或者被分配了一组特定的权限,则可以在加载库之前,对这些属性进行校验。
最后,不可能对程序提供百分之百的保护,来防止强大的攻击者控制其所加载的库。对输入参数和环境可能执行的任何操作,都应努力鉴别并加以保护。其目标就是尽可能地防范各种攻击。
java.text.Format 中的 parse() 和 format() 方法包含一个可导致用户看到其他用户数据的 race condition。
例 1: 以下代码显示了此设计缺陷如何暴露自己。
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:26AM, 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
调用类 java.text.Format 中的 parse() 和 format() 时,请使用同步来防止 race condition。
例 2: 以下代码显示了使用同步构造更正例 1 中代码的两种方式。
public class Common {
private static SimpleDateFormat dateFormat;
...
public synchronized String format1(Date date) {
return dateFormat.format(date);
}
public String format2(Date date) {
synchronized(dateFormat)
{
return dateFormat.format(date);
}
}
}
此外还可以使用 org.apache.commons.lang.time.FastDateFormat 类,该类是一种对线程安全的java.text.SimpleDateFormat 版本。
Java 程序员常常会误解包含在许多 java.io 类中的 read() 及相关方法。在 Java 结果中,将大部分错误和异常事件都作为异常抛出。(这是 Java 相对于 C 语言等编程语言的优势:各种异常更加便于程序员考虑是哪里出现了问题。)但是,如果只有少量的数据可用,stream 和 reader 类并不认为这是异常的情况。这些类只是将这些少量的数据添加到返回值缓冲区,并且将返回值设置为读取的字节或字符数。所以,并不能保证返回的数据量一定等于请求的数据量。
这样,程序员就需要检查 read() 和其他 IO 方法的返回值,以确保接收到期望的数据量。
示例:下列代码会在一组用户中进行循环,读取每个用户的私人数据文件。程序员假设这些文件总是正好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);
}
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 的攻击,凭借这个攻击者可以在文件大小检查和从文件调用读取数据之间使用恶意文件替换结构良好的文件。
程序可能无法成功释放某一项系统资源。
资源泄露至少有两种常见的原因:
- 错误状况及其他异常情况。
- 未明确程序的哪一部份负责释放资源。
大部分 Unreleased Resource 问题只会导致一般的软件可靠性问题,但如果攻击者能够故意触发资源泄漏,该攻击者就有可能通过耗尽资源池的方式发起 denial of service 攻击。
示例:下面的方法绝不会关闭它所打开的文件句柄。FileInputStream 中的 finalize() 方法最终会调用 close(),但是不能确定何时会调用 finalize() 方法。在繁忙的环境中,这会导致 JVM 用尽它所有的文件句柄。
private void processFile(String fName) throws FileNotFoundException,IOException {
FileInputStream fis = new FileInputStream(fName);
int sz;
byte[] byteArray = new byte[BLOCK_SIZE];
while ((sz = fis.read(byteArray)) != -1) {
processBytes(byteArray, sz);
}
}
1. 请不要依赖 finalize() 回收资源。为了使对象的 finalize() 方法能被调用,垃圾收集器必须确认对象符合垃圾回收的条件。但是垃圾收集器只有在 JVM 内存过小时才会使用。因此,无法保证何时能够调用该对象的 finalize() 方法。垃圾收集器最终运行时,可能出现这样的情况,即在短时间内回收大量的资源,这种情况会导致“突发”性能,并降低总体系统通过量。随着系统负载的增加,这种影响会越来越明显。最后,如果某一资源回收操作被挂起(例如该操作需要通过网络访问数据库),那么执行 finalize() 方法的线程也将被挂起。
2. 在 finally 代码段中释放资源。示例中的代码可按以下方式改写:
public void processFile(String fName) throws FileNotFoundException,IOException {
FileInputStream fis;
try {
fis = new FileInputStream(fName);
int sz;
byte[] byteArray = new byte[BLOCK_SIZE];
while ((sz = fis.read(byteArray)) != -1) {
processBytes(byteArray, sz);
}
}
finally {
if (fis != null) {
safeClose(fis);
}
}
}
public static void safeClose(FileInputStream fis) {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
log(e);
}
}
}
以上方案使用了一个助手函数,用以记录在尝试关闭流时可能发生的异常。该助手函数大约会在需要关闭流时重新使用。
同样,processFile 方法不会将 fis 对象初始化为 null。而是进行检查,以确保调用 safeClose() 之前,fis 不是 null。如果没有检查 null,Java 编译器会报告 fis 可能没有进行初始化。编译器做出这一判断源于 Java 可以检测未初始化的变量。如果用一种更加复杂的方法将 fis 初始化为 null,那么编译器就无法检测 fis 未经初始化便使用的情况。