Java编码指南:编写安全可靠程序的75条建议

978-7-115-40371-1
作者: 【美】Fred Long(弗雷德•朗) Dhruv Mohindra(德鲁•莫欣达) Robert C. Seacord(罗伯特 C.西科德) Dean F. Sutherland(迪恩 F.萨瑟兰) David Svoboda(大卫•斯沃博达)
译者: 刘先宁尤青松
编辑: 杨海玲
分类: Java

图书目录:

详情

本书把那些不必列入Java安全编码标准但是同样会导致系统不可靠或不安全的Java编码实践整理了出来,并为这些糟糕的实践提供了相应的文档和警告,以及合规解决方案。读者可以将本书作为Java安全方面的工具书,根据自己的需要找到自己感兴趣的规则进行阅读和理解,也可以系统地了解Java安全规则,增强对Java安全特性、语言使用、运行环境特性的理解。

图书摘要

PEARSON

JAVA™CODING GUIDELINES 75 RECOMMENDATIONS FOR RELIABLE AND SECURE PROGRAMS

Java编码指南:编写安全可靠程序的75条建议

〔美〕Fred Long Dhruv Mohindra Robert C.Seacord Dean F.Sutherland David Svoboda 著

刘先宁 尤青松 译

人民邮电出版社

北京

图书在版编目(CIP)数据

Java编码指南:编写安全可靠程序的75条建议/(美)朗(Long,F.)等著;刘先宁,尤青松译.--北京:人民邮电出版社,2015.12

书名原文:Java Goding Guidelines:75 Recommendations for Reliable and Secure Programs

ISBN 978-7-115-40371-1

Ⅰ.①J… Ⅱ.①朗…②刘…③尤… Ⅲ.①JAVA语言—程序设计—指南 Ⅳ.①TP312-62

中国版本图书馆CIP数据核字(2015)第242307号

内容提要

本书是《Java安全编码标准》一书的扩展,书中把那些不必列入Java安全编码标准但是同样会导致系统不可靠或不安全的Java编码实践整理了出来,并为这些糟糕的实践提供了相应的文档和警告,以及合规解决方案。读者可以将本书作为Java安全方面的工具书,根据自己的需要,找到自己感兴趣的规则进行阅读和理解,或者在实际开发中遇到安全问题时,根据书中列出的大致分类对规则进行索引和阅读,也可以通读全书的所有规则,系统地了解Java安全规则,增强对Java安全特性、语言使用、运行环境特性的理解。

本书给出了帮助Java软件工程师设计出高质量的、安全的、可靠的、强大的、有弹性的、可用性和可维护性高的软件系统的75条编码指南,适合所有Java开发人员阅读,也适合高等院校教师和学生学习和参考。

◆著 [美]Fred Long Dhruv Mohindra Robert C.Seacord Dean F.Sutherland David Svoboda

译 刘先宁 尤青松

责任编辑 杨海玲

责任印制 张佳莹 焦志炜

◆人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

固安县铭成印刷有限公司印刷

◆开本:720×960 1/16

印张:17.75

字数:328千字  2015年12月第1版

印数:1-3000册  2015年12月河北第1次印刷

著作权合同登记号 图字:01-2013-9195号

定价:55.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

版权声明

Authorized translation from the English language edition,entitled Java Coding Guidelines:75 Recommendations for Reliable and Secure Programs,9780321933157 by Fred Long,Dhruv Mohindra,Robert C.Seacord,Dean F.Sutherland,and David Svoboda,published by Pearson Education,Inc.,publishing as Addison-Wesley,Copyright © 2014 by Pearson Education,Inc.

All rights reserved.No part of this book may be reproduced or transmitted in any form or by any means,electronic or mechanical,including photocopying,recording or by any information storage retrieval system,without permission from Pearson Education,Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD.and POSTS & TELECOM PRESS Copyright © 2015.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

译者简介

刘先宁 ThoughtWorks高级咨询师,长期从事一线软件开发工作,对Java、面向对象、敏捷方法论都有较深理解。其译作还包括《HTML5移动Web开发实践》。

尤青松 ThoughtWorks咨询师,在敏捷软件交付团队中担任技术领导人,尤其对Java企业软件开发及其安全编程有较深理解。

作为《The CERT®Oracle®Secure Coding Standard for Java™》的一个延伸,本书是非常有价值的,它甚至可以命名为《可靠的 Java 编码指南》(Reliable Java™ Coding Guidelines)。这些年来,可靠性和安全性之间的相互影响深深地触动了我。现如今,虽然有各种各样的显式的安全手段(如加密、身份验证等)用以确保系统安全,但是大多数的漏洞都来自于开发中的失误:编码太差或防御不足。当我们说要构建一个可靠的系统时,在很大程度上等同于构建一个安全的系统。系统的安全性将会得益于你对系统可靠性所做的一切,反之亦然。

本书强调了这样一个事实:所谓的安全性其实不是一个特性,而是一种针对所有的潜在的不安全因素都予以充分考虑的态度。安全性应该被持续贯穿在每一位软件工程师的设计思考过程中。它的基础是一系列的编码指南。本书最精彩的地方就是这些编码指南背后的微妙之处。例如,“用散列函数存储密码”,这看上去是一件很基础很明显的事情,然而经常有新闻报道说,由于程序员没有考虑到密码的加密而导致重要数据泄露。大量的细节之处成为了安全隐患的藏身之所,这让系统安全变得很棘手。本书充满了处理这些细节的出色指导。

——“Java之父”James A.Gosling

前言

本书为Java程序员提供了具体的建议。这些Java编码指南的应用将会带来更健壮、更能抵御攻击的系统。这些编码指南覆盖范围广泛,适用于大多数基于Java编写的运行在不同设备上的产品,这些设备包括电脑、游戏机、手机、平板电脑、家用电器和汽车电子设备。

不管是哪一门编程语言,开发人员在控制程序结构时都应遵守一系列基于该语言特定规则的指南。Java程序员也理应如此。

为了编写安全可靠的Java程序,Java程序员需要很多的帮助,单凭Java语言规范(Java Language Specification,JLS)[JLS 2013]是远远不够的。由于Java包含的许多语言特性和API很容易被误用,因此需要一些必要的避免这些陷阱的指导。

对于一个程序来说,可靠意味着在所有场景或所有可能输入条件下均能正常工作。不可避免的是,任何重要的程序都会遇到一些完全意想不到的输入或场景,从而发生错误。当此类错误发生时,最重要的是它产生的影响必须是有限的,而这可以通过快速定位错误并尽快处理它来实现。预期不寻常的输入或编程场景,并采用防御式编程方式,程序员会受益良多。

其中一些指南可能被认为是一种编码风格,但对于代码的可读性和可维护性来说,它们仍然很重要。针对Java语言,Oracle公司提供了一组编码约定[Conventions 2009]来帮助程序员编写具有一致编程风格的代码,这些约定已经被Java程序员广泛采用。

《The CERT®Oracle®Secure Coding Standard for Java™》

本书由《The CERT®Oracle®Secure Coding Standard for Java™》[Long 2012]一书的作者编写。该编码标准提供了一组针对Java语言的安全编码规则,目的是消除那些可能导致安全隐患的不安全编码实践。该安全编码标准为软件系统建立了规范的需求,同时也可以用来评估软件系统的一致性,例如,使用《Source Code Analysis Laboratory (SCALe)》[Seacord 2012]来检测软件系统的一致性。不过,有些不必列入Java安全编码标准的、糟糕的Java编码实践,也会导致不可靠或不安全的程序。本书针对这样的编码实践提供了相应的文档和警告。

虽然这些编码指南没有出现在《The CERT®Oracle®Secure Coding Standard for Java™》[Long 2012]中,但是它们的重要性却不应该被忽视。当一个编码指南不能构成一个规范需求时,是不能被收录到编码标准里的。无法构成规范需求的原因有很多,可能最常见的原因是,规则取决于程序员的意图。这些规则不能自动应用,除非程序员有特定的意图,在这种情况下,代码和特定的意图是需要保持一致的。为了形成一个规范的需求,需要证明的是,如果不遵守这一需求将会导致代码缺陷。有些编码指南已经被排除在编码标准之外(但包括在本书当中),遵守这些指南进行编码始终是一个好主意,但违反这些指南也并不总是会导致错误的结果。之所以会有这样的区别,是因为如果该系统没有因此产生特定的缺陷,我们不能说它不合格。因此,编码规则必须有非常严格的定义。而编码指南往往会对安全性和可靠性产生更深远的影响,因为它们可以被更概括地定义。

许多指南都参考了《The CERT®Oracle®Secure Coding Standard for Java™》里面的规则。这些引用的形式类似于“IDS01-J. Normalize strings before validating them”,引号中的前三个字母表示的是《The CERT®Oracle®Secure Coding Standard for Java™》一书的相关章节。例如,IDS指的是第2章“Input Validation and Data Sanitization (IDS)”。

这些安全编码标准针对Java的具体规则也可在CERT的安全编码百科网站(www.securecoding.cert.org)上找到,在那里它们有持续的更新。《The CERT®Oracle® Secure Coding Standard for Java™》提供了针对一致性测试的定义,但安全编码百科上可能有该书没有涉及的、关于这些定义的扩展信息以及见解,这些可以帮助程序员理解这些规则的意义。

本书中对于其他编码指南的交叉引用都只给出了编码指南的编号。

范围

本书侧重于Java SE 7平台环境,同时针对在安全编码过程中由于使用Java SE 7 API而产生的问题,进行了一些指导。Java语言规范Java SE 7版(The Java Language Specification: Java SE 7 Edition,JLS)[JLS 2013]规定了Java编程语言的行为,本书的这些指南主要是参考它开发出来的。

传统编程语言,如C和C++,它们的语言标准里包括了一些未定义的、未指明的以及实现定义的(implementation-defined)行为,这些都容易使程序员对这些行为的可移植性作出不正确的假设,从而导致漏洞。相比之下,Java语言规范更严格地定义了语言行为,因为Java是一种跨平台语言。即便如此,某些行为的自由裁量权还是留给了Java虚拟机(Java Virtual Machine,JVM)的实现者或者Java编译器。这些指南确定出了这种语言的特点,提供了一些可以帮助开发者定位问题的解决方案,让程序员领会和理解语言的局限性并更好地利用它。

只关注语言本身并不能编写出可靠安全的软件。有时,Java API中设计有问题的接口会被弃用。其他时候,API或相关文档也有可能被编程社区不正确地解读。这些指南识别出了有可能被曲解的API,并强调了它们的正确用法,常用的、有缺陷的设计模式和编程风格的示例也包括在内。

Java语言、其核心API和扩展API以及Java虚拟机提供了一些安全特性,如安全管理器、访问控制器、加密解密、自动内存管理、强类型检查和字节码验证。这些特性可以为大多数应用程序提供足够的安全,但它们的正确使用却是至关重要的。这些指南不仅强调了那些与安全体系结构相关的陷阱和警告,同时强调了它的正确实现。遵守这些指南可以确保被信任程序不出现大量的可利用的安全漏洞,从而避免可能导致的拒绝服务、信息泄露、错误的计算和特权升级。

包含的库

图P-1是Oracle公司Java SE产品的概念图。

这些编码指南主要适用于基于lang和util的库以及“其他基础库”,解决了其中的安全问题。这些编码指南没有包含那些已经标记为需要修复的公开bug或者那些没有负面影响的问题。某些功能性bug也被包含其中,它们发生频率高,可造成相当大的安全或可靠性问题,或者影响大多数依赖于核心平台的Java技术。这些指南不仅包括特定于核心API的安全问题,还包括重要的有关标准扩展API(javax包)的可靠性和安全性问题。

为了掌握Java提供的全方位的安全特性,程序员需要学习通过代码与其他组件和框架进行交互。偶尔,本书中的编码指南使用的示例来自于流行的Web应用程序框架(如Spring和Struts)和流行的技术,如Java服务器页面(Java Server Page,JSP),通过这些示例突出安全漏洞并不是单独存在的。只有当标准API本身没有提供选项来避免漏洞时,第三方库和解决方案才应予以考虑。

没有解决的问题

这个安全编码标准没有解决的若干问题。

内容

本书中采用的这些编码指南是广泛适用于几乎所有平台的,那些关注点在单一Java平台上的编码指南(例如,那些Android、Java微小版(ME)或Java企业版(EE)适用但Java标准版(SE)不适用的编码指南)被排除在了本书之外。另外,在Java标准版中,用于处理用户界面(用户界面工具包)或为 Web 界面提供特性(如声音、图像渲染、用户账户访问控制、会话管理、身份验证以及授权)的API,也超出了本指南的范围。尽管如此,书中的这些指南还是讨论了网络化的 Java 系统因不适当的输入验证而带来的相关风险,以及面临的注入式缺陷,并提供了适当的解决方案。另外,书中这些指南已假定产品的功能规范已被正确识别,同时没有来自高层设计和架构的缺陷。

编码风格

编码风格问题是主观性的,已被证明的是——在编码风格方面是不可能达成共识的。因此,本书通常尽量避免采用任何特定的编码风格。相反,我们建议用户通过这些指南定义自己的编码风格。让编码风格持续一致的最简单方法就是使用代码格式化工具。许多集成开发环境(IDE)都提供了这样的功能。

工具

这些指南并不能够自动检测或修正。在某些情况下,工具厂商可以选择实现检查器来确定代码是否违反了这些指南。软件工程研究所(Software Engineering Institute,       SEI)作为美国联邦政府资助的研发中心(Federally Funded Research and Development Center,FFRDC),不适合为此推荐特定的供应商或工具。

有争议的指南

通常,本书会尽量避免包含具有争议的、缺乏广泛共识的指南。

目标读者

本书主要适用于Java语言程序开发人员。虽然这些指南的重点放在了Java SE 7平台环境上,但对于工作在Java ME、Java EE或其他Java版本平台环境上的程序员们,也具有一定的参考价值(虽然不完全)。

尽管这些指南的主要设计目的是构建安全可靠的系统,但这些指南同时也有助于提高系统的其他质量属性,如安全性、可靠性、健壮性、可用性和可维护性。

这些指南还适合于:

分析工具的开发者,用于诊断出不安全或不合规范的Java语言程序;

软件开发经理、软件购买者或其他软件开发专家,用于建立一套严格的安全编码标准;

Java编码课程的教育工作者,可作为主教材或辅助教材。

内容组织

本书的75条编码指南是围绕着以下原则组织的。

第1章“安全”介绍了用于确保Java应用程序安全性的编码指南。

第2章“防御式编程”包含一些防御式编码指南,通过这些指南,程序员可以编写出防御性的程序。

第3章“可靠性”给出了提高Java应用程序可靠性和安全性的建议。

第4章“程序的可理解性”给出了让程序更易读易懂的建议。

第5章“程序员的常见误解”展示一些Java语言和编程概念经常被误解的情形。

附录A描述了本书针对在Android平台上进行Java编程的适用性。本书还包含了常用术语表和参考文献。

本书中的指南均有一致的结构。标题和起始段落定义了该指南的本质。紧接着通常是由一个或多个违规代码示例及其相应的合规解决方案。每个指南的最后一个部分均给出了该指南的适用性和具体参考文献。

致谢

本书得以完成离不开广大社区人员的帮助,首先,要感谢为本书编码指南做过贡献的Ron Bandes、Jose Sandoval Chaverri、Ryan Hall、Fei He、Ryan Hofler、Sam Kaplan、Michael Kross、Christopher Leonavicius、Bocong Liu、Bastian Marquis、Aniket Mokashi、Jonathan、Paulson、Michael Rosenman、Tamir Sen、John Truelove和Matthew Wiethoff。

其次,非常感谢为本书的内容提供过帮助的James Ahlborn、Neelkamal Gaharwar、Ankur Goyal、Tim Halloran、Sujay Jain、Pranjal Jumde、Justin Loo、Yitzhak Mandelbaum、Todd Nowacki、Vishal Patel、Justin Pincar、Abhishek Ramani、Brendon Saulsbury、Kirk Sayre、Glenn Stroz、Yozo Toda和Shishir Kumar Yadav。另外,还需要感谢Hiroshi Kumagai和JPCERT对本书的Android附录所做的贡献。

感谢本书的审阅人员:Thomas Hawtin、Dan Plakosh和Steve Scholnick。

感谢SEI和CERT的管理者支持和鼓励我们完成本书,他们是Archie Andrews、Rich Pethia、Greg Shannon和Bill Wilson。

感谢本书的编辑Peter Gordon以及他在 Addison-Wesley出版社的团队:Kim Boedigheimer、Jennifer Bortel、John Fuller、Stephane Nakib和Julie Nahil;同样感谢项目编辑Anna Popick和文字编辑Melinda Rankin。

感谢CERT团队的其他成员的支持和帮助,没有你们的帮助,本书不可能完成。最后,同样要感谢本书的责任编辑Carol J. Lallier,是她让这本书变成可能。

作者介绍

Fred Long是英国Aberystwyth大学计算机科学系的高级讲师,主要讲授:形式方法论,Java、C++和 C 语言编程,以及编程相关的安全问题。他还是英国计算机协会中威尔士分会的主席。Fred自1992年起一直担任软件工程研究所(Software Engineering Institute)的客座科学家。最近他的研究内容涉及了Java中的漏洞调查。Fred还是《The CERT®Oracle®Secure Coding Standard for Java™》一书的合著者。

Dhruv Mohindra是印度Persistent Systems有限公司CTO办公室下的安全实践小组的技术领导。他为各个领域的公司提供信息安全方面的咨询服务,包括云服务、协作、银行、金融业、电信行业、企业、移动、生命科学和卫生保健领域。他会定期为财富500强、中小企业和创业公司的高级经理和开发团队提供咨询服务,帮助他们在软件开发周期中构建安全,实施最佳信息安全实践。

Dhruv曾在软件工程研究院的CERT分部工作,持续推动编程社区的安全意识提升。Dhruv拥有印度Pune大学的本科学位,以及卡内基-梅隆大学的信息安全策略与管理硕士学位。Dhruv同样也是《The CERT®Oracle®Secure Coding Standard for Java™》一书的合著者。

Robert C. Seacord是一位安全编码技术经理,就职于宾夕法尼亚州匹兹堡市的卡内基-梅隆软件工程研究院(Software Engineering Institute,SEI)的CERT分部。他还是卡内基-梅隆大学计算机科学与信息网络学院的教授,是《The CERT®C Secure Coding Standard》一书的作者,以及下列书籍的合著者:《Building Systems from Commercial Components》《Modernizing Legacy Systems》《The CERT®Oracle®Secure Coding Standard for Java™》和《Secure Coding in C and C++, Second Edition》。他发表过60多篇论文,这些论文涉及的领域有:软件安全、基于组件的软件工程、基于 Web 的系统设计、遗留系统升级,组件仓库和搜索引擎,以及用户界面的设计与开发。Robert 自 2005 年起,一直在为私人企业、学术界和政府教授《Secure Coding in C and C++》。他从1982年就在IBM开始了职业编程生涯,主要工作有通信软件和操作系统软件,处理器开发和软件工程。Robert还曾服务于X Consortium,在那儿他开发和维护了Common Desktop Environment和X Window System的代码。他还是卡内基-梅隆大学在C编程语言国际标准化组织ISO/IEC JTC1/SC22/WG14中的代表。

Dean F. Sutherland是CERT的高级软件安全工程师,拥有卡内基-梅隆大学软件工程博士学位(2008年获得)。在回归学术界以前,他在Tartan公司做了14年的软件工程师,在Tartan的最后6年主要是以技术委员会的高级成员和技术领导的身份研究编译器底层技术。他是整个公司研发中心最活跃的技术人员,也是Tartan公司设计和推动新的软件开发流程的煽动者。在 Tartan,他管理研发项目,并领导了12人的编译器底层技术团队。Dean同样也是《The CERT®Oracle®Secure Coding Standard for Java™》一书的合著者。

David Svoboda是CERT/SEI的软件安全工程师,也是《The CERT®Oracle®Secure Coding Standard for Java™》一书的合著者。他还维护了CERT安全编码标准的官方网站,这些网站包括Java、C、C++、Perl语言的安全编码标准。自 1991 年起,David 就是卡内基-梅隆大学的主要开发人员,参与过很多不同的软件开发项目,项目类型从分层芯片建模、社会组织仿真到自动机器翻译(AMT)都有。他1996年开发的KANTOO AMT软件仍然在Caterpillar产品上使用着。他有13年多的Java开发经验,从Java 2开始,他的Java项目包括Tomcat Servlets和一些Eclipse插件。他还在全世界范围为军队、政府和银行业教授Secure Coding in C and C++。David还是C编程语言的国际标准化组织ISO/IEC JTC1/SC22/WG14和C++编程语言的国际化标准组织ISO/IEC JTC1/SC22/WG21的活跃参与者。

第1章 安全

Java编程语言及其运行时系统在最初被设计时就考虑到了安全性。例如,指针操纵对程序员来说是不可见的、隐式的,任何引用空指针的尝试都会导致抛出异常。相似地,如果对数组或者字符串的尝试访问超出了边界,那么也会导致异常。Java是一种强类型的语言,所有的隐式类型转换都定义得非常明确,而且与平台无关,算术类型及其转换也是如此。Java虚拟机(Java Virtual Machine,JVM)有一个内置的字节码校验器,可以确保被执行的字节码符合Java语言规范Java SE 7版(JLS),因此所有在语言中定义的检查都能生效,无一例外。

Java类加载器机制能够识别出那些被加载到 JVM 的类,并且能够区分出可信的系统类和不可信的其他类。通过对来自外部的类进行数字签名,可以为这些类赋予相应的特权;这些数字签名也能被类加载器检测到,从而有助于对类的识别。Java还提供了一个细粒度可扩展的安全机制,使程序员能够对其希望使用的资源进行访问控制,如系统信息、文件、网络套接字以及任何其他安全敏感性资源。这种安全机制需要一个运行时安全管理器,用于强制执行安全策略。安全管理器及其安全策略通常是通过命令行参数指定的,但有时也可以通过编程方式进行安装,前提是这样的动作没有被现有的安全策略所禁止。通过类加载器机制提供的识别功能,资源访问特权可以被扩展到非系统Java类中。

企业级Java应用程序容易受到攻击,因为它们接受不被信任的输入并与多个复杂的子系统进行交互。如果某个子系统易受注入攻击(如跨站脚本攻击[XSS]、XPath注入和LDAP注入)影响时,那么就可能导致整个系统都受到影响。一种有效缓解威胁的策略是:只接受白名单允许的输入;如果输入的值需要被渲染,那么要先对其进行编码或转义,然后再输出。

本章包含的指南重点关注如何确保基于Java的应用程序的安全。这些指南将覆盖以下4个方面。

(1)处理敏感数据。

(2)避免受到常见的注入攻击。

(3)被滥用并导致安全威胁的语言特性。

(4)Java细粒度安全机制的细节。

指南1:限制敏感数据的生命周期

内存中的敏感信息很容易受到攻击,导致泄漏。对于以下条件,不管应用程序满足哪一个,能在应用程序所在的系统上执行代码的攻击者,都能访问这些数据。

使用对象来存储敏感数据,但是内容在使用后没有被清除或没有被垃圾收集器回收。

具有可以被操作系统按需(例如,为了执行内存管理任务或者为了支持系统休眠)交换到磁盘的内存页面。

在缓冲区(如BufferedReader)中有敏感数据。这些缓冲区持有敏感数据在操作系统缓存或内存中的副本。

使用了一些反射机制来控制敏感数据的流动,这些反射技巧可能规避掉系统对该数据的生命周期限制。

通过调试信息、日志文件、环境变量、线程转储和核心转储等方式暴露敏感数据。

如果内存中包含的敏感数据在使用后没有及时被清除,那么这些数据将极有可能被泄露。为了降低敏感数据泄露的风险,程序必须尽可能地最小化敏感数据的生命周期。

完全缓解(即对内存数据万无一失的保护)需要底层操作系统和Java虚拟机的支持。例如,如果将敏感数据交换至磁盘是一个问题,那么就需要有一个禁用交换和休眠的安全操作系统。

违规代码示例

在以下违规代码示例中,程序从控制台读取用户名和密码信息,并将密码存储在一个String对象中。在垃圾收集器回收这个String对象关联的内存之前,凭证信息会一直处于暴露状态。

class Password {

public static void main (String args[]) throws IOException {

Console c = System.console();

if (c == null) {

System.err.println("No console.");

System.exit(1);

}

String username = c.readLine("Enter your user name: ");

String password = c.readLine("Enter your password: ");

if (!verify(username, password)) {

throw new SecurityException("Invalid Credentials");

}

// ...

}

// Dummy verify method, always returns true

private static final boolean verify(String username,

String password) {

return true;

}

}

合规解决方案

下面的合规解决方案使用 Console.readPassword()方法从控制台获取密码信息。

class Password {

public static void main (String args[]) throws IOException {

Console c = System.console();

if (c == null) {

System.err.println("No console.");

System.exit(1);

}

String username = c.readLine("Enter your user name: ");

char[] password = c.readPassword("Enter your password: ");

if (!verify(username, password)) {

throw new SecurityException("Invalid Credentials");

}

// Clear the password

Arrays.fill(password, ' ');

}

// Dummy verify method, always returns true

private static final boolean verify(String username,

char[] password) {

return true;

}

}

Console.readPassword()方法允许密码以字符序列的形式返回,而不是以String对象的形式。因此,程序员可以在使用密码后立即将其从数组中清除。同时这个方法也禁止将密码输出到控制台。

违规代码示例

下面的违规代码示例使用了一个BufferedReader来包装InputStreamReader对象,导致敏感数据可以从文件中被读取。

void readData() throws IOException{

BufferedReader br = new BufferedReader(new InputStreamReader(

new FileInputStream("file")));

// Read from the file

String data = br.readLine();

}

BufferedReader.readLine()方法以String对象的形式返回敏感数据,导致数据在不被需要后仍然会存留很久。BufferedReader.read(char[], int, int)方法可以读取和填充一个char数组,不过,它需要程序员在使用完数组中的敏感数据后,手动将其清除,否则就会导致泄漏。另外,尽管BufferedReader是用来包装FileReader对象的,但它同样也会遭遇相似的陷阱。

合规解决方案

在下面的合规解决方案中,程序使用了一个直接分配的新I/O(new I/O,NIO)缓冲区从文件中读取敏感数据。这些数据可以在使用后被立即清除,并且不会被缓存或缓冲在多个位置上,它只会出现在系统内存中。

void readData(){

ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);

try (FileChannel rdr =

(new FileInputStream("file")).getChannel()) {

while (rdr.read(buffer) > 0) {

// Do something with the buffer

buffer.clear();

}

} catch (Throwable e) {

// Handle error

}

}

注意,必须手动清除缓冲数据,因为垃圾收集器不会回收直接缓冲区。

适用性

对敏感数据生命周期限制失败,导致信息泄露。

参考文献

[API 2013]   Class ByteBuffer

[Oracle 2013b]  “Reading ASCII Passwords from an InputStream Example” from the Java Cryptography Architecture[JCA]Reference Guide

[Tutorials 2013]  I/O from the Command Line

指南2:不要在客户端存储未经加密的敏感数据

在使用客户端服务器模式构建应用程序时,如果客户端容易受到攻击,那么将用户凭证信息这样的敏感数据存储在客户端,会导致这些信息在未经授权的情况下被泄露。

对于 Web 应用程序,最常见的缓解这个问题的方法,就是为客户端提供一个cookie,将敏感信息存储在服务器上。这些cookie由Web服务器创建,只存储在客户端上,一段时间以后就会失效。当客户端重新连接至服务器时,必须提供相应的cookie,服务器用它来识别客户端,然后为识别后的客户端提供敏感信息。

cookie并不能防止敏感信息免受跨站脚本(Cross-Site Scripting,XSS)攻击。只要攻击者通过XSS攻击获得cookie,或者通过攻击客户端直接获得cookie,就可以使用这些cookie从服务器上获得敏感信息。如果一个会话超出限制时间(如15分钟)后,服务器端随即让该会话失效,那么这样的风险是可以被时间控制的。

一个cookie通常是一个短字符串。如果它包含敏感信息,那么这些信息应该被加密。敏感信息包括用户名、密码、信用卡号码、社会保险号以及任何可识别该用户的个人信息。更多关于密码管理的细节,参见指南13。更多有关保护存有敏感信息的内存的信息,参见指南1。

违规代码示例

在下面的违规代码示例中,登录程序将用户名和密码存储至cookie中,用以识别该用户并允许后续请求。

protected void doPost(HttpServletRequest request,

HttpServletResponse response) {

// Validate input (omitted)

String username = request.getParameter("username");

char[] password =

request.getParameter("password").toCharArray();

boolean rememberMe =

Boolean.valueOf(request.getParameter("rememberme"));

LoginService loginService = new LoginServiceImpl();

if (rememberMe) {

if (request.getCookies()[0] != null &&

request.getCookies()[0].getValue() != null) {

String[] value =

request.getCookies()[0].getValue().split(";");

if (!loginService.isUserValid(value[0],

value[1].toCharArray())) {

// Set error and return

} else {

// Forward to welcome page

}

} else {

boolean validated =

loginService.isUserValid(username, password);

if (validated) {

Cookie loginCookie = new Cookie("rememberme", username +

";" + new String(password));

response.addCookie(loginCookie);

// ... forward to welcome page

} else {

// Set error and return

}

}

} else {

// No remember-me functionality selected

// Proceed with regular authentication;

// if it fails set error and return

}

Arrays.fill(password, ' ');

}

不管怎样,用以上述方式实现“记住我”的功能,是不安全的,因为攻击者只要能访问客户端机器就能直接在客户端上获得这些敏感信息。这段代码也违背了指南13。

合规解决方案(会话)

在下面的合规解决方案中,程序是通过在cookie中只存储一个用户名和一个安全随机字符串来实现“记住我”功能的。同时程序还使用了HttpSession来管理会话状态。

protected void doPost(HttpServletRequest request, HttpServletResponse response) {

// Validate input (omitted)

String username = request.getParameter("username");

char[] password =

request.getParameter("password").toCharArray();

boolean rememberMe =

Boolean.valueOf(request.getParameter("rememberme"));

LoginService loginService = new LoginServiceImpl();

boolean validated = false;

if (rememberMe) {

if (request.getCookies()[0] != null &&

request.getCookies()[0].getValue() != null) {

String[] value =

request.getCookies()[0].getValue().split(";");

if (value.length != 2) {

// Set error and return

}

if (!loginService.mappingExists(value[0], value[1])) {

// (username, random) pair is checked

// Set error and return

}

} else {

validated = loginService.isUserValid(username, password);

if (!validated) {

// Set error and return

}

}

String newRandom = loginService.getRandomString();

// Reset the random every time

loginService.mapUserForRememberMe(username, newRandom);

HttpSession session = request.getSession();

session.invalidate();

session = request.getSession(true);

// Set session timeout to 15 minutes

session.setMaxInactiveInterval(60 * 15);

// Store user attribute and a random attribute

// in session scope

session.setAttribute("user", loginService.getUsername());

Cookie loginCookie =

new Cookie("rememberme", username + ";" + newRandom);

response.addCookie(loginCookie);

// ... forward to welcome page

} else { // No remember-me functionality selected

// ... authenticate using isUserValid(),

// and if failed, set error

}

Arrays.fill(password, ' ');

}

服务器维护了一个用户名与安全随机字符串之间的映射。当用户选择“记住我”时,doPost()方法会检查请求提供的cookie是否包含一个有效的用户名和随机字符串对。如果在映射中找到了这个用户名和随机字符串对,那么服务器会对这个用户进行身份验证,并在验证成功的情况下跳转到该用户的欢迎页面。如果没有找到,服务器会向客户端返回一个错误。如果用户选择了“记住我”,但客户端未能提供一个有效的cookie,那么服务器会要求用户使用他的凭据进行身份验证。如果身份验证成功,服务器会给客户端发送一个新的具有“记住我”特征的cookie。

上述解决方案,通过在验证完毕后立即令当前会话失效并重新创建一个新的会话,避免了“会话固化”攻击;另外,通过将客户端访问会话超时时间设置为15分钟,也减小了攻击者进行“会话劫持”攻击的机会。

适用性

在客户端存储未经加密的敏感信息,导致信息泄露给客户端的攻击者。

参考文献

[Oracle 2011c]    Package javax.servlet.http

[OWASP 2009]   Session Fixation in Java

[OWASP 2011]   Cross-Site Scripting(XSS)

[W3C 2003]     The World Wide Web Security FAQ

指南3:为敏感可变类提供不可修改的包装器

// ...

字段的不变性可以防止其被意外修改以及恶意篡改,因此在接受输入或返回值时,防御性复制不可变字段是不必要的。然而,部分敏感类由于某些原因必须要被改变。幸运的是,可以通过不可修改的包装器,将可变类的只读访问权限授予不可信代码。例如,Collection(集合)类包括一组包装器,允许客户端观察一个不可修改的集合对象视图。

违规代码示例

在下面的违规代码示例中,Mutable这个类允许内部数组对象被修改:

class Mutable {

private int[] array = new int[10];

public int[] getArray() {

return array;

}

public void setArray(int[] i) {

array = i;

}

}

// ...

private Mutable mutable = new Mutable();

public Mutable getMutable() {return mutable;}

不可信的调用程序能调用setArray()方法,这违反了对象的不变性属性。调用getArray()方法还允许修改该类的私有内部状态。此外,这个类还违反了《The CERT®Oracle®Secure Coding Standard for Java™》[Long 2012]的“OBJ05-J. Defensively copy private mutable class members before returning their references”。

违规代码示例

在下面的违规代码示例中,MutableProtector继承了Mutable类。

class MutableProtector extends Mutable {

@Override

public int[] getArray() {

return super.getArray().clone();

}

}

// ...

private Mutable mutable = new MutableProtector();

// May be safely invoked by untrusted caller having read ability

public Mutable getMutable() {return mutable;}

在这个类中,调用getArray()方法不允许修改该类的私有内部状态,符合“OBJ05-J. Defensively copy private mutable class members before returning their references”[Long 2012]。然而,不可信的调用程序能调用 setArray()方法修改Mutable对象。

合规解决方案

一般来说,通过对核心接口定义的所有方法(包括赋值方法),提供合适的包装器,可以将敏感类转化为安全视图(safe-view)对象。赋值方法的包装器必须抛出UnsupportedOperationException 异常,这样调用者就不太可能做出影响属性不变性的操作。

在下面的解决方案中,setArray()方法覆盖了Mutable.setArray()方法,防止了对Mutable对象的改变。

class MutableProtector extends Mutable {

@Override

public int[] getArray() {

return super.getArray().clone();

}

@Override

public void setArray(int[] i) {

throw new UnsupportedOperationException();

}

}

private Mutable mutable = new MutableProtector();

// May be safely invoked by untrusted caller having read ability

public Mutable getMutable() {return mutable; }

MutableProtector类覆盖了Mutable类的getArray()方法,该方法克隆了Mutable类的原始数组。因此,尽管调用代码能够得到Mutable对象中的数组字段的数据,但原始数组是保持不变的,并且调用代码不能访问到原始数组。当调用者试图调用MutableProtector对象的 setArray()方法时,覆盖后的setArray()方法会抛出一个异常。这样保证了MutableProtector对象可以被传递给不可信代码,因为该对象只允许读操作。

适用性

对于敏感的可变对象,程序没能给不可信的代码提供一个不可修改的安全视图,导致恶意篡改和对象腐化。

参考文献

[Long 2012]   OBJ05-J. Defensively copy private mutable class members before returning their references

[Tutorials 2013]  Unmodifiable Wrappers

指南4:确保安全敏感方法被调用时参数经过验证

当应用程序代码调用安全敏感方法时,必须验证被传递到方法中的参数。特别是,       null值可能会被某些特定的安全敏感方法解读为良性参数,导致覆盖程序默认设置。尽管安全敏感方法应该是防御式编码,但是客户端代码也必须验证参数。如果不这样做,就会导致特权升级,可以执行任意代码。

违规代码示例

下面的违规代码示例展示了一个双参数方法 doPrivileged(),它的第二个参数是一个访问控制上下文。这段代码的功能是从以前保存的上下文中恢复特权。

AccessController.doPrivileged(

new PrivilegedAction<Void>() {

public Void run() {

// ...

}

}, accessControlContext);

当传入空的(null)访问控制上下文时,双参数方法doPrivileged()不能仅对之前保存的上下文进行授权。因此,当accessControlContext参数为null时,这段代码可能会导致过度授权。如果程序员打算以一个空的访问控制上下文调用AccessController.doPrivileged()方法,那么必须显式传递一个null参数或使用单参数版本的AccessController.doPrivileged()方法。

合规解决方案

下面的合规解决方案通过确保 accessControlContext 不为空,阻止了程序的过度授权。

if (accessControlContext == null) {

throw new SecurityException("Missing AccessControlContext");

}

AccessController.doPrivileged(

new PrivilegedAction<Void>() {

public Void run() {

// ...

}

}, accessControlContext);

适用性

必须彻底理解安全敏感方法,并验证它们的参数,以防止意想不到的参数值(如空值)导致的极端情况。如果有意想不到的参数值被传递给安全敏感方法,那么有可能会导致程序可以执行任意代码,还有可能会导致特权升级。

参考文献

[API 2013]   AccessController.doPrivileged(), System.setSecurityManager()

指南5:防止任意文件上传

Java应用程序,包括Web应用程序,在接受文件上传的同时,必须确保攻击者不能上传或者传输恶意文件。如果被限制的文件中包含了可以在目标系统上执行的代码,那么应用程序层的防御将会受到威胁。例如,如果一个应用程序允许了 HTML文件的上传,那么也就相当于间接允许了恶意代码的执行——攻击者可以提交一个有效的HTML文件,如果该文件包含了跨站脚本(XSS)攻击的代码片段,在程序缺乏输出过滤的情况下这些攻击脚本将会被有效执行。出于这个原因,许多应用程序都对可上传文件的类型做了限制。

也有可能出现上传的文件具有危险的后缀这种情况,如.exe(可执行文件后缀)和.sh(可执行脚本后缀),这种情况会导致服务器端应用程序执行任意代码。如果一个上传文件的应用程序,只对HTTP报头中的Content-Type(内容类型)字段进行了限制,那么该程序就容易遭受到这种攻击。

一个典型的支持文件上传的Java Server Page(JSP)页面可能包含以下代码:

<s:form action="doupload" method="POST"

enctype="multipart/form-data">

<s:file name="uploadFile" label="Choose File" size="40" />

<s:submit value="Upload" name="submit" />

</s:form>

许多Java企业级框架都提供了相应的配置设置,旨在防止任意文件被上传。遗憾的是,大多数企业级框架都无法提供足够的保护。为了弥补这个不足,需要检查元数据属性中的文件大小、内容类型和文件内容。

违规代码示例

下面的违规代码示例展示了Struts 2应用程序中实现上传功能的XML代码,其中拦截器(interceptor)代码的职责是允许文件上传。

<action name="doUpload" class="com.example.UploadAction">

<interceptor-ref name="fileupload">

<param name="maximumSize"> 10240 </param>

<param name="allowedTypes">

text/plain,image/JPEG,text/html

</param>

</interceptor-ref>

</action>

负责文件上传的代码在UploadAction类中:

public class UploadAction extends ActionSupport {

private File uploadedFile;

// setter and getter for uploadedFile

public String execute() {

try {

// File path and file name are hardcoded for illustration

File fileToCreate = new File("filepath", "filename");

// Copy temporary file content to this file

FileUtils.copyFile(uploadedFile, fileToCreate);

return "SUCCESS";

} catch (Throwable e) {

addActionError(e.getMessage());

return "ERROR";

}

}

}

参数maximumSize确保了指定的Action不会接收到一个非常大的文件。参数allowedTypes定义了可以被接受的文件类型。然而,这种方法不能完全确保上传的文件符合安全要求,因为拦截器检查可以被轻易地绕过。如果攻击者使用一个代理工具在网络传输过程中改变原始HTTP请求的内容类型,那么该框架将无法防止文件的上传。因此,攻击者可以上传一个恶意文件,如扩展名为.exe的可执行文件。

合规解决方案

只有当文件的内容类型严格匹配文件的实际内容时,文件才能成功上传。例如,一个具有image头信息的文件,只能包含一个图像,不能包含可执行代码。下面的这个合规解决方案,使用Apache Tika库[Apache 2013],通过现有的解析器库检测和提取文档中的元数据信息和结构化文本内容。在调用负责上传文件的 execute()方法之前,必须先调用checkMetaData()方法。

public class UploadAction extends ActionSupport {

private File uploadedFile;

// setter and getter for uploadedFile

public String execute() {

try {

// File path and file name are hardcoded for illustration

File fileToCreate = new File("filepath", "filename");

boolean textPlain = checkMetaData(uploadedFile,

"text/plain");

boolean img = checkMetaData(uploadedFile, "image/JPEG");

boolean textHtml = checkMetaData(uploadedFile,

"text/html");

if (!textPlain || !img || !textHtml) {

return "ERROR";

}

// Copy temporary file content to this file

FileUtils.copyFile(uploadedFile, fileToCreate);

return "SUCCESS";

} catch (Throwable e) {

addActionError(e.getMessage());

return "ERROR";

}

}

public static boolean checkMetaData(

File f, String getContentType) {

try (InputStream is = new FileInputStream(f)) {

ContentHandler contenthandler = new BodyContentHandler();

Metadata metadata = new Metadata();

metadata.set(Metadata.RESOURCE_NAME_KEY, f.getName());

Parser parser = new AutoDetectParser();

try {

parser.parse(is, contenthandler,

metadata, new ParseContext());

} catch (SAXException | TikaException e) {

// Handle error

return false;

}

if (metadata.get(Metadata.CONTENT_TYPE).equalsIgnoreCase(

getContentType)) {

return true;

} else {

return false;

}

} catch (IOException e) {

// Handle error

return false;

}

}

}

AutoDetectParser会基于需要解析的文件的内容类型,选择最佳的可用解析器。

适用性

任意文件的上传导致特权升级和任意代码的执行。

参考文献

[Apache 2013]  Apache Tika:A Content Analysis Toolkit

指南6:正确地编码或转义输出

适当的输入检查可以防止恶意数据插入数据库等子系统。虽然不同的子系统需要不同类型的数据无害化处理,但是子系统最终要接收的输入形式都很明确,因此可以很清楚地知道需要什么样的数据无害化处理。

有几个用于输出数据的子系统。HTML渲染器是一种常见的用于显示程序输出的子系统。发送给输出子系统的数据,来源似乎都很可靠。然而,假设输出数据不必做无害化处理,是很危险的,因为这些数据可能间接来源于一个不可信的来源,并且可能包含恶意的内容。如果没能正确地处理传递给输出子系统的数据,就会让多种类型的攻击有机可乘。例如,HTML渲染器易受HTML注入攻击和跨站脚本(XSS)攻击[OWASP 2011]。因此,用于防止此类攻击的输出无害化处理,和输入无害化处理一样重要。

和输入验证一样,数据应该在消除恶意字符之前被标准化。正确编码所有输出字符,其中那些已知的、不会由于绕过数据验证而导致安全漏洞的字符除外。更多信息参见《The CERT®Oracle®Secure Coding Standard for Java™》[Long 2012]的“IDS01-J. Normalize strings before validating them”。

违规代码示例

下面的违规代码示例使用基于Java EE的Spring框架中的模型-视图-控制器(Model-View-Controller,MVC)概念,向用户显示了没有经过编码或转义的数据。因为数据会被发送到Web浏览器,所以该代码容易受到HTML注入攻击和XSS攻击。

@RequestMapping("/getnotifications.htm")

public ModelAndView getNotifications(

HttpServletRequest request, HttpServletResponse response) {

ModelAndView mv = new ModelAndView();

try {

UserInfo userDetails = getUserInfo();

List<Map<String,Object>> list =

new ArrayList<Map<String, Object>>();

List<Notification> notificationList =

NotificationService.getNotificationsForUserId(

userDetails.getPersonId());

for (Notification notification: notificationList) {

Map<String,Object> map = new HashMap<String, Object>();

map.put("id", notification.getId());

map.put("message", notification.getMessage());

list.add(map);

}

mv.addObject("Notifications", list);

} catch (Throwable t) {

// Log to file and handle

}

return mv;

}

合规解决方案

下面的合规解决方案定义了一个ValidateOutput类,这个类首先将输出规范化到了一个已知的字符集,然后使用白名单的对数据做了无害化处理,最后对所有未指明的数据值进行编码,强制执行了双重检查机制。注意,所需的白名单模式将根据不同字段的具体需求而变化[OWASP 2013]。

public class ValidateOutput {

// Allows only alphanumeric characters and spaces

private static final Pattern pattern =

Pattern.compile("^[a-zA-Z0-9\\s]{0,20}$");

// Validates and encodes the input field based on a whitelist

public String validate(String name, String input)

throws ValidationException {

String canonical = normalize(input);

if (!pattern.matcher(canonical).matches()) {

throw new ValidationException("Improper format in " +

name + " field");

}

// Performs output encoding for nonvalid characters

canonical = HTMLEntityEncode(canonical);

return canonical;

}

// Normalizes to known instances

private String normalize(String input) {

String canonical =

java.text.Normalizer.normalize(input,

Normalizer.Form.NFKC);

return canonical;

}

// Encodes nonvalid data

private static String HTMLEntityEncode(String input) {

StringBuffer sb = new StringBuffer();

for (int i = 0; i < input.length(); i++) {

char ch = input.charAt(i);

if (Character.isLetterOrDigit(ch) ||

Character.isWhitespace(ch)) {

sb.append(ch);

} else {

sb.append("&#" + (int)ch + ";");

}

}

return sb.toString();

}

}

// ...

@RequestMapping("/getnotifications.htm")

public ModelAndView getNotifications(HttpServletRequest request,

HttpServletResponse response) {

ValidateOutput vo = new ValidateOutput();

ModelAndView mv = new ModelAndView();

try {

UserInfo userDetails = getUserInfo();

List<Map<String,Object>> list =

new ArrayList<Map<String,Object>>();

List<Notification> notificationList =

NotificationService.getNotificationsForUserId(

serDetails.getPersonId());

for (Notification notification: notificationList) {

Map<String,Object> map = new HashMap<String,Object>();

map.put("id", vo.validate("id" ,notification.getId()));

map.put("message",

vo.validate("message", notification.getMessage()));

list.add(map);

}

mv.addObject("Notifications", list);

}

catch (Throwable t) {

// Log to file and handle

}

return mv;

}

当接受危险的字符如双引号和尖括号时,必须对输出进行编码和转义。即使在输入白名单中不允许出现这样的字符,也要对输出进行转义,因为这样就提供了一个二级防御。注意,确切的转义序列会发生变化,具体取决于该输出将要被嵌入的地方。例如,HTML标签属性值、CSS、URL或者脚本中都可能会出现不可信输出,不同情况下的输出编码例程也会有所不同。另外,在有些上下文中,无法安全地使用不可信的数据。关于防止XSS攻击的更多信息,请参考OWASP XSS (Cross-Site Scripting) Prevention Cheat Sheet(www.owasp.org/index.php/XSS_Prevention_Cheat_Sheet)。

适用性

在输出被显示前或被传递到可信边界前,没能对其进行编码或转义,导致任意代码的执行。

相关漏洞

据2006年1月报道,Apache GERONIMO-1474漏洞允许攻击者提交包含JavaScript的URL。网络访问日志查看器(Web Access Log Viewer)未能对跳转到管理员控制台的数据进行无害化处理,从而促成了一个典型的XSS攻击。

参考文献

[Long 2012]   IDS01-J.Normalize strings before validating them

[OWASP 2011]  Cross-Site Scripting(XSS)

[OWASP 2013]  How to Add Validation Logic to HttpServletRequest XSS (Cross-Site Scripting)Prevention Cheat Sheet

指南7:防止代码注入

当有不可信的输入注入动态构造的代码中时,会引起代码注入攻击。一个明显的潜在漏洞是在Java代码中使用JavaScript代码。javax.script包定义了Java脚本引擎的接口和类,以及在Java代码中使用这些接口和类的框架。javax.script API的滥用,会导致攻击者在目标系统上执行任意代码。

这条指南是《The CERT®Oracle®Secure Coding Standard for Java™》[Long 2012]的“IDS00-J. Sanitize untrusted data passed across a trust boundary”的一个实例。

违规代码示例

下面的违规代码示例将不可信的用户输入嵌入了负责打印输入的JavaScript语句中。

private static void evalScript(String firstName)

throws ScriptException {

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("javascript");

engine.eval("print('"+ firstName + "')");

}

攻击者可以传入一个别有用心的输入值,用以注入恶意的JavaScript代码。下面的这个示例展示了一个恶意的字符串,该字符串包含的JavaScript代码可以在目标系统上创建或覆盖现有的文件。

dummy\');

var bw = new JavaImporter(java.io.BufferedWriter);

var fw = new JavaImporter(java.io.FileWriter);

with(fw) with(bw) {

bwr = new BufferedWriter(new FileWriter(\"config.cfg\"));

bwr.write(\"some text\"); bwr.close();

}

// ;

这个示例中的脚本首先打印“dummy”,然后将“some text”写入一个名为config.cfg的配置文件中。这是一个可以导致任意代码执行的真实漏洞。

合规解决方案(白名单)

最好的防御代码注入漏洞的方式就是阻止包含可执行代码的用户输入。任何用于动态代码的用户输入,都必须进行无害化处理,例如,确保用户输入只包含白名单里的有效字符。最好是在数据输入后,通过使用用于存储和处理数据的提取方法,立即执行数据无害化处理。更多细节请参考“IDS00-J. Sanitize untrusted data passed across a trust boundary”[Long 2012]。如果用户名中必须包含某些特殊字符,那么必须先对它们进行标准化,然后再对它们进行表单输入验证处理。下面的合规解决方案使用了白名单来防止脚本引擎对未经处理的输入进行解析。

private static void evalScript(String firstName)

throws ScriptException {

// Allow only alphanumeric and underscore chars in firstName

// (modify if firstName may also include special characters)

if (!firstName.matches("[\\w]*")) {

// String does not match whitelisted characters

throw new IllegalArgumentException();

}

ScriptEngineManager manager = new ScriptEngineManager();

ScriptEngine engine = manager.getEngineByName("javascript");

engine.eval("print('"+ firstName + "')");

}

合规解决方案(安全沙盒)

另一种方式是使用安全管理器创建一个安全的沙盒(参见指南20)。应用程序应该阻止执行任意命令的脚本,如查询本地文件系统。双参数版本的doPrivileged()方法可用于处理更低特权的操作,这种情况下,应用程序本身必须执行更高特权的操作,但脚本引擎决不能拥有这样的特权。对于默认策略文件中那些新创建的保护域,       RestrictedAccessControlContext类减少了授予它们的权限。这些有效权限是新创建的保护域和系统安全策略二者权限的交集。有关 doPrivileged()方法的更多细节,请参考指南16。

下面的合规解决方案演示了如何在双参数版本的doPrivileged()中使用AccessControlContext。

class ACC {

private static class RestrictedAccessControlContext {

private static final AccessControlContext INSTANCE;

static {

INSTANCE =

new AccessControlContext(

new ProtectionDomain[] {

new ProtectionDomain(null, null) // No permissions

});

}

}

private static void evalScript(final String firstName)

throws ScriptException {

ScriptEngineManager manager = new ScriptEngineManager();

final ScriptEngine engine =

manager.getEngineByName("javascript");

// Restrict permission using the two-argument

// form of doPrivileged()

try {

AccessController.doPrivileged(

new PrivilegedExceptionAction<Object>() {

public Object run() throws ScriptException {

engine.eval("print('" + firstName + "')");

return null;

}

},

// From nested class

RestrictedAccessControlContext.INSTANCE);

} catch (PrivilegedActionException pae) {

// Handle error

}

}

}

将这个方法和白名单结合在一起使用,可以获得更高的安全性。

适用性

未能防止代码注入可能导致任意代码的执行。

参考文献

[API 2013]   javax.script包

[Long 2012]   IDS00-J.Sanitize untrusted data passed across a trust boundary

[OWASP 2013]  Code Injection in Java

指南8:防止XPath注入

可扩展标记语言(Extensible Markup Language,XML)可用于以类似于关系数据库的方式来存储数据。XML文档中的数据通常是用XPath来检索。当给XPath检索例程提供的数据没有做合适的无害化处理时,就有可能会导致XPath注入(XPath injection)攻击。这种攻击类似于SQL注入或XML注入(参见《The CERT®Oracle® Secure Coding Standard for Java™》[Long 2012]的“IDS00-J. Sanitize untrusted data passed across a trust boundary”。攻击者可以在查询用的数据字段中输入有效的SQL构造或XML 构造。典型的攻击是,让条件查询字段解析为一个永真式,这样就会导致攻击者访问到未经授权的信息。

该指南是指南7的一个具体示例。

XML路径注入示例

先来看看下面的XML模式。

<users>

<user>

<username>Utah</username>

<password>e90205372a3b89e2</password>

</user>

<user>

<username>Bohdi</username>

<password>6c16b22029df4ec6</password>

</user>

<user>

<username>Busey</username>

<password>ad39b3c2a4dabc98</password>

</user>

</users>

密码已被散列加密,这符合指南13。出于演示目的,这里的密码就用MD5散列算法加密;在实践中,应该使用SHA-256这样的更安全的算法。

不可信的代码可能会尝试在用户输入中动态构造XPath语句,然后利用这个语句从XML文件中检索出用户的详细信息。

//users/user[username/text()='&LOGIN&' and

password/text()='&PASSWORD&' ]

如果攻击者知道Utah是一个有效的用户名,他可以指定一个下面这样的输入:

Utah' or '1'='1

这样就构造出以下查询字符串:

//users/user[username/text()='Utah' or '1'='1'

and password/text()='xxxx']

因为'1'='1'自动为真,所以密码永远也不会被检查。因此,攻击者在不知道用户Utah的密码的情况下被不适当地验证成了该用户。

违规代码示例

下面的违规代码示例从用户输入中读取用户名和密码,并使用它们来构建查询字符串,将密码以字符数组的形式传递,然后对其进行散列加密。这个示例容易受到上面提到的那种方式的攻击。如果将上面描述的攻击字符串传递给evaluate()方法,这个方法调用会返回XML文件中的相应节点,这会导致doLogin()方法返回true,并绕过所有授权。

private boolean doLogin(String userName, char[] password)

throws ParserConfigurationException, SAXException,

IOException, XPathExpressionException {

DocumentBuilderFactory domFactory =

DocumentBuilderFactory.newInstance();

domFactory.setNamespaceAware(true);

DocumentBuilder builder = domFactory.newDocumentBuilder();

Document doc = builder.parse("users.xml");

String pwd = hashPassword( password);

XPathFactory factory = XPathFactory.newInstance();

XPath xpath = factory.newXPath();

XPathExpression expr =

xpath.compile("//users/user[username/text()='" +

userName + "' and password/text()='" + pwd + "' ]");

Object result = expr.evaluate(doc, XPathConstants.NODESET);

NodeList nodes = (NodeList) result;

// Print first names to the console

for (int i = 0; i < nodes.getLength(); i++) {

Node node =

nodes.item(i).getChildNodes().item(1).

getChildNodes().item(0);

System.out.println(

"Authenticated: " + node.getNodeValue()

);

}

return (nodes.getLength() >= 1);

}

合规解决方案(XQuery)

为了防止XPath注入,可以采用类似于防止SQL注入的方式。

将所有的用户输入视为不可信,并执行适当的无害化处理。

对用户输入进行无害化处理时,验证数据类型、数据长度、数据格式和数据内容的正确性。例如,使用一个正则表达式检查用户输入中是否包含 XML标签和特殊字符。这种做法符合对用户输入进行无害化处理的规范。更多细节参见指南7。

在客户端-服务器(client-server,CS)应用程序中,既执行客户端验证,也执行服务器端验证。

广泛地测试用于提供、传播或接受用户输入的应用程序。

一种有效防止SQL注入相关问题的技术是参数化。参数化能确保将用户指定的数据以参数的形式传递给API,这样数据就不会被解释为可执行内容了。遗憾的是,       Java SE目前缺乏一个类似于XPath查询的接口。不过,通过使用XQuery这样的接口,XPath可以模拟SQL参数化。XQuery支持将查询语句写入运行时环境中的一个单独文件中。

输入文件:login.xq

declare variable $userName as xs:string external;

declare variable $password as xs:string external;

//users/user[@userName=$userName and @password=$password]

下面的合规解决方案从一个文本文件中读取所需的特定格式的查询语句,然后将用户名和密码的值插入一个映射中。XQuery库构造了这些来自用户输入的XML查询。

private boolean doLogin(String userName, String pwd)

throws ParserConfigurationException, SAXException,

IOException, XPathExpressionException {

DocumentBuilderFactory domFactory =

DocumentBuilderFactory.newInstance();

domFactory.setNamespaceAware(true);

DocumentBuilder builder = domFactory.newDocumentBuilder();

Document doc = builder.parse("users.xml");

XQuery xquery =

new XQueryFactory().createXQuery(new File("login.xq"));

Map queryVars = new HashMap();

queryVars.put("userName", userName);

queryVars.put("password", pwd);

NodeList nodes =

xquery.execute(doc, null, queryVars).toNodes();

// Print first names to the console

for (int i = 0; i < nodes.getLength(); i++) {

Node node =

nodes.item(i).getChildNodes().item(1).

getChildNodes().item(0);

System.out.println(node.getNodeValue());

}

return (nodes.getLength() >= 1);

}

使用这种方法,用户名(userName)和密码(password)字段中输入的数据不会被运行时环境解释为可执行的内容。

适用性

未能验证用户输入可能会导致信息披露和未经授权代码的执行。

根据OWASP [OWASP 2013]:

(防止XPath注入)需要被删除(即禁止)或适当转义的字符如下。

< > / ' = "可用于防止直接的参数注入。

XPath查询不应该包含任何元字符(如'、=、*、?、//或类似字符)。

XSLT扩展不应该包含任何用户输入,如果包含了,你应该全面测试文件的存在,并确保文件在Java 2安全策略设定的范围内。

参考文献

[Fortify 2013]  “Input Validation and Representation:XML Injection”

[Long 2012]   IDS00-J.Sanitize untrusted data passed across a trust boundary

[OWASP 2013]  Testing for XPath Injection

[Sen 2007]   Avoid the Dangers of XPath Injection

[Oracle 2011b]  Ensure Data Security

指南9:防止LDAP注入

轻量级目录访问协议(Lightweight Directory Access Protocol,LDAP)允许应用程序执行远程操作,如在目录中搜索和修改记录。不足的输入处理(sanitization)和验证会导致LDAP注入攻击,并允许恶意用户通过使用目录服务来收集被限制的信息。

白名单可以用来限制用户输入,使其符合一个有效的字符列表。禁止加入白名单的字符或字符序列包括:Java命名和目录接口(Java Naming and Directory Interface,       JNDI)的元字符和LDAP特殊字符,这些字符都列在了表1-1中。

续表

注:*这是一个字符序列。

LDAP注入示例

试想一个LDAP数据交换格式(LDAP Data Interchange Format,LDIF)文件,其中包含的记录格式如下:

dn: dc=example,dc=com

objectclass: dcobject

objectClass: organization

o: Some Name

dc: example

dn: ou=People,dc=example,dc=com

ou: People

objectClass: dcobject

objectClass: organizationalUnit

dc: example

dn: cn=Manager,ou=People,dc=example,dc=com

cn: Manager

sn: John Watson

# Several objectClass definitions here (omitted)

userPassword: secret1

mail: john@holmesassociates.com

dn: cn=Senior Manager,ou=People,dc=example,dc=com

cn: Senior Manager

sn: Sherlock Holmes

# Several objectClass definitions here (omitted)

userPassword: secret2

mail: sherlock@holmesassociates.com

并且对有效用户名和密码的搜索经常使用下面这种形式:

(&(sn=<USERSN>)(userPassword=<USERPASSWORD>))

然而,攻击者可以通过在USERSN字段中输入S*、在USERPASSWORD字段中输入*来绕过身份验证。这样的输入将会使所有在USERSN字段中以S开头的记录被悉数查出。

如果验证例程存在LDAP注入风险,就会导致未经验证的用户登录。同样地,有类似问题的搜索例程就会将该目录下部分甚至全部数据暴露给攻击者。

违规代码示例

下面的违规代码示例使用LDAP协议来允许调用者通过searchRecord()方法来搜索目录中的记录。匹配到调用者提供的用户名和密码后,字符串过滤器将会从结果集中过滤出匹配的结果。

// String userSN = "S*"; // Invalid

// String userPassword = "*"; // Invalid

public class LDAPInjection {

private void searchRecord(String userSN, String userPassword)

throws NamingException {

Hashtable<String, String> env =

new Hashtable<String, String>();

env.put(Context.INITIAL_CONTEXT_FACTORY,

"com.sun.jndi.ldap.LdapCtxFactory");

try {

DirContext dctx = new InitialDirContext(env);

SearchControls sc = new SearchControls();

String[] attributeFilter = {"cn", "mail"};

sc.setReturningAttributes(attributeFilter);

sc.setSearchScope(SearchControls.SUBTREE_SCOPE);

String base = "dc=example,dc=com";

// The following resolves to (&(sn=S*)(userPassword=*))

String filter = "(&(sn=" + userSN + ")(userPassword=" +

userPassword + "))";

NamingEnumeration<?> results =

dctx.search(base, filter, sc);

while (results.hasMore()) {

SearchResult sr = (SearchResult) results.next();

Attributes attrs = (Attributes) sr.getAttributes();

Attribute attr = (Attribute) attrs.get("cn");

System.out.println(attr);

attr = (Attribute) attrs.get("mail");

System.out.println(attr);

}

dctx.close();

} catch (NamingException e) {

// Forward to handler

}

}

}

当恶意用户输入别有用心的值时,正如前面提到的,这个基本的身份验证方案没能对需要用户访问特权的信息查询作出限制。

合规解决方案

下面的合规解决方案使用白名单对用户输入进行无害化处理,使得filter字符串只包含有效字符。在这段代码中,userSN只可能包含字母和空格,而密码则只可能包含字母数字字符。

// String userSN = "Sherlock Holmes"; // Valid

// String userPassword = "secret2"; // Valid

// ... beginning of LDAPInjection.searchRecord() ...

sc.setSearchScope(SearchControls.SUBTREE_SCOPE);

String base = "dc=example,dc=com";

if (!userSN.matches("[\\w\\s]*") ||

!userPassword.matches("[\\w]*")) {

throw new IllegalArgumentException("Invalid input");

}

String filter = "(&(sn = " + userSN + ")(userPassword=" +

userPassword + "))";

// ... remainder of LDAPInjection.searchRecord() ...

当密码这样的数据库字段必须包含特殊字符时,至关重要的是,确保可信的数据以无害的形式存储在数据库中,并且在验证或比较用户输入前必须对其进行标准化。在缺乏广泛的标准化和白名单过滤的情况下,使用在JNDI和LDAP中有特殊含义字符的程序是极不安全的。特殊字符在被添加到白名单验证之前必须被转换为清洁的安全值。就像用户输入在被验证之前首先要被标准化一样。

适用性

未能对不可信输入做无害化处理可能导致信息泄露和特权升级。

参考文献

[OWASP 2013]  Preventing LDAP Injection in Java

指南10:不要使用clone()方法来复制不可信的方法参数

创建可变方法参数的防御性副本,可以减轻来自各种安全漏洞的威胁,更多信息请参考《The CERT®Oracle®Secure Coding Standard for Java™》[Long 2012]的“OBJ06-J. Defensively copy mutable inputs and mutable internal components”。然而,对clone()方法不当地使用,可以使攻击者利用这一漏洞,提供看上去正常的参数,但随后返回意想不到的结果。这样的对象可能因此绕过验证和安全检查。当这样一个类可能会作为一个参数传递给一个方法时,应当把这个参数视为不可信任的,同时不要使用该类提供的clone()方法。另外,不要使用未经final修饰的类的clone()方法来创建防御性副本。

该指南是指南15的一个特定实例。

违规代码示例

下面的违规代码示例定义了一个validateValue()方法来验证时间值。

private Boolean validateValue(long time) {

// Perform validation

return true; // If the time is valid

}

private void storeDateInDB(java.util.Date date)

throws SQLException {

final java.util.Date copy = (java.util.Date)date.clone();

if (validateValue(copy.getTime())) {

Connection con =

DriverManager.getConnection(

"jdbc:microsoft:sqlserver://<HOST>:1433",

"<UID>", "<PWD>"

);

PreparedStatement pstmt =

con.prepareStatement("UPDATE ACCESSDB SET TIME = ?");

pstmt.setLong(1, copy.getTime());

// ...

}

}

storeDateInDB()方法接受一个不可信的日期参数,尝试利用其 clone()方法创建一个防御性副本。这就允许攻击者通过一个继承自Date的恶意日期类来取得程序的控制权。如果攻击者的代码运行特权和storeDateInDB()方法一样,那么他只需在clone()方法里嵌入恶意代码:

class MaliciousDate extends java.util.Date {

@Override

public MaliciousDate clone() {

// malicious code goes here

}

}

然而,如果攻击者只能提供恶意的日期参数,但是没有足够特权,他还是可以绕过验证,从而混淆程序的其余部分。试想一下这个例子:

public class MaliciousDate extends java.util.Date {

private static int count = 0;

@Override

public long getTime() {

java.util.Date d = new java.util.Date();

return (count++ == 1) ? d.getTime() : d.getTime() - 1000;

}

}

getTime()方法第一次被调用时,这个恶意的日期看上去是一个正常的日期对象,这使得它绕过了storeDateInDB()里的验证方法。然而,实际存储在数据库中的时间却是不正确的。

合规解决方案

下面的合规解决方案避免了clone()方法的使用。取而代之,创建了一个新的java.util.Date对象,并在后续的代码中,使用该对象做访问控制检查和数据库插入。

private void storeDateInDB(java.util.Date date)

throws SQLException {

final java.util.Date copy = new java.util.Date(date.getTime());

if (validateValue(copy.getTime())) {

Connection con =

DriverManager.getConnection(

"jdbc:microsoft:sqlserver://<HOST>:1433",

"<UID>", "<PWD>"

);

PreparedStatement pstmt =

con.prepareStatement("UPDATE ACCESSDB SET TIME = ?");

pstmt.setLong(1, copy.getTime());

// ...

}

}

违规代码示例(CVE-2012-0507)

下面的违规代码示例展示了一个Java核心类AtomicReferenceArray的构造函数,它来自于Java 1.7.0版本的第2次更新。

public AtomicReferenceArray(E[] array) {

// Visibility guaranteed by final field guarantees

this.array = array.clone();

}

这段代码的调用导致漏洞被利用,在2012年4月影响了600 000台Macintosh电脑。[1]

合规解决方案(CVE-2012-0507)

在Java 1.7.0版本的第3次更新中,对上面提到的构造函数进行了修改,使用Arrays.copyOf()方法替换了clone()方法。

public AtomicReferenceArray(E[] array) {

// Visibility guaranteed by final field guarantees

this.array = Arrays.copyOf(

array, array.length, Object[].class);

}

适用性

使用clone()方法复制不可信的参数会给攻击者执行任意代码的机会。

参考文献

[Long 2012]   OBJ06-J. Defensively copy mutable inputs and mutable internal components

[Sterbenz 2006]  Secure Coding Antipatterns:Avoiding Vulnerabilities

指南11:不要使用Object.equals()来比较密钥

java.lang.Object.equals()方法,在默认情况下是无法比较复合对象(如密钥)的。大多数Key类都没能提供覆盖Object.equals()方法的equals()实现。在这种情况下,复合对象的组件必须单独进行比较,以确保正确性。

违规代码示例

下面的违规代码示例使用 equals()方法比较两个密钥。即使它们代表相同的值,也有可能被视为不相等。

private static boolean keysEqual(Key key1, Key key2) {

if (key1.equals(key2)) {

return true;

}

return false;

}

合规解决方案

下面的合规解决方案首先使用equals()方法进行检查,如果不相等,则继续比较密钥的编码版本,这可以使密钥比对逻辑和密钥提供方实现逻辑解耦。如果还不相等,则进一步比较RSAPrivateKey和RSAPrivateCrtKey是否表示同一个私钥[Oracle 2011b]。上述三步中,任何一步相等,都直接返回相等。

private static boolean keysEqual(Key key1, Key key2) {

if (key1.equals(key2)) {

return true;

}

if (Arrays.equals(key1.getEncoded(), key2.getEncoded())) {

return true;

}

// More code for different types of keys here

// For example, the following code can check whether

// an RSAPrivateKey and an RSAPrivateCrtKey are equal

if ((key1 instanceof RSAPrivateKey) &&

(key2 instanceof RSAPrivateKey)) {

if ((((RSAKey) key1).getModulus().equals(

((RSAKey) key2).getModulus())) &&

(((RSAPrivateKey) key1).getPrivateExponent().equals(

((RSAPrivateKey) key2).getPrivateExponent()))) {

return true;

}

}

return false;

}

自动检测

使用Object.equals()比较密钥可能会产生意想不到的结果。

参考文献

[API 2013]   java.lang.Object.equals(),Object.equals()

[Oracle 2011b]  Determining If Two Keys Are Equal(JCA Reference Guide)

指南12:不要使用不安全的弱加密算法

安全敏感的应用程序必须避免使用不安全的弱加密方式。现代计算机的计算能力允许通过蛮干攻击破解这样的加密。例如,数据加密标准(Data Encryption Standard,       DES)加密算法被认为是很不安全的;使用DES加密的消息,能够在一天之内被机器(如Electronic Frontier Foundation——简称EFF——的Deep Crack)蛮干攻击破解。

违规代码示例

下面的违规代码示例使用了一种弱加密算法(DES)来加密一个字符串输入。

SecretKey key = KeyGenerator.getInstance("DES").generateKey();

Cipher cipher = Cipher.getInstance("DES");

cipher.init(Cipher.ENCRYPT_MODE, key);

// Encode bytes as UTF8; strToBeEncrypted contains

// the input string that is to be encrypted

byte[] encoded = strToBeEncrypted.getBytes("UTF8");

// Perform encryption

byte[] encrypted = cipher.doFinal(encoded);

合规解决方案

下面的合规解决方案使用更安全的高级加密标准(Advanced Encryption Standard,       AES)算法执行加密。

Cipher cipher = Cipher.getInstance("AES");

KeyGenerator kgen = KeyGenerator.getInstance("AES");

kgen.init(128); // 192 and 256 bits may be unavailable

SecretKey skey = kgen.generateKey();

byte[] raw = skey.getEncoded();

SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");

cipher.init(Cipher.ENCRYPT_MODE, skeySpec);

// Encode bytes as UTF8; strToBeEncrypted contains the

// input string that is to be encrypted

byte[] encoded = strToBeEncrypted.getBytes("UTF8");

// Perform encryption

byte[] encrypted = cipher.doFinal(encoded);

适用性

使用数学和计算上不安全的加密算法会导致敏感信息的泄露。

在Java SE 7中,弱加密算法可以被禁用;参见Java™ PKI Programmer’s Guide的附录D“Disabling Cryptographic Algorithms”[Oracle 2011a]。

弱加密算法可用于需要易破解的密文的场景 。例如,ROT13密文经常用在论坛和网站,这个加密的目的是保护人们免受信息干扰,而不是保护信息免受他人获取。

参考文献

[Oracle 2011a]  Appendix D, “Disabling Cryptographic Algorithms”

[Oracle 2013b]  Java Cryptography Architecture(JCA)Reference Guide

指南13:使用散列函数存储密码

程序以明文(未加密的文本数据)方式存储密码将会导致密码以多种方式被泄露的风险。尽管程序通常接收到的用户密码是明文的,但是程序应该确保密码不以明文方式存储。

一种可行的限制密码暴露的技术是使用散列函数(hash function),它允许程序间接地比较输入的密码和原始密码,而不需要以明文或可解密的方式存储密码字符串。这种方法最大限度地减少了密码的暴露,而且没有表现出任何实用上的缺点。

加密散列函数

散列函数产生的值是散列值(hash value)或消息摘要(message digest)。散列加密过程在计算上可行,而它的逆向过程在计算上是不可行的。在实践中,明文密码可以编码为散列值,但是散列值不能解码为明文。要比较两个密码是否相等,需要对比它们的散列值是否相等。

总是为需要加密的密码添加一个盐(salt)是一个好的实践。盐是一个唯一的(通常是连续的)或随机生成的数据,与散列值存储在一起。使用盐有助于防止对散列值的蛮干攻击,提供的盐需要足够长,这样才能生成足够的熵(盐值过短不能显著缓解蛮干攻击)。每个密码都应该有自己的盐与之关联。如果一个盐被用于多个密码,那么两个用户将能够看到他们的密码是否相同。

对散列函数和盐的长度的选择,需要在安全性和性能之间做出权衡。通过选择一个更强大的散列函数来增加有效蛮干攻击所需努力的同时,也增加了验证密码所需的时间。虽然增加盐的长度可使蛮干攻击更为困难,但是却需要占用额外的存储空间。

Java的MessageDigest类提供了各种加密散列函数的实现。要避免使用有缺陷的函数,如消息摘要算法(Message-Digest Algorithm,MD5)。安全散列算法SHA-1和SHA-2是由美国国家安全局维护的,目前它们被认为是安全的散列函数。在实践中,许多应用程序使用SHA-256,因为这个散列函数在被认为是安全的同时,还具有合理的性能。

违规代码示例

下面的违规代码示例使用对称密钥算法来加密和解密存储在password.bin中的密码。

public final class Password {

private void setPassword(byte[] pass) throws Exception {

// Arbitrary encryption scheme

bytes[] encrypted = encrypt(pass);

clearArray(pass);

// Encrypted password to password.bin

saveBytes(encrypted,"password.bin");

clearArray(encrypted);

}

boolean checkPassword(byte[] pass) throws Exception {

// Load the encrypted password

byte[] encrypted = loadBytes("password.bin");

byte[] decrypted = decrypt(encrypted);

boolean arraysEqual = Arrays.equal(decrypted, pass);

clearArray(decrypted);

clearArray(pass);

return arraysEqual;

}

private void clearArray(byte[] a) {

for (int i = 0; i < a.length; i++) {

a[i] = 0;

}

}

}

攻击者可能会解密这个文件,从而发现密码,特别是当攻击者知道程序是如何使用这个密钥和加密方案的时候。任何人都不应该看到明文密码,即使是系统管理员和特权用户也不应该。因此,使用加密只是部分有效地减轻密码泄露的威胁。

违规代码示例

下面的违规代码示例虽然是通过使用MessageDigest类的SHA-256散列函数对密码的散列值进行的比较,没有使用明文字符串,但是它却使用了字符串来存储密码。

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

public final class Password {

private void setPassword(String pass) throws Exception {

byte[] salt = generateSalt(12);

MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");

// Encode the string and salt

byte[] hashVal = msgDigest.digest((pass+salt).getBytes());

saveBytes(salt, "salt.bin");

// Save the hash value to password.bin

saveBytes(hashVal,"password.bin");

}

boolean checkPassword(String pass) throws Exception {

byte[] salt = loadBytes("salt.bin");

MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");

// Encode the string and salt

byte[] hashVal1 = msgDigest.digest((pass+salt).getBytes());

// Load the hash value stored in password.bin

byte[] hashVal2 = loadBytes("password.bin");

return Arrays.equals(hashVal1, hashVal2);

}

private byte[] generateSalt(int n) {

// Generate a random byte array of length n

}

}

即使攻击者知道程序在存储密码时使用的是SHA-256算法和一个12字节的盐,他也无法从password.bin和salt.bin中获取实际的密码。

尽管这种方式解决了上一个违规代码示例中的解密问题,但是这个程序可能无意中就将密码明文存储在了内存中。Java的字符串对象是不可变的,可以被Java虚拟机复制和存储在其内部。因此,Java缺乏一种用于安全删除存储在字符串中的密码的机制。更多相关信息参见指南1。

合规解决方案

下面的合规解决方案通过使用字节数组来存储密码,解决了上一个违规代码示例中的问题。

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

public final class Password {

private void setPassword(byte[] pass) throws Exception {

byte[] salt = generateSalt(12);

byte[] input = appendArrays(pass, salt);

MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");

// Encode the string and salt

byte[] hashVal = msgDigest.digest(input);

clearArray(pass);

clearArray(input);

saveBytes(salt, "salt.bin");

// Save the hash value to password.bin

saveBytes(hashVal,"password.bin");

clearArray(salt);

clearArray(hashVal);

}

boolean checkPassword(byte[] pass) throws Exception {

byte[] salt = loadBytes("salt.bin");

byte[] input = appendArrays(pass, salt);

MessageDigest msgDigest = MessageDigest.getInstance("SHA-256");

// Encode the string and salt

byte[] hashVal1 = msgDigest.digest(input);

clearArray(pass);

clearArray(input);

// Load the hash value stored in password.bin

byte[] hashVal2 = loadBytes("password.bin");

boolean arraysEqual = Arrays.equals(hashVal1, hashVal2);

clearArray(hashVal1);

clearArray(hashVal2);

return arraysEqual;

}

private byte[] generateSalt(int n) {

// Generate a random byte array of length n

}

private byte[] appendArrays(byte[] a, byte[] b) {

// Return a new array of a[] appended to b[]

}

private void clearArray(byte[] a) {

for (int i = 0; i < a.length; i++) {

a[i] = 0;

}

}

}

在setPassword()和checkPassword()方法,密码的明文表示,在转化为散列值后会被立即擦除。因此,在密码明文被擦除后,攻击者想要获取密码将会变得更加困难。确保数据的完全擦除是极具挑战性的,很可能是与平台有关的,甚至是不可能的——因为需要复制垃圾收集器、进行动态分页,还涉及Java语言层级以下的其他平台相关的操作。

适用性

没有经过安全散列加密的密码将会暴露给恶意用户。违反这条指南,通常会导致与之相关联的明确的漏洞。

应用程序(如密码管理器)可能需要检索出原始密码并输入一个第三方的应用程序中。这样做即使违背了这条指南,也是被允许的。密码管理器是供单个用户访问的,始终拥有存储用户密码和用命令显示那些密码的权限。因此,安全保障的限制因素是用户的能力,而不是程序的操作。

参考文献

[API 2013]     ClassMessageDigest,ClassString

[Hirondelle 2013]   Passwords Never Clear in Text

[OWASP 2012]   “Why Add Salt?”

[Paar 2010]     Chapter 11,“Hash Functions”

指南14:确保SecureRandom正确地选择随机数种子

随机数的生成取决于熵的来源,如信号、设备或硬件输入等。《The CERT®Oracle® Secure Coding Standard for Java™》[Long 2012]的“MSC02-J. Generate strong random numbers”解决了安全随机数的生成。

java.security.SecureRandom类被广泛用于生成密码强随机数。根据Java运行时环境的lib/security文件夹中,java.security文件的描述[API 2013]:

为 SecureRandom 种子数据选择来源。默认情况下尝试使用securerandom. source属性所指定的熵收集装置。如果访问URL时发生异常,那么传统的系统/线程活动算法将会派上用场。

在Solaris和Linux系统中,如果file:/dev/urandom被指定且是存在的,那么会默认激活一个特殊的SecureRandom实现。这种“NativePRNG”直接从/dev/urandom读取随机数字节。在Windows系统中,file:/dev/random和file:/dev/urandom这个URL启用了微软CryptoAPI种子功能。

攻击者不应该能够从给出的几个随机数字的样本中确定原始随机种子。如果违反了这一限制,之后所有随机数都可以被攻击者成功地预测出来。

违规代码示例

下面的违规代码示例通过具有指定种子字节的种子构造了一个安全的随机数生成器。

SecureRandom random = new SecureRandom(

String.valueOf(new Date().getTime()).getBytes()

);

这个构造函数搜索注册的安全提供程序并返回第一个支持安全随机数生成的提供程序。如果不存在这样的提供程序,那么会选择一个默认的特定实现。此外,系统提供的默认种子会被程序员提供的种子所覆盖。使用当前系统时间作为种子是可以被预测的,会导致使用不足够的熵来产生随机数。

合规解决方案

使用SecureRandom类的无参构造函数更好,该函数使用了特定于系统的种子值来生成128字节长的随机数。

byte[] randomBytes = new byte[128];

SecureRandom random = new SecureRandom();

random.nextBytes(randomBytes);

指定确切的随机数生成器和提供者也是一个好的实践,这样会带来更好的可移植性。

适用性

不够安全的随机数会使得攻击者能够在它们被使用的上下文中获得特定的信息。

不安全的随机数在一些没有安全要求的情况下也是有用的。这些随机数在“MSC02-J. Generate strong random numbers”[Long 2012]的一些例外中有提到。

参考文献

[API 2013]   SecureRandom

[Sethi 2009]   Proper Use of Java’s SecureRandom

[Long 2012]   MSC02-J.Generate strong random numbers

指南15:不要依赖可以被不可信代码覆盖的方法

不可信代码可以滥用可信代码提供的API来覆盖一些方法,如Object.equals()、Object. hashCode()和Thread.run()。这些方法是很重要的目标,因为它们通常是在幕后被使用,很可能以不容易辨别的方式与其他组件进行交互。

通过提供覆盖的实现,攻击者可以使用不可信的代码来收集敏感信息、运行任意代码,或者发起拒绝服务攻击。

关于覆盖Object.clone()方法的更多详细信息参见指南10。

违规代码示例(hashCode)

下面的违规代码示例展示了一个 LicenseManager 类,它维持着一个licenseMap。这个映射存储的是许可证类型(LicenseType)和许可证值对。

public class LicenseManager {

Map<LicenseType, String> licenseMap =

new HashMap<LicenseType, String>();

public LicenseManager() {

LicenseType type = new LicenseType();

type.setType("demo-license-key");

licenseMap.put(type, "ABC-DEF-PQR-XYZ");

}

public Object getLicenseKey(LicenseType licenseType) {

return licenseMap.get(licenseType);

}

public void setLicenseKey(LicenseType licenseType,

String licenseKey) {

licenseMap.put(licenseType, licenseKey);

}

}

class LicenseType {

private String type;

public String getType() {

return type;

}

public void setType(String type) {

this.type = type;

}

@Override

public int hashCode() {

int res = 17;

res = res * 31 + type == null ? 0 : type.hashCode();

return res;

}

@Override

public boolean equals(Object arg) {

if (arg == null || !(arg instanceof LicenseType)) {

return false;

}

if (type.equals(((LicenseType) arg).getType())) {

return true;

}

return false;

}

}

LicenseManager的构造函数用必须保持密文的演示许可证密钥,对licenseMap进行了初始化。为便于说明,许可证密钥是硬编码的;理想情况下应该从外部配置文件中读取经加密后存储的密钥。LicenseType类提供了equals()方法和hashCode()方法的覆盖实现。

这个实现是易受攻击的,攻击者可以扩展LicenseType类并覆盖equals()方法和hashCode()方法:

public class CraftedLicenseType extends LicenseType {

private static int guessedHashCode = 0;

@Override

public int hashCode() {

// Returns a new hashCode to test every time get() is called

guessedHashCode++;

return guessedHashCode;

}

@Override

public boolean equals(Object arg) {

// Always returns true

return true;

}

}

下面是恶意的客户端程序:

public class DemoClient {

public static void main(String[] args) {

LicenseManager licenseManager = new LicenseManager();

for (int i = 0; i <= Integer.MAX_VALUE; i++) {

Object guessed =

licenseManager.getLicenseKey(new CraftedLicenseType());

if (guessed != null) {

// prints ABC-DEF-PQR-XYZ

System.out.println(guessed);

}

}

}

}

客户端程序使用CraftedLicenseType类遍历所有可能的散列码序列,直到它成功匹配到存储在LicenseManager类中的演示许可证密钥对象的散列码。因此,仅仅只需几分钟,攻击者就可以发现licenseMap中的敏感数据。这个攻击是通过发现至少一个关于映射中键的散列冲突进行的。

合规解决方案(IdentityHashMap)

下面的合规解决方案使用了一个IdentityHashMap来存储许可证信息,而不是HashMap。

public class LicenseManager {

Map<LicenseType, String> licenseMap =

new IdentityHashMap<LicenseType, String>();

// ...

}

根据Java API中IdentityHashMap类的文档[API 2006]:

这个类以一个散列表实现Map(映射)接口,在比较键(和值)时使用引用相等代替对象相等。换句话说,如果在一个IdentityHashMap中有k1和k2两个键,那么当且仅当(k1==k2)时,才可以说它们是相等的。(而对于普通的Map实现(如HashMap)中的两个键k1和k2,当且仅当(k1==null ? k2==null : k1.equals(k2))时,才可以说它们是相等的。)

因此,覆盖方法不能暴露内部类的细节。客户端程序可以继续添加许可证密钥,甚至可以检索添加的键值对,如下列客户端代码所示。

public class DemoClient {

public static void main(String[] args) {

LicenseManager licenseManager = new LicenseManager();

LicenseType type = new LicenseType();

type.setType("custom-license-key");

licenseManager.setLicenseKey(type, "CUS-TOM-LIC-KEY");

Object licenseKeyValue = licenseManager.getLicenseKey(type);

// Prints CUS-TOM-LIC-KEY

System.out.println(licenseKeyValue);

}

}

合规解决方案(final类)

下面的合规解决方案将LicenseType类用final关键字声明成了不可更改的类,这样它的所有方法就都不能被覆盖了。

final class LicenseType {

// ...

}

违规代码示例

下面的违规代码示例包含一个Widget类和一个含有一组部件的LayoutManager类。

public class Widget {

private int noOfComponents;

public Widget(int noOfComponents) {

this.noOfComponents = noOfComponents;

}

public int getNoOfComponents() {

return noOfComponents;

}

public final void setNoOfComponents(int noOfComponents) {

this.noOfComponents = noOfComponents;

}

public boolean equals(Object o) {

if (o == null || !(o instanceof Widget)) {

return false;

}

Widget widget = (Widget) o;

return this.noOfComponents == widget.getNoOfComponents();

}

@Override

public int hashCode() {

int res = 31;

res = res * 17 + noOfComponents;

return res;

}

}

public class LayoutManager {

private Set<Widget> layouts = new HashSet<Widget>();

public void addWidget(Widget widget) {

if (!layouts.contains(widget)) {

layouts.add(widget);

}

}

public int getLayoutSize() {

return layouts.size();

}

}

攻击者可以用Navigator部件扩展Widget类,并覆盖hashCode()方法:

public class Navigator extends Widget {

public Navigator(int noOfComponents) {

super(noOfComponents);

}

@Override

public int hashCode() {

int res = 31;

res = res * 17;

return res;

}

}

客户端代码如下:

Widget nav = new Navigator(1);

Widget widget = new Widget(1);

LayoutManager manager = new LayoutManager();

manager.addWidget(nav);

manager.addWidget(widget);

System.out.println(manager.getLayoutSize()); // Prints 2

layouts(布局)集合本应只包含一个条目,因为被添加的Navigator和Widget的组件数量都是1。然而,getLayoutSize()方法确返回了2。

产生这种差异的原因是,Widget的hashCode()方法只在Widget对象被添加到集合中时使用了一次。当添加Navigator时,集合使用的是Navigator类提供的hashCode()方法。因此,集合中包含两个不同的对象实例。

合规解决方案(final类)

下面的合规解决方案将Widget类声明成final类,这样它的方法就不能被覆盖了。

public final class Widget {

// ...

}

违规代码示例(run())

在下面的违规代码示例中,Worker类及其子类SubWorker,均包含一个用来启动一个线程的startThread()方法。

public class Worker implements Runnable {

Worker() { }

public void startThread(String name) {

new Thread(this, name).start();

}

@Override

public void run() {

System.out.println("Parent");

}

}

public class SubWorker extends Worker {

@Override

public void startThread(String name) {

super.startThread(name);

new Thread(this, name).start();

}

@Override

public void run() {

System.out.println("Child");

}

}

如果一个客户端运行下面的代码:

Worker w = new SubWorker();

w.startThread("thread");

客户端可能会希望Parent和Child都被打印出来。然而,Child会被打印两次,因为被覆盖的方法run()在启动一个新线程时被调用了两次。

合规解决方案

下面的合规解决方案修改了SubWorkder类,移除了对super.startThread()的调用。

public class SubWorker extends Worker {

@Override

public void startThread(String name) {

new Thread(this, name).start();

}

// ...

}

对客户端代码也做了修改,单独开启父线程和子线程。这个程序将会产生预期的输出:

Worker w1 = new Worker();

w1.startThread("parent-thread");

Worker w2 = new SubWorker();

w2.startThread("child-thread");

参考文献

[API 2013]   Class IdentityHashMap

[Hawtin 2006]  [drlvm][kernel_classes]ThreadLocal vulnerability

指南16:避免授予过多特权

Java安全策略为代码授予权限,用来允许指定代码访问特定的系统资源。一个被授予许可的代码源(CodeSource类型的对象),是由代码位置(URL)和证书引用组成的,该证书包含公钥以及与之对应的私钥,用来对代码进行数字签名, 代码只有在被某证书数字签名之后,才能关联到该证书引用。代码只有在被某证书数字签名之后,才能关联到该证书引用。保护域(protection domain)包含一个CodeSource对象,以及CodeSource中的代码被授予的权限,这是由当前生效的安全策略所决定的。因此,用相同的密钥来进行签名的、来自相同 URL 的类,会被放置在相同的保护域中。一个类仅仅属于一个保护域。具有相同权限、但来自不同代码源的类,属于不同的保护域。

每个Java类都运行在由代码源决定的恰当的保护域里。运行在安全管理器之下的任何代码,在执行任何安全相关的操作时,都必须被授予特定的权限,如读或者写一个文件时必须要有执行文件读或者写的权限。通过使用 AccessController. doPrivileged()方法,特权代码可以代表无特权的调用者,访问特权资源,这是很有必要的。例如,当一个系统工具程序需要代表用户打开一个字体文件用来显示一个文档,但是应用程序本身缺乏权限做这样的事的时候。为了执行该操作,系统工具程序会使用它的全部特权来获取这个字体,而忽略调用者的特权。特权代码运行在与代码源相关的所有特权保护域里。这些特权往往超出了执行特权操作的需要。理想的情况下,代码应该被授予恰好满足其完成操作所需特权的最小集合。

指南19中描述了另外一种用来消除多余特权的方法。

违规代码示例

下面的违规代码示例显示了一个库方法,通过使用包装器方法performActionOnFile()来允许调用者执行授权操作(读文件)。

private FileInputStream openFile() {

final FileInputStream f[] = { null };

AccessController.doPrivileged(new PrivilegedAction() {

public Object run() {

try {

f[0] = new FileInputStream("file");

} catch(FileNotFoundException fnf) {

// Forward to handler

}

return null;

}

});

return f[0];

}

// Wrapper method

public void performActionOnFile() {

try (FileInputStream f = openFile()){

// Perform operation

} catch (Throwable t) {

// Handle exception

}

}

在这个例子中,对可信代码授予的特权超出了读取一个文件的真实需要,即便是需要读取文件,也只需要为 doPrivileged()代码块授权。因此,这段代码为代码块提供了多余的特权,从而违反了最小特权原则。

合规解决方案

双参数形式的doPrivileged()方法从调用者那里接受一个作为第二个参数传递的AccessControlContext对象,并将所包含代码的特权限制在保护域特权和上下文权限的交集中。因此,当调用者只希望授予代码读取文件权限时,可以提供一个只有文件读取权限的上下文。

一个被适当授予文件读取权限的AccessControlContext,可以作为一个内部类:

private FileInputStream openFile(AccessControlContext context) {

if (context == null) {

throw new SecurityException("Missing AccessControlContext");

}

final FileInputStream f[] = { null };

AccessController.doPrivileged(

new PrivilegedAction() {

public Object run() {

try {

f[0] = new FileInputStream("file");

} catch (FileNotFoundException fnf) {

// Forward to handler

}

return null;

}

},

// Restrict the privileges by passing the context argument

context);

return f[0];

}

private static class FileAccessControlContext {

public static final AccessControlContext INSTANCE;

static {

Permission perm = new java.io.FilePermission("file", "read");

PermissionCollection perms = perm.newPermissionCollection();

perms.add(perm);

INSTANCE = new AccessControlContext(new ProtectionDomain[] {

new ProtectionDomain(null, perms)});

}

}

// Wrapper method

public void performActionOnFile() {

try {

final FileInputStream f =

// Grant only open-for-reading privileges

openFile(FileAccessControlContext.INSTANCE);

// Perform action

} catch (Throwable t) {

// Handle exception

}

}

如果调用者缺乏创建一个适当的AccessControlContext的权限,那么可以通过请求AccessController.getContext()来创建一个这样的实例。

适用性

未能遵循最小特权原则可能导致不可信、未授权的代码执行意想不到的特权操作。然而,过细地限制特权会增加程序复杂性。这些增加的复杂性和相应减少的可维护性必须同安全改进做出利弊权衡。

参考文献

[API 2013]   Class AccessController

[Oracle 2013]  API for Privileged Blocks

指南17:最小化特权代码

程序必须符合最小特权原则,不仅为特权块提供正确操作所需的最低权限(参见指南 16),而且要确保特权代码只包含那些需要增加特权的操作。如果特权代码块里包含多余的代码,那么这些多余的代码就会具有与代码块相同的操作特权,这会增加攻击面。

违规代码示例

下面的违规代码示例包含一个 changePassword()方法,它试图在doPrivileged()代码块里打开一个密码文件,并使用该文件执行操作。doPrivileged()代码块还包含一个多余的代码调用 system.loadLibrary(),用来加载验证库。

public void changePassword(String currentPassword,

String newPassword) {

final FileInputStream f[] = { null };

AccessController.doPrivileged(new PrivilegedAction() {

public Object run() {

try {

String passwordFile = System.getProperty("user.dir") +

File.separator + "PasswordFileName";

f[0] = new FileInputStream(passwordFile);

// Check whether oldPassword matches the one in the file

// If not, throw an exception

System.loadLibrary("authentication");

} catch (FileNotFoundException cnf) {

// Forward to handler

}

return null;

}

}); // Forward to handler

}

这个示例违反了最小特权原则,因为未授权调用者也可能加载身份验证库。未授权调用者不能直接调用System.loadLibrary()方法,因为这可能会使本地方法暴露给无特权的代码[SCG 2010]。此外,System.loadLibrary()方法只检查它的直接调用者的特权,所以在使用它时要千万小心。更多相关信息参见指南18。

合规解决方案

下面的合规解决方案将对 System.loadLibrary()的调用挪到了doPrivileged()代码块以外。这样做就只允许无特权代码通过密码文件执行初步的密码重置检查,从而防止了身份验证库的加载。

public void changePassword(String currentPassword,

String newPassword) {

final FileInputStream f[] = { null };

AccessController.doPrivileged(new PrivilegedAction() {

public Object run() {

try {

String passwordFile = System.getProperty("user.dir") +

File.separator + "PasswordFileName";

f[0] = new FileInputStream(passwordFile);

// Check whether oldPassword matches the one in the file

// If not, throw an exception

} catch (FileNotFoundException cnf) {

// Forward to handler

}

return null;

}

}); // End of doPrivileged()

System.loadLibrary("authentication");

}

loadLibrary()调用也可以在初步密码重置检查之前执行;在本例中,由于性能原因它会有一定的延迟。

适用性

最小化特权代码将减小应用程序的攻击面、简化审核特权代码的任务。

参考文献

[API 2013]    Class AccessController

指南18:不要将使用降低安全性检查的方法暴露给不可信代码

大多数方法缺乏安全管理器检查,是因为它们不提供对系统敏感部分(如文件系统)的访问。大多数提供安全管理器检查的方法,都是在调用堆栈中的每个类和方法被执行之前进行身份验证。这个安全模型允许Java applet这样的受限制程序对核心Java库具有完全访问权限。它还可以防止敏感方法扮演成藏身于可信的调用堆栈中的恶意方法。

但是,某些方法使用了降低安全性检查,只检查正在调用的方法是否已授权,而不检查调用堆栈中的每一个方法。任何调用这些方法的代码必须保证它们不能代表不可信的代码。表1-2列出了这些方法。

因为方法java.lang.reflect.Field.setAccessible()和getAccessible()被用来通知Java虚拟机(JVM)进行覆盖语言访问检查,所以它们执行标准的(甚至更严格的)安全管理器检查,因此不会出现这条指南中描述的漏洞。然而,使用这些方法时也要倍加小心,其余set()*和get()*字段反射方法只执行语言访问检查,因此易受到攻击。

类加载器

类加载器允许Java应用程序通过加载额外的类而在运行时动态扩展。对于每个被加载的类,JVM都会跟踪用于加载该类的类加载器。当已加载的类第一次引用另一个类时,虚拟机请求使用该类的加载器来加载被引用的类。Java的类加载器架构通过使用不同的类加载器,来控制跟加载自不同来源的代码之间的交互。这种类加载器的分离是代码分离的基础:它可以防止恶意代码获取访问并破坏可信代码。

其中几个负责加载类的方法,将它们的工作委派给了被调用方法的类加载器。类加载器会执行与类的加载有关的安全检查。因此,任何调用其中的一个类加载方法的代码,必须保证这些方法不能代表不可信的代码。这些方法如表1-3所示。

除了loadLibrary()和load()方法,列表中其他方法不执行任何安全管理器检查;它们将安全检查委托给了适当的类加载器。

在实践中,可信代码的类加载器经常允许调用这些方法,而不可信代码的类加载器可能缺少这样的特权。然而,当不可信代码的类加载器委托给可信代码的类加载器时,可信代码对于不可信代码来说是可见的。在缺乏这样的委托关系时,类加载器会确保命名空间的分离,因此,不可信代码将无法观察属于可信代码的成员,也无法调用属于可信代码的方法。

类加载器委托模型是许多Java实现及框架的基础。要避免将表1-2和表1-3中列出的方法暴露给不可信的代码。例如,试想不可信代码试图加载一个特权类的攻击场景,如果它的类加载器自身缺少加载所请求的特权类的权限,但是,类加载器可以将类的加载委托给可信类的类加载器,那么就会发生特权升级。此外,如果可信代码接受被污染的输入,那么可信代码的类加载器就会代表不可信代码,加载恶意的特权类。

具有相同的类加载器定义的类,将会存在于相同的命名空间里,但根据安全策略的不同,它们可能具有不同的特权。当特权代码与同一个类加载器加载的无特权代码(或者更少特权的代码)共存时,就会出现安全漏洞。在这种情况下,更少特权的代码可以根据特权代码声明的可访问性,自由地访问特权代码的成员。使用上述表格中API的特权代码,能绕过安全管理器检查(loadLibrary()和load()方法除外)。

该指南类似于《The CERT®Oracle®Secure Coding Standard for Java™》[Long 2012]的“SEC03-J. Do not load trusted classes after allowing untrusted code to load arbitrary classes”。许多例子也违反“SEC00-J. Do not allow privileged blocks to leak sensitive information across a trust boundary”。

违规代码示例

下面的违规代码示例将System.loadLibrary()方法的调用嵌到了doPrivileged()语句块中。

public void load(String libName) {

AccessController.doPrivileged(new PrivilegedAction() {

public Object run() {

System.loadLibrary(libName);

return null;

}

});

}

这段代码是不安全的,因为它可以代表不可信代码来加载一个库。在本质上,不可信代码的类加载器可以使用这段代码来加载一个库,即使它缺乏足够的权限直接去加载。加载一个库后,不可信代码可以从该库中调用可供访问的本地方法,因为doPrivileged()语句块会妨碍安全管理器检查被应用到调用者进而执行堆栈。

非本地库的代码也容易受相关安全漏洞的影响。假设存在一个库,该库包含一个没有直接暴露的漏洞,也许就藏在一个未被使用的方法中。加载这个库可能也不会直接暴露该漏洞。然而,攻击者可以加载一个额外的库,攻破第一个库的漏洞。此外,非本地库经常使用doPrivileged语句块,这让它们成了有吸引力的攻击目标。

合规解决方案

下面的合规解决方案对代码库的名称进行了硬编码,防止了输入值被污染的可能性。它同时也减少了load()方法的可访问性,从public(公共)变为private(私有)。因此,不可信的调用者被禁止加载awt库。

private void load() {

AccessController.doPrivileged(new PrivilegedAction() {

public Object run() {

System.loadLibrary("awt");

return null;

}

});

}

违规代码示例

下面的违规代码示例将一个java.sql.Connection的实例从可信代码返回到不可信代码。

public Connection getConnection(String url, String username,

String password) {

// ...

return DriverManager.getConnection(url, username, password);

}

缺少创建SQL连接所需权限的不可信代码,可以通过使用直接获取的实例,绕过这些限制。getConnection()方法是不安全的,因为它使用url参数来指示要加载的类,这个类就是数据库驱动程序。

合规解决方案

下面的合规解决方案可以防止恶意用户提供他们自己的数据库连接URL,从而限制了它们加载不可信的驱动程序。

private String url = // Hardwired value

public Connection getConnection(String username,

String password) {

// ...

return DriverManager.getConnection(this.url,

username, password);

}

违规代码示例(CERT Vulnerability 636312)

CERT漏洞注解VU#636312描述了一个Java 1.7.0版本第6次更新中的漏洞,该漏洞在2012年8月被广泛利用。攻击程序实际上利用了两个漏洞,另一个的描述在《The CERT®Oracle®Secure Coding Standard for Java™》[Long 2012]的“SEC05-J. Do not use reflection to increase accessibility of classes, methods, or fields”里。

该攻击程序作为一个Java applet运行。applet的类加载器确保applet不能直接调用com.sun.*包中类的方法。一个正常的安全管理器检查,可以根据调用堆栈中所有调用者方法的特权(这些特权是和类的代码源相关联的),确定是允许还是拒绝特定动作。

攻击程序的第一个目标是访问私有的sun.awt.SunToolkit类。不过,用该类的名称直接调用class.forName()方法,将会导致抛出SecurityException异常。因此,攻击程序利用了下面的代码来访问任意类,绕过了安全管理器:

private Class GetClass(String paramString)

throws Throwable {

Object arrayOfObject[] = new Object[1];

arrayOfObject[0] = paramString;

Expression localExpression =

new Expression(Class.class, "forName", arrayOfObject);

localExpression.execute();

return (Class)localExpression.getValue();

}

java.beans.Expression.execute()方法将它的工作委托给了下面的方法:

private Object invokeInternal() throws Exception {

Object target = getTarget();

String methodName = getMethodName();

if (target == null || methodName == null) {

throw new NullPointerException(

(target == null ? "target" : "methodName") +

" should not be null");

}

Object[] arguments = getArguments();

if (arguments == null) {

arguments = emptyArray;

}

// Class.forName() won't load classes outside

// of core from a class inside core, so it

// is handled as a special case.

if (target == Class.class && methodName.equals("forName")) {

return ClassFinder.resolveClass((String)arguments[0],

this.loader);

}

// ...

com.sun.beans.finder.ClassFinder.resolveClass()方法将它工作委托给了它的findClass()方法:

public static Class<?> findClass(String name)

throws ClassNotFoundException {

try {

ClassLoader loader =

Thread.currentThread().getContextClassLoader();

if (loader == null) {

loader = ClassLoader.getSystemClassLoader();

}

if (loader != null) {

return Class.forName(name, false, loader);

}

} catch (ClassNotFoundException exception) {

// Use current class loader instead

} catch (SecurityException exception) {

// Use current class loader instead

}

return Class.forName(name);

}

虽然这个方法是在applet的上下文中调用的,但它还是使用了class.forName()来获取所请求的类。Class.forName()将该搜索委托给调用方法的类加载器。在这种情况下,调用类(com.sun.beans.finder.ClassFinder)是 Java 核心的一部分,因此,可信的类加载器代替了受更多限制的applet类加载器,同时可信的类加载器加载了所请求的类,它并不知道自己正在为恶意代码服务。

合规解决方案(CVE-2012-4681)

Oracle公司通过对 com.sun.beans.finder.ClassFinder.findClass()方法打补丁,在Java 1.7.0版本的第7次更新中缓解了这个漏洞。在下面这个实例中,       checkPackageAccess()方法检查整个调用堆栈,确保class.forName()只为可信代码获取类。

public static Class<?> findClass(String name)

throws ClassNotFoundException {

checkPackageAccess(name);

try {

ClassLoader loader =

Thread.currentThread().getContextClassLoader();

if (loader == null) {

// Can be null in IE (see 6204697)

loader = ClassLoader.getSystemClassLoader();

}

if (loader != null) {

return Class.forName(name, false, loader);

}

} catch (ClassNotFoundException exception) {

// Use current class loader instead

} catch (SecurityException exception) {

// Use current class loader instead

}

return Class.forName(name);

}

违规代码示例(CVE-2013-0422)

Java 1.7.0版本的第10次更新在2013年1月因为几个漏洞被广泛攻击。其中有一个这样的漏洞:com.sun.jmx.mbeanserver.MBeanInstantiator类给无特权代码授予了访问任何类的权限,不受当前安全策略或可访问性规则的限制。可以通过任意一个字符串来调用 MBeanInstantiator.findClass()方法,并尝试返回以该字符串命名的Class对象。这个方法将它的工作委派给了MBeanInstantiator. loadClass()方法,其源代码如下所示:

/**

* Load a class with the specified loader, or with this object

* class loader if the specified loader is null.

**/

static Class<?> loadClass(String className, ClassLoader loader)

throws ReflectionException {

Class<?> theClass;

if (className == null) {

throw new RuntimeOperationsException(

new IllegalArgumentException(

"The class name cannot be null"),

"Exception occurred during object instantiation");

} try {

if (loader == null) {

loader = MBeanInstantiator.class.getClassLoader();

}

if (loader != null) {

theClass = Class.forName(className, false, loader);

} else {

theClass = Class.forName(className);

}

} catch (ClassNotFoundException e) {

throw new ReflectionException(

e, "The MBean class could not be loaded");

}

return theClass;

}

这个方法将动态加载指定类的任务委托给了Class.forName()方法,       Class.forName()又将其工作委托给了它调用的方法的类加载器。因为调用的方法是 MBeanInstantiator.loadClass(),而它使用的是核心类加载器,因此没有提供安全检查。

合规解决方案(CVE-2013-0422)

Oracle公司在Java 1.7.0版本的第11次更新中,添加了对MBeanInstantiator. loadClass()方法的访问检查,缓解了这个漏洞。这个访问检查确保了调用者可以访问所寻求的类。

// ...

if (className == null) {

throw new RuntimeOperationsException(

new IllegalArgumentException(

"The class name cannot be null"),

"Exception occurred during object instantiation");

}

ReflectUtil.checkPackageAccess(className);

try {

if (loader == null)

// ...

适用性

允许不可信代码调用降低安全性检查的方法,将会导致特权升级。同样地,允许不可信代码使用直接调用者的类加载器来执行操作,可能会允许不可信代码以与直接调用者相同的权限执行。

避免使用直接调用者的类加载器实例的方法,超出了本指南的讨论范围。例如,三参数的java.lang.Class.forName()方法需要一个显式的参数,用以指定要使用的类加载器实例。

public static Class forName(String name, boolean initialize,

ClassLoader loader) throws ClassNotFoundException

当实例必须返回给不可信代码时,不要使用直接调用者的类加载器作为第三个参数。

参考文献

[API 2013]   Class ClassLoader

[Chan 1998]   java.lang.reflect AccessibleObject

[Guillardoy 2012] Java 0Day Analysis(CVE-2012-4681)

[Long 2012]   SEC00-J.Do not allow privileged blocks to leak sensitive information across a trust boundary

SEC03-J. Do not load trusted classes after allowing untrusted code to load arbitrary classes

SEC05-J. Do not use reflection to increase accessibility of classes, methods,or fields

[Manion 2013]  “Anatomy of Java Exploits”

[Oracle 2013d]  Oracle Security Alert for CVE-2013-0422

指南19:对细粒度的安全定义自定义安全权限

默认的SecurityManager会检查给定方法的调用者是否具有足够的继续执行动作的权限。动作定义在Java安全架构的访问级别,需要特定的权限才能执行。例如,java.io.FilePermission类的动作是读、写、执行和删除[API 2013]。“权限描述和风险”指南(Permission Descriptions and Risks guide)[Oracle 2011d]列举了默认的权限和为Java代码授予这些权限有关的风险。

有时候,我们需要的限制比默认安全管理器所能提供的还要强。当不存在对应的默认权限且未能提供自定义的权限时,可能会导致特权升级漏洞,从而允许不可信的调用者执行限制操作或动作。

本指南讨论了过多权限的问题,有关解决这个问题的另外一个办法,参见指南16。

违规代码示例

下面的违规代码示例包含一个特权代码块,用来执行两个敏感操作:加载一个库;设置默认异常处理程序。

class LoadLibrary {

private void loadLibrary() {

AccessController.doPrivileged(

new PrivilegedAction() {

public Object run() {

// Privileged code

System.loadLibrary("myLib.so");

// Perform some sensitive operation like

// setting the default exception handler

MyExceptionReporter.setExceptionReporter(reporter);

return null;

}

});

}

}

使用时,默认的安全管理器会禁止库的加载,除非RuntimePermission loadLibrary.myLib在策略文件中已被授权。然而,安全管理器不会自动防护调用者的第二个敏感操作的执行,即设置默认异常处理程序,因为该操作的权限不是默认的,因此,安全管理器此时不会生效。这个安全弱点可以被利用,例如,编程并安装一个能泄露信息的异常处理程序,泄露那些合法处理程序会过滤掉的信息。

合规解决方案

下面的合规解决方案定义了一个自定义的权限ExceptionReporterPermission,与目标 exc.reporter,用以禁止非法调用者设置默认异常处理程序。这可以通过子类化BasicPermission来实现,它允许二进制风格的权限(允许或不允许)。该解决方案然后使用安全管理器,检查调用者是否拥有必要的设置异常处理程序的权限。如果检查失败,代码会抛出 SecurityException 异常。自定义权限类ExceptionReporterPermission还定义了所需的两个构造函数。

class LoadLibrary {

private void loadLibrary() {

AccessController.doPrivileged(

new PrivilegedAction() {

public Object run() {

// Privileged code

System.loadLibrary("myLib.so");

// Perform some sensitive operation like

// setting the default exception handler

MyExceptionReporter.setExceptionReporter(reporter);

return null;

}

});

}

}

final class MyExceptionReporter extends ExceptionReporter {

public void setExceptionReporter(ExceptionReporter reporter) {

SecurityManager sm = System.getSecurityManager();

if(sm != null) {

sm.checkPermission(

new ExceptionReporterPermission("exc.reporter"));

}

// Proceed to set the exception reporter

}

// ... Other methods of MyExceptionReporter

}

final class ExceptionReporterPermission extends BasicPermission {

public ExceptionReporterPermission(String permName) {

super(permName);

}

// Even though the actions parameter is ignored,

// this constructor has to be defined

public ExceptionReporterPermission(String permName,

String actions) {

super(permName, actions);

}

}

策略文件需要授予两个权限:将ExceptionReporterPermission权限授予exc.reporter;将RuntimePermission权限授予loadlibrary.myLib。以下策略文件假设上述资源位于Windows系统的c:\package目录下。

grant codeBase "file:/c:/package" {

//For *nix, file:${user.home}/package/

permission ExceptionReporterPermission "exc.reporter";

permission java.lang.RuntimePermission "loadLibrary.myLib";

};

默认情况下,不能使用BasicPermission将权限定义为支持动作,如果需要的话,可以在 ExceptionReporterPermission 的子类中自由地实现这些动作。BasicPermission 是一个抽象类,尽管它不包含抽象方法;它声明了所有从Permission类继承的方法。BasicPermission类的自定义子类必须定义两个构造函数,调用最合适的(单参数或双参数)超类构造函数(因为超类没有默认构造函数)。双参数构造函数也接受一个动作,即使基本权限不会使用它。从策略文件中构造权限对象时,需要这种行为。注意,BasicPermission类的自定义子类要被声明成final类。

适用性

运行的Java代码没有定义自定义的权限,默认的权限也不适用,这可导致应用程序出现特权升级漏洞。

参考文献

[API 2013]   Class FilePermission

Class SecurityManager

[Oaks 2001]   “Permissions” subsection of Chapter 5, “The Access Controller,”

[Oracle 2011d]  Permissions in the Java™ SE 6 Development Kit(JDK)

[Oracle 2013c]  Java Platform Standard Edition 7 Documentation

[Policy 2010]  “Permission Descriptions and Risks”

指南20:使用安全管理器创建一个安全的沙盒

根据Java API中SecurityManager类的文档[API 2013]:

安全管理器是一个类,它允许应用程序实现一个安全策略。在执行一个可能不安全或敏感的操作前,它允许应用程序确定这个操作是什么,它是否正在一个安全的允许执行这个操作的上下文中进行尝试。该应用程序可以允许或禁止该操作。

安全管理器可以与任何Java代码相关联。

applet的安全管理器拒绝applet最基本特权以外的所有其他特权。如此设计旨在防止无意的系统修改、信息泄漏和用户冒名。安全管理器的使用并不局限于客户端保护。比如Tomcat和WebSphere这样的Web服务器,使用安全管理器来隔离木马servlet和恶意的Java服务器页面(JSP),保护敏感的系统资源不被无意访问。

从命令行运行的Java应用程序,可以通过命令行参数标志来设置默认的安全管理器或者自定义的安全管理器。另外,可以通过编程方式安装一个安全管理器。以编程方式安装安全管理器能促使程序创建一个默认的沙盒,这个沙盒会基于当前生效的安全策略来允许或拒绝敏感动作。

从Java 2 SE平台开始,SecurityManager不再是一个抽象类。因此,不再需要显式覆盖它的方法。以编程方式创建和使用安全管理器时,代码必须具有运行时权限来调用createSecurityManager(实例化SecurityManager)和setSecurityManager(安装它)。这些权限只在安全管理器已经安装时才被检查。在有些情况下,这很有用,比如在一个虚拟主机上有一个默认的安全管理器,它必须拒绝个人主机以自定义的安全管理器来覆盖默认的安全管理器。

安全管理器与AccessController类密切相关,前者是访问控制的中心,后者提供了访问控制算法的实际实现。安全管理器支持以下两项。

提供向后兼容性:老程序通常包含自定义安全性管理器类的实现,因为它最初是抽象类。

定义自定义策略:通过子类化安全管理器来允许定义自定义的安全策略(如多层次、粗粒度或细粒度)。

关于自定义安全管理器与默认安全管理器的实现和使用,Java安全架构规范(Java security architecture specification)  [SecuritySpec 2010]中是这么声明的:

我们鼓励在应用程序代码中使用AccessController类,而定制安全管理器(通过子类化)应该是最后的手段,应当十分小心。此外,一个定制的安全管理器,如在调用标准安全检查前总是检查当前时间,在合适的时候可以而且应该使用AccessController所提供的算法。

许多 Java SE API 在执行敏感操作前,都会默认执行安全管理器检查。例如,       java.io.FileInputStream 的构造函数,如果调用者没有足够的读取文件的权限,它就会抛出 SecurityException 异常。因为 SecurityException 安全异常是 RuntimeException 运行时异常的一个子类,所以一些 API 方法(如java.io.FileReader类的方法)的声明可能缺乏列出SecurityException的throws语句。要避免依赖于没有在API方法文档中指定的安全管理器检查是否存在。

违规代码示例(命令行安装)

下面的违规代码示例没有从命令行安装任何安全管理器。因此,程序运行时启用了所有权限,也就是说,没有安全管理器来防止程序可能执行的任何邪恶动作。

java LocalJavaApp

合规解决方案(默认策略文件)

任何 Java 程序都可以尝试以编程方式安装 SecurityManager,尽管当前活动的安全管理器可能禁止此操作。被设计为本地运行的应用程序可以在调用时通过命令行参数指定一个默认的安全管理器。

当必须禁止应用程序以编程方式安装定制安全管理器,并且在任何情况下都要遵守默认的安全策略时,使用命令行选项更好。下面的合规解决方案使用了合适的命令行参数来安装默认安全管理器。安全策略文件为应用程序的预期动作授予了权限。

java -Djava.security.manager -Djava.security.policy=policyURL \

LocalJavaApp

命令行参数标志可以指定一个自定义的安全管理器,其策略被全局执行。使用-Djava.security.manager标志,如下所示:

java -Djava.security.manager=my.security.CustomManager ...

如果由当前安全管理器执行的当前安全策略禁止替换(通过省略 Runtime Permission("setSecurityManager")),那么任何试图调用 setSecurity Manager()的方法都会抛出SecurityException异常。

在类Unix系统及其等效的微软Windows系统的/path/to/java.home/lib/security目录中,可以找到默认的安全策略文件java.sercurity,它负责许多权限(读取系统属性、绑定到未授权端口等)的授予。一个特定于用户的策略文件也可能位于该用户的主目录中。这些策略文件的集合用来指定为程序授予的权限。java.security文件可以指定使用的策略文件。如果系统级的java.policy文件或java.security文件其中之一被删除,那么就没有用来执行Java程序的权限。

合规解决方案(定制策略文件)

当以一个定制的策略文件覆盖全局 Java 安全策略时,要使用双等号(==)替换单等号(=):

java -Djava.security.manager \

-Djava.security.policy==policyURL \

LocalJavaApp

合规解决方案(附加策略文件)

appletviewer 自动安装了一个带有标准策略文件的安全管理器,并且使用-J标志指定了附加的策略文件。

appletviewer -J-Djava.security.manager \

-J-Djava.security.policy==policyURL LocalJavaApp

注意,当安全属性文件(java.security)中的policy.allowSystemProperty属性值被设置为false时,参数中指定的策略文件就会被忽略;该属性的默认值为true。默认策略实现和策略文件语法(Default Policy Implementation and Policy File Syntax)[Policy 2010]深入讨论了编写策略文件时的问题和语法。

违规代码示例(编程式安装)

也可以使用静态的System.setSecurityManager() 方法来激活SecurityManager。同时只能有一个SecurityManager处于激活状态。这个方法以参数中提供的SecurityManager或null来取代当前活动的SecurityManager。

下面的违规代码示例在让当前安全管理器失效的同时,并没有在原来位置上安装另一个安全管理器。因此,后续代码将在启用所有权限的状态下运行,对程序可能执行的任何邪恶动作都没有了限制。

try {

System.setSecurityManager(null);

} catch (SecurityException se) {

// Cannot set security manager, log to file

}

一个实施合理安全策略的活动的SecurityManager能阻止系统令其失效,让这段代码抛出SecurityException异常。

合规解决方案(默认安全管理器)

下面的合规解决方案对默认安全管理器进行了实例化和设置。

try {

System.setSecurityManager(new SecurityManager());

} catch (SecurityException se) {

// Cannot set security manager, log appropriately

}

合规解决方案(自定义安全管理器)

下面的合规解决方案演示了如何实例化一个名为CustomSecurityManager的自定义安全管理器。程序首先通过密码调用了它的构造函数,然后将这个自定义安全管理器安装成了活动的安全管理器。

char password[] = /* 初始化 */

try {

System.setSecurityManager(

new CustomSecurityManager("password here")

);

} catch (SecurityException se) {

// Cannot set security manager, log appropriately

}

这段代码执行后,执行安全检查的API将使用这个自定义安全管理器。如前所述,只有当默认安全管理器缺少所需要的功能时,才考虑安装自定义安全管理器。

适用性

Java的安全性从根本上取决于安全管理器的存在。当其不存在时,敏感动作可以无限制地执行。

在运行时编程检测安全管理器的存在与否是很简单的。静态分析可以解决此类代码的存在与否,如果该代码被执行,它将试图安装一个安全管理器。在某些情况下,可以检查安全管理器是否安装得够早、是否指定了所需的属性,或者是否可以保证会被安装,但通常情况下是不可判定的。

当已知默认的全局安全管理器总是会从命令行安装时,对 setSecurity Manager()方法的调用在受控环境中可能会被忽略。这很难实施,如果环境配置不正确,会出现漏洞。

参考文献

[API 2013]     Class SecurityManager

Class AccessControlContext

Class AccessController

[Gong 2003]    §6.1, “Security Manager”

[Policy 2010]    Default Policy Implementation and Policy File Syntax

[SecuritySpec 2010]  §6.2, “SecurityManager versus AccessController”

[Pistoia 2004]

§7.4, “The Security Manager”

指南21:不要让不可信代码误用回调方法的特权

回调提供一种注册方法的手段,在其感兴趣的事件发生时将会被调用(或回调。Java在很多地方都使用了回调,       如applet技术、响应Servlet的生命周期事件、AWT和 Swing 框架中的事件通知(如按钮单击事件),以及异步的读写操作等。甚至在线程的运行机制Runnable.run()中,新起一个线程时,自动执行对应的run()方法都使用到了回调技术。

在Java中,回调函数通常是使用接口来实现。下面是回调的一般结构:

public interface CallBack {

void callMethod();

}

class CallBackImpl implements CallBack {

public void callMethod() {

System.out.println("CallBack invoked");

}

}

class CallBackAction {

private CallBack callback;

public CallBackAction(CallBack callback) {

this.callback = callback;

}

public void perform() {

callback.callMethod();

}

}

class Client {

public static void main(String[] args) {

CallBackAction action =

new CallBackAction(new CallBackImpl());

// ...

action.perform(); // Prints "CallBack invoked"

}

}

回调方法的调用通常没有特权的变化,这意味着,执行它们的上下文的特权可能比声明它们的上下文的多。如果这些回调方法接受不可信代码的数据,那么就有可能发生特权升级。

根据Oracle的安全编码指南[SCG 2010]:

从系统调用回调方法通常具有完全权限。恶意代码在执行操作时会出现在栈上——这似乎是一个合理的期望,但事实并非如此。恶意代码可以设置一个对象,用来将回调过渡给一个经过安全检查的操作。例如,一个文件选择器对话框,可以从用户动作中操作文件系统,这就可能导致恶意代码发送某些事件。另外,恶意代码可以通过将看上去无害的东西伪装成一个文件选择器来重定向用户事件。

这条指南是指南17的一个实例,并且和《The CERT®Oracle®Secure Coding Standard for Java™》[Long 2012]的“SEC01-J.Do not allow tainted variables in privileged blocks”有关。

违规代码示例

下面的违规代码示例使用UserLookupCallBack类实现CallBack接口,通过给定的用户ID查找用户名。这个查找代码假定这些信息在/etc/passwd文件中,这需要提升特权才能打开。因此,Client类使用提升的特权(在 doPrivileged语句块中)来调用所有回调。

public interface CallBack {

void callMethod();

}

class UserLookupCallBack implements CallBack {

private int uid;

private String name;

public UserLookupCallBack(int uid) {

this.uid = uid;

}

public String getName() {

return name;

}

public void callMethod() {

try (InputStream fis = new FileInputStream("/etc/passwd")) {

// Look up uid & assign to name

} catch (IOException x) {

name = null;

}

}

}

final class CallBackAction {

private CallBack callback;

public CallBackAction(CallBack callback) {

this.callback = callback;

}

public void perform() {

AccessController.doPrivileged(new PrivilegedAction<Void>() {

public Void run() {

callback.callMethod();

return null;

}

});

}

}

这段代码可以被客户端安全地使用,如下所示:

public static void main(String[] args) {

int uid = Integer.parseInt(args[0]);

CallBack callBack = new UserLookupCallBack(uid);

CallBackAction action = new CallBackAction(callBack);

// ...

action.perform(); // Looks up user name

System.out.println("User " + uid + " is named " +

callBack.getName());

}

然而,攻击者可以通过注册MaliciousCallBack 实例,使用 CallBackAction和提升的特权执行恶意代码:

class MaliciousCallBack implements CallBack {

public void callMethod() {

// Code here gets executed with elevated privileges

}

}

// Client code

public static void main(String[] args) {

CallBack callBack = new MaliciousCallBack();

CallBackAction action = new CallBackAction(callBack);

action.perform(); // Executes malicious code

}

合规解决方案(回调自己调用doPrivileged块)

根据Oracle公司的安全编码指南[SCG 2010]:按照惯例,PrivilegedAction的实例和PrivilegedExceptionAction的实例可以提供给不可信代码,但不能以调用者提供的动作调用doPrivileged。

下面的合规解决方案将doPrivileged()的调用从CallBackAction代码中移到了它自己的回调里。

public interface CallBack {

void callMethod();

}

class UserLookupCallBack implements CallBack {

private int uid;

private String name;

public UserLookupCallBack(int uid) {

this.uid = uid;

}

public String getName() {

return name;

}

public final void callMethod() {

AccessController.doPrivileged(new PrivilegedAction<Void>() {

public Void run() {

try (InputStream fis =

new FileInputStream("/etc/passwd")) {

// Look up userid and assign to

// UserLookupCallBack.this.name

} catch (IOException x) {

UserLookupCallBack.this.name = null;

}

return null;

}

});

}

}

final class CallBackAction {

private CallBack callback;

public CallBackAction(CallBack callback) {

this.callback = callback;

}

public void perform() {

callback.callMethod();

}

}

这段代码的行为和之前一样,但攻击者不能再以提升的特权执行恶意回调代码。即使攻击者可以通过使用CallBackAction类的构造函数传递一个恶意的回调实例,代码也不能以提升的特权执行,因为恶意实例必须包含的doPrivileged语句块没有和可信代码一样的权限。此外,因为CallBackAction类已被声明为final,所以它不能被子类化,从而不能覆盖perform()方法。

合规解决方案(将回调声明为final)

下面的合规解决方案通过将UserLookupCallBack类声明为final来防止callMethod()被覆盖。

final class UserLookupCallBack implements CallBack {

// ...

}

// Remaining code is unchanged

适用性

通过回调暴露敏感方法可能导致特权误用和任意代码执行。

参考文献

[API 2013]   AccessController.doPrivileged()

[Long 2012]   SEC01-J.Do not allow tainted variables in privileged blocks

[SCG 2010]   Guideline 9-2:Beware of callback methods

Guideline 9-3: Safely invoke java.security. AccessController. doPrivileged

[1].“Exploiting Java Vulnerability CVE-2012-0507 Using Metasploit” is shared by user BreakTheSec on Slideshare.net (July 14, 2012); see www.slideshare.net/BreakTheSec/exploiting-java-vulnerability.

相关图书

Effective Java中文版(原书第3版)
Effective Java中文版(原书第3版)
Java核心技术速学版(第3版)
Java核心技术速学版(第3版)
Java编程动手学
Java编程动手学
Java研发自测入门与进阶
Java研发自测入门与进阶
Java开发坑点解析:从根因分析到最佳实践
Java开发坑点解析:从根因分析到最佳实践
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)

相关文章

相关课程