java安全问题其实是很多程序员想了解又容易忽略的问题,但需要我们重视起来,提高应用程序的安全性。常出现的安全问题包括,程序接受数据可能来源于未经验证的用户,网络连接和其他不受信任的来源,如果未对程序接受数据进行校验,则可能会引发安全问题等等,具体也可以分成以下几方面:
- 数据校验
- 敏感信息
- 加密算法
- 序列化与反序列化
- I/O操作
- 多线程安全
- 框架和组件
数据校验
数据校验-校验策略
1. 白名单策略 -接受已知好的数据( 任何时候,尽可能使用“白名单”的策略 )
下面的示例代码确保 name参数只包含字母、以及下划线
if (Pattern.matches("^[0 -9A -Za -z_]+$", name)){
throw new IllegalArgumentException("Invalid name");
}
2. 黑名单策略 -拒绝已知好的数据
public String removeJavascript(String input){
Pattern p = Pattern.compile("javascript", Pattern.CASE_INSENSITIVE );
Matcher m = p.matcher(input);
return (! m.matches()) ? input : "";
}
3. 白名单净化
对数据中任何不属于某个已验证的、合法字符列表进行删除编码或者替换,然后再使用这些净化的数据
4. 黑名单净化: 剔除或者转换某些字符(例如,删除引号、转换成HTML实体)
public static String quoteApostrophe(String input){
if (input != null){
return input.replaceAll(" \'","’");
} else{
return null;
}
}
数据校验 -输入输出
规则1.1 校验跨信任边界传递的不可数据**
程序接受的不可信数据源跨越任边界传递必须经过内校验,包括输入和出校验。
不可信数据:用户、网络连接等源 不可信数据:用户、网络连接等源
数据入口:
- 终端计算机
- 互联网出入口
- 广域网出入口
- 公司对外发布服务的 DMZ服务器
- VPN和类似远程连接设备。
信任边界:根据威胁建模划分的信任边 如 web 应用的服务端;
规则 1.2:禁止直接使用不可信数据来拼SQL语句
SQL 注入是指原始SQL查询被动态更改成一个与程序预期完全不同的查询。执行这样后可能导致信息泄露或者数据被篡改。防止 SQL注入的方式主要可以分为两类:
- 使用参数化查询 (推荐使用)
- 对不可信数据进行校验
- 预编译处理
Statement stmt= null;
ResultSet rs= null;
try{
String userName= ctx.getAuthenticatedUserName(); //this is a constant
String sqlString= "SELECT * FROM t_item
WHERE owner='" + userName+ "' AND itemName='" + request.getParameter("itemName") + "'";
stmt= connection.createStatement();
rs= stmt.executeQuery(sqlString);// ... result set handling
}
添加 name' OR 'a' = 'a
SELECT * FROM t_item WHERE owner = 'wiley' AND itemname= 'name' OR 'a'='a';
预编译处理:
PreparedStatement stmt= null
ResultSet rs=null
try
{
String userName= ctx.getAuthenticatedUserName(); //this is a constant
String itemName= request.getParameter("");
// ...Ensure that the length of userName and itemNameis legitimate
// ...
String sqlString= "SELECT * FROM t_item WHERE owner=? AND itemName=?";
stmt= connection.prepareStatement(sqlString);
stmt.setString(1, userName);
stmt.setString(2, itemName);
rs=stmt.executeQuery();
// ... result set handling
}catch(SQLExceptions e)
{
// ... logging and error handling
}
在存储过程中,通拼接参数值来构建查询字符串和应用序代码一样同是有SQL注入风险
反例:
CallableStatement= null
ResultSet results = null;
try{
String userName= ctx.getAuthenticatedUserName(); //this is a constant
String itemName= request.getParameter("itemName");
cs= connection.prepareCall("{call sp_queryItem(?,?)}");
cs.setString(1, userName);
cs.setString(2, itemName);
results = cs.executeQuery();
// ... result set handling
}catch(SQLException se){
// ... logging and error handling
}
对应的SQL Server存储过程:
CREATE PROCEDURE sp_queryItem
@userNamevarchar(50),
@itemNamevarchar(50)
AS
BEGIN
DECLARE @sql nvarchar(500);
SET @sql= 'SELECT * FROM t_item
WHERE owner = ''' + @userName+ '''
AND itemName= ''' + @itemName+ '''';
EXEC(@sql);
END
GO
正例:
** 在存储过程中动态构建sql,采用预编译的方式防御sql注入,**
CallableStatement= null
ResultSet results = null;
try{
String userName= ctx.getAuthenticatedUserName(); //this is a constant
String itemName=request.getParameter("itemName");
// ... Ensure that the length of userName and itemName is legitimate
// ...
cs= ("{call sp_queryItem(?,?)}");
cs.setString(1, userName);
cs.setString(2, itemName);
results = cs.executeQuery();
// ... result set handling
}catch(SQLException se){
// ... logging and error handling
}
对应的SQL Server存储过程:
CREATE PROCEDURE sp_queryItem
@userName varchar(50),
@itemName varchar(50)
AS
BEGIN
SELECT * FROM t_item
WHERE userName= @userName
AND itemName= @itemName;
END
使用Hibernate,如果在动态构建SQL/HQL查询时包含了不可信输入,同样也会面临SQL/HQL注入的问题。
反例:
//原生sql查询
String userName= ctx.getAuthenticatedUserName();
//this is a constant
String itemName= request.getParameter("itemName");
Query sqlQuery= session.createSQLQuery("select * from where owner = '"
+ userName+ "' and itemName= '" + itemName+ “’”);
List<Item> rs= (List<Item>) sqlQuery.list();
//HQL查询
String userName= ctx.getAuthenticatedUserName();
//this is a constant
String itemName=request.getParameter("itemName");
Query hqlQuery= session.createQuery("from Item as item where item.owner= '"
+ userName+ "' and = '" + itemName+ "'");
List<Item> hrs= (List<Item>) hqlQuery.list();
正例:
//HQL中基于位置的参数化查询:
String userName= ctx.getAuthenticatedUserName();
String itemName=request.getParameter("itemName");
Query hqlQuery= session.createQuery("from Item as item where item.owner= ? and item.itemName= ?");
hqlQuery.setString(1, userName);
hqlQuery.setString(2, itemName);
List<Item> rs= (List<Item>) hqlQuery.list();
//HQL中基于名称的参数化查询:
String userName= ctx.getAuthenticatedUserName();
String itemName= ("itemName");
Query hqlQuery= session.createQuery("from Item as item where item.owner= :owner and = :itemName");
hqlQuery.setString("owner", userName);
hqlQuery.setString("itemName", itemName);
List<Item> rs= (List<Item>) hqlQuery.list();
//原生参数化查询:
String userName=ctx.getAuthenticatedUserName(); //this is a constant
String itemName= request.getParameter("itemName");
Query sqlQuery= session.createSQLQuery("select * from t_itemwhere owner = ? and itemName= ?");
sqlQuery.setString(0, owner);
sqlQuery.setString(1, itemName);
List<Item> rs= (List<Item>) sqlQuery.list();
Mybaits和ibaits的#和$
Mybaits:
<select id="getItems" parameterClass="MyClass" resultClass="Item">
SELECT * FROM t_item
WHERE owner = #userName# AND itemName= #itemName#
</select>
String sqlString= "SELECT * FROM t_itemWHERE owner=? AND itemName=?";
PreparedStatement stmt= connection.prepareStatement(sqlString);
stmt.setString(1, myClassObj.getUserName());
stmt.setString(2, myClassObj.getItemName());
ResultSet rs= stmt.executeQuery();
// ... convert results set to Item objects
ibaits:
<select id="getItems" parameterClass="MyClass"="items">
SELECT * FROM t_item
WHERE owner = #userName# AND itemName= '$itemName$'
</select>
String sqlString= "SELECT * FROM t_itemWHERE owner=? AND itemName='" +myClassObj.getItemName() + "'";
PreparedStatementstmt=connection.prepareStatement(sqlString);
stmt.setString(1, myClassObj.getUserName());
ResultSetrs= stmt.executeQuery();
输入验证,针对无法参数化查询的场景
public List<Book> queryBooks(queryCondition){
try{
StringBuilder sb= StringBuilder("select * from t_bookwhere ");
Codec oe= new OracleCodec();
if(queryCondition!= null&& !queryCondition.isEmpty()){
for(Expression e : queryCondition){
String exprString=e.getColumn() + e.getOperator() + e.getValue();
String safeExpr= ESAPI.encoder().encodeForSQL(oe, exprString);
sb.append(safeExpr).append(" and ");
}
sb.append("1=1");
Statement stat = connection.createStatement();
ResultSet rs= stat.executeQuery(sb.toString());
//other omitted code
}
}
}
规则1.3 禁止直接使用不可信数据来拼接XML
一个用户,如果他被允许输入结构化的XML片段,则他可以在XML的数据域中注入XML标签来改写目标XML文档的结构与内容。XML解析器会对注入的标签进行识别和解释。
private void createXMLStream(BufferedOutputStreamoutStream, User user) throws IOException{
String xmlString;
xmlString= "<user><role>operator</role><id>" + user.getUserId()+ "</id><description>" + user.getDescription() + "</description></user>";
outStream.write(xmlString.getBytes());
outStream.flush();}}
添加joe</id><role>administrator</role><id>joe
<user>
<role>operator</role>
<id>joe</id>
<role>administrator</role>
<id>joe</id>
<description>I want to be an administrator</description>
</user>
XML Schema或者DTD校验,反例:
private void createXMLStream(BufferedOutputStreamoutStream, User user)throwsIOException{
String xmlString;
xmlString= "<user><id>" + user.getUserId()+ "</id><role>operator</role><description>"+ user.getDescription() + "</description></user>";
StreamSource xmlStream= new StreamSource(new StringReader(xmlString));
// Build a validating SAX parser using the schema
SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
StreamSource ss= new StreamSource(newFile("schema.xsd"));
try{
Schema schema= sf.newSchema(ss);
Validator validator= schema.newValidator();
validator.validate(xmlStream);
}catch(SAXException x){
throw new IOException("Invalid userId", x);
}
// the XML is valid, proceed
outStream.write(xmlString.getBytes());
outStream.flush();
}
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="user">
<xs:complexType>
<xs:sequence>
<xs:elementname="id" type="xs:string"/>
<xs:element name="role"type="xs:string"/>
<xs:element name="description" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
某个恶意用户可能会使用下面的字符串作为用户ID:
"joe</id><role>Administrator</role>
<!—"并使用如下字符串作为描述字段:
"-><description>I want to be an administrator"
<user>
<id>joe</id>
<role>Administrator</role><!--</id>
<role>operator</role> <description> -->
<description>I want to be an administrator</description>
</user>
安全做法:白名单+安全的xml库
private void createXMLStream(BufferedOutputStreamoutStream, User user) throws IOException{
// Write XML string if userID contains alphanumeric and underscore characters only
if (!Pattern.matches("[_a-bA-B0-9]+", user.getUserId())){
// Handle format violation
}
if (!Pattern.matches("[_a-bA-B0-9]+", user.getDescription())){
// Handle format violation
}
String xmlString= "<user><id>"+ user.getUserId()+ "</id><role>operator</role><description>"+ user.getDescription() + "</description></user>";
outStream.write(xmlString.getBytes());
outStream.flush();
}
public static void buidlXML(FileWriterwriter, User user) throwsIOException{
Document userDoc= DocumentHelper.createDocumen();
Element userElem= userDoc.addElement("user");
Element idElem= userElem.addElement("id");
idElem.setText(user.getUserId());
Element roleElem= userElem.addElement("role");
roleElem.setText("operator");
Element descrElem=userElem.addElement("description");
descrElem.setText(user.getDescription());
XMLWriter output = null;
try{
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding("UTF-8");
output = new XMLWriter(writer, format);
output.write(userDoc);
output.flush();
}
}
Xml注入净化之后的数据
<user>
<id>joe</id><role>Administrator</role><!—</id>
<role>operator</role>
<description>-->lt;description>Iwant to be an administrator</description>
</user>
规则1.4:禁止直接使用不可信数据来记录日志
如果在记录的日志中包含未经校验的不可信数据,则可能导致日志注入漏洞。恶意用户会插入伪造的日志数据,从而让系统
管理员误以为这些日志数据是由系统记录的。例如,一个用户可能通过输入一个回车符和一个换行符(CRLF)序列来将一
条合法日志拆分成两条日志,其中每一条都可能会令人误解。
将未经净化的用户输入写入日志还可能会导致向信任边界之外泄露敏感数据,或者导致违反当地法律法规,在日志中写入和存储了某些类型的敏感数据。
if(loginSuccessful){
logger.severe("User login succeeded for: "+ username);
}else{
logger.severe("User login failed for: "+ username);
}
生成log:
david May 15, 2011 2:25:52 PM java.util.logging.LogManager$RootLogger.log
SEVERE: User login succeeded for: administrator
May 15, 2011 2:19:10 PM java.util.logging.LogManager$RootLogger log
SEVERE: User login failed for: david
May 15, 2011 2:25:52 PM java.util.logging.LogManager log
SEVERE: User login succeeded for: administrator
Username=David(生成标准日志)
May 15, 2011 2:19:10 PM java.util.logging.LogManager$RootLogger log
SEVERE: User login failed for: david
登录之前会对用户名输入进行净化,从而防止注入攻击
if(!Pattern.("[A-Za-z0-9_]+", username)){
// Unsanitized username
logger.severe("User login failed for unauthorized user");
}else if(loginSuccessful){
logger.severe("User login succeeded for: "+ username);
}else{
logger.severe("User login failed for: "+ username);
}
规则1.5:禁止向Runtime.exec() 方法传递不可信、未净化的数据
在执行任意系统命令或者外部程序时使用了未经校验的不可信输入,就会导致产生命令和参数注入漏洞。
class DirList{
public static void main(String[] args){
if(args.length== 0){
System.out.println("No arguments");
System.exit(1);
}
try{
Runtime rt= Runtime.getRuntime();
Process proc = rt.exec("cmd.exe /c dir" + args[0]);
// ...
}catch(Exception e){
// Handle errors
}
}
}
java DirList"dummy & echo bad"
dirdummy
echo bad
安全建议:
- 避免直接使用Runtime.exec(),采用标准的API替代运行系统命令来完成任务
- 白名单数据校验和数据净化
class DirList{
public static void main(String[] args){
if(args.length== 0){
System.out.println("No arguments");
System.exit(1);
}
try{
File dir= newFile(args[0]);
// the dir need to be validated
if (!validate(dir)) {
System.out.println("An illegal directory");
}else{
for (String file : dir.list()){
System.out.println(file);
}
}
}
}
}
类型 | 举例 | 常见注入模式和结果 |
---|---|---|
管道 | | | | shell_command -执行命令并返回命令输出信息 |
内联 | ; & |
; shell_command -执行命令并返回命令输出信息 & shell_command -执行命令并返回命令输出信息 |
逻辑运算符 | $ && || |
$(shell_command) -执行命令 && shell_command -执行命令并返回命令输出信息 || shell_command -执行命令并返回命令输出信息 |
重定向运算符 | > >> < |
> target_file -使用前面命令的输出信息写入目标文件 >> target_file -将前面命令的输出信息附加到目标文件 < target_file-将目标文件的内容发送到前面的命令 |
规则1.6:验证路径之前应该先将其标准化
绝对路径名或者相对路径名中可能会包含文件链接,对文件名标准化可以使得验证文件路径更加容易,同时可以防御目录遍历引发的安全漏洞。
public static void main(String[] args){
File f = newFile(System.getProperty("user.home")
+ System.getProperty("file.separator") + args[0]);
String absPath= f.getAbsolutePath();
if(!isInSecureDir(Paths.get(absPath))){
// Refer to Rule 3.5 for the details of isInSecureDir()
throw new IllegalArgumentException();
}
if(!validate(absPath)){
// Validation
throw new IllegalArgumentException();
}
/* … */
}
public static void main(String[] args) throwsIOException{
File f = newFile(System.getProperty("user.home")
+ System.getProperty("file.separator") + args[0]);
String canonicalPath= f.getCanonicalPath();
if(!isInSecureDir(Paths.get(absPath))){
// Refer to Rule3.5 for the details of isInSecureDir()
throw new IllegalArgumentException();
}
if(!validate(absPath)){
// Validation
throw new IllegalArgumentException();
}
/* ... */
}
规则1.7:安全地从ZipInputStream提取文件
- 提取出的文件标准路径落在解压的目标目录之外-跨目录解压攻击,
- 是提取出的文件消耗过多的系统资源-zip压缩炸弹。
static final int BUFFER= 512;
// ...
public final void unzip(String fileName) throws java.io.IOException{
FileInputStream fis= new FileInputStream(fileName);
ZipInputStream zis= new ZipInputStream(newBufferedInputStream(fis));
ZipEntry entry;
while((entry = zis.getNextEntry()) != null){
System.out.println("Extracting: "+ entry);
int count;
byte data[] = newbyte[BUFFER];
// Write the files to the disk
FileOutputStreamfos= new FileOutputStream(entry.getName());
BufferedOutputStreamdest= new BufferedOutputStream(fos, BUFFER);
while((count = zis.read(data, 0, BUFFER)) != -1){
dest.write(data, 0, count);
}
dest.flush();
dest.close();
zis.closeEntry();
}
zis.close();
}
未对解压的文件名做验证,直接将文件名传递给FileOutputStream构造器。它也未检查解压文件的资源消耗情况,它允许程序运行到操作完成或者本地资源被耗尽
示例
public static final int BUFFER= 512;
public static final int TOOBIG= 0x6400000; // 100MB
public final void unzip(String filename) throws java.io.IOException{
FileInputStream fis= newFileInputStream(filename);
ZipInputStreamzis= newZipInputStream(newBufferedInputStream(fis));
ZipEntry entry;
try{
while((entry = zis.getNextEntry()) != null){
System.out.println("Extracting: "+ entry);
int count;
byte data[] = new byte[BUFFER];
if (entry.getSize() > TOOBIG){
throw new IllegalStateException("File to be unzipped is huge.");
}
if(entry.getSize() == -1){
throw new IllegalStateException("File to be unzipped might be huge.");
}
FileOutputStreamfos= newFileOutputStream(entry.getName());
BufferedOutputStreamdest= new BufferedOutputStream(fos,BUFFER);
while((count = zis.read(data, 0, BUFFER)) != -1){
dest.write(data, 0, count);
}
dest.flush();
dest.close();
zis.closeEntry();
}
}
}
ZipEntry.getSize()方法在解压提取一个条目之前判断其大小,以试图解决之前的问题。攻击者可以伪造ZIP文件中用来描述解压条目大小的字段,因此,getSize()可靠的,本地资源实际仍可能被过度消耗
static final int BUFFER= 512;
static final int TOOBIG= 0x6400000; // max size of unzipped data, 100MB
static final int TOOMANY = 1024; // max number of files
// ...
private String sanitzeFileName(String entryName, String intendedDir) throws IOException{
File f = newFile(intendedDir, entryName);
String canonicalPath= f.getCanonicalPath();
File iD= newFile(intendedDir);
String canonicalID= iD.getCanonicalPath();
if(canonicalPath.startsWith(canonicalID)){
return canonicalPath;
}else{
throw new IllegalStateException("File is outside extraction target directory.");
}
}
public final void unzip(String fileName) throws java.io.IOException{
FileInputStream fis= new FileInputStream(fileName);
ZipInputStream zis= newZipInputStream(newBufferedInputStream(fis));
ZipEntryentry;
int entries = 0;
int total = 0;
byte[] data = newbyte[BUFFER];
try{
while((entry = zis.getNextEntry()) != null){
System.out.println("Extracting: "+ entry);
int count;
String name = sanitzeFileName(entry.getName(), ".");
FileOutputStream fos= newFileOutputStream(name);
BufferedOutputStream dest= new BufferedOutputStream(fos, BUFFER);
while (total + BUFFER<= && (count = zis.read(data, 0, BUFFER)) != -1){
dest.write(data, 0, count);
total += count;
}
dest.flush();
dest.close();
zis.closeEntry();
entries++;
if(entries > TOOMANY){
throw new IllegalStateException("Too many files to unzip.");
}
if(total > TOOBIG){
throw new IllegalStateException("File being unzipped is too big.");
}
}
}
}
规则1.8:禁止未经验证的用户输入直接输出到html界面
用户输入未经过验证直接输出到html界面容易导致xss注入攻击,该攻击方式可以盗取用户cookie信息,严重的可以形成xss蠕虫攻击漏洞,也可以结合其他的安全漏洞进一步进行攻击和破坏系统
反例:
String eid=request.getParameter("eid");
eid=StringEscapeUtils.escapeHtml(eid);//insufficient validation
...
ServletOutputStream out=response.getOutputStream();
out.print("Employee ID:"+eid);
...
out.close();
...
正例:
...
Statement stmt=conn.creatStatement();
ResultSet rs=stmt.executeQuery("select * from emp where id ="+eid);
if(rs != null){
rs.next();
String name=StringEscapeUtils.escapeHtml(rs.getString("name"));//insufficient validation
}
ServletOutputStream out =response.getOutputStream();
...
out.close();
...
数据类型 | 上下文 | 示例代码 | 防御措施 |
---|---|---|---|
string | HTML Body | <span>UNTRUSTED DATA</span> | HTML Entity编码 |
String | 安全HTML变量 | <input type="text" name="fname" value="UNTRUSTED DATA"> | 1. HTML Attribute编码 2. 只把不可信数据放在安全白名单内的变量上(白名单在下文列出) 3. 严格地校验不安全变量,如background、id和name |
String | GET参数 | <a href="/site/search?value=UNTRUSTED DATA">clickme</a> | URL编码 |
String | 使用在src或href变量上的不可信URLs | <a href="UNTRUSTED URL">clickme</a><iframe src="UNTRUSTED URL" / | 1. 对输入进行规范化; 2. URL校验; 3. URL安全性认证 4. 只允许使用http和https协议(避免使用JavaScript协议去打开一个新窗口) 5. HTML Attribute编码 |
String | CSS值 | <div style="width: UNTRUSTED DATA;">Selection</div> | 1. 使用CSS编码; 2. 使用CSS Hex编码; 3. 良好的CSS设计 |
String | JavaScript变量 | <script> var currentValue='UNTRUSTED DATA';</script> <script>someFunction('UNTRUSTED DATA');</script> |
1. 确保所有变量值都被引号括起来; 2. 使用JavaScript Hex编码 3. 使用JavaScript Unicode编码; 4. 避免使用“反斜杠转译”(\"、\'或者\) |
HTML | HTML Body | <div>UNTRUSTED HTML</div> | [HTML校验(JSoup, AntiSamy, HTML Sanitizer)] |
String | DOM XSS | <script> document.write("UNTRUSTED INPUT: " + document.location.hash);<script/> | 基于DOM操作的XSS漏洞防御措施 |
1、输入过滤:客户端求情参数:包括用户输入,url参数、post参数。
- 在产品形态上,针对不同输入类型,对输入做变量类型限制。
- 字符串类型的数据,需要针对<、>、/、’、”、&五个字符进行实体化转义
2、输出编码:浏览器解析中html和js编码不一样,以及上下文场景多样,所以对于后台输出的变量,不同的上下文中渲染后端变量,转码不一样。
特殊字符 | 实体编码 |
---|---|
& | & |
< | < |
> | > |
“ | " |
/ | / |
‘ | ' |
规则1.9:禁止直接解析未验证的xml实体
当允许引用外部实体时,若程序针对输入xml实体未验证,攻击者通过构造恶意内容,进行xxe注入攻击,可导致读取任意文件、执行系统命令、探测内网端口、攻击内网网站等危害
反例:
public void transform(InputStream xmlStream,OutputStream output)throws Exception{
Transformer trans=null;
TransformerFactory transFactory=TransformerFactory.newInstance();
if(this.style!=null){
trans=transFactory.newTranformer(this.style);
}else{
trans=transFactory.newTranformer();
}
/*********UTF-8***/
trans.setOutputProperty(OutputKeys.ENCOOING,"UTF-8");
Source source=new SAXSource(new InputSource(xmlStream));
Result result=new StreamResult(output);
trans.transform(source,result);
}
正例:
public void transform(InputStream xmlStream,OutputStream output)throws Exception{
Transformer trans=null;
TransformerFactory transFactory=TransformerFactory.newInstance();
if(this.style!=null){
//trans=transFactory.newTranformer(this.style);
TransformerFactory trans=TransformerFactory.newInstance();
trfactory.setFeature(XMLConstans.FEATURE_SECURE_PROCESSING,true);
trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_DTD,"");
trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_STYLESHEET,"");
}else{
//trans=transFactory.newTranformer();
TransformerFactory trans=TransformerFactory.newInstance();
trfactory.setFeature(XMLConstans.FEATURE_SECURE_PROCESSING,true);
trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_DTD,"");
trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_STYLESHEET,"");
}
/*********UTF-8***/
trans.setOutputProperty(OutputKeys.ENCOOING,"UTF-8");
Source source=new SAXSource(new InputSource(xmlStream));
Result result=new StreamResult(output);
trans.transform(source,result);
}
不同xml解析器防御xxe注入的方法:
XMLReader
To protect a java org.xml.sax.XMLReader from XXE,do this:
XMLReader reader=XMLReaderFactory.createXMLReader();
reader.setFeatrue("http://apache.org/xml/features/disallow-doctype-decl",true);
reader.setFeatrue("http://apache.org/xml/features/disallow-doctype-decl",true);
//stictly required as DTDs should not be allowed at all ,per previous
reader.setFeatrue("http://xml.org/sax/features/external-general-entilies",false);
reader.setFeatrue("http://xml.org/sax/features/external-parameter-entilies",false);
SAXReader
To protect a java org.dom4j.io.SAXReader from XXE,do this:
saxReader.setFeatrue("http://apache.org/xml/features/disallow-doctype-decl",true);
saxReader.setFeatrue("http://xml.org/sax/features/external-general-entilies",false);
saxReader.setFeatrue("http://xml.org/sax/features/external-parameter-entilies",false);
数据校验-权限管理
规则1.10:禁止程序数据进行增、删、改、查实对客户端请求的数据过分相信而遗漏对于权限的判定
垂直越权漏洞: 称为权限提升,是一种“基于URL的访问控制”设计缺陷引起的漏洞。由于Web应用程序没有做权限控制或者仅在菜单上做了权限控制,导致恶意用户只要猜测其他管理页面的URL,就可以访问或控制其他角色拥有的数据或页面,达到权限提升的目的。
水平越权漏洞: 一种“基于数据的访问控制”设计缺陷引起的漏洞。由于服务器端在接收到请求数据进行操作时没有判断数据的所属人而导致的越权数据访问漏洞。如服务器端从客户端提交的request参数(用户能够控制的数据)中获取用户id,恶意攻击者通过变换请求ID的值,查看或修改不属于本人的数据。
反例:
@RequestMapping(value = "delete")
public String delete(HttpServletRequest request, @RequestParam long id) throws Exception{
try {
userManage.delete(id);
request.setAttribute("msg","delete user success");
}catch (Exception e){
request.setAttribute("msg","delete user failure");
}
return list(request);
}
@RequestMapping(value = "/delete/{addrId}")
public Object remove(@RequestParam long addrId) {
Map<String,Object> resMap=new HashMap<>();
if(WebUtils.isLogged){
this.addressService.removeUserAddress(addrId);
resMap.put(Constans.RESP_STATUS_CODE_KEY,Constans.RESP_STATUS_CODE_SUCCESS);
resMap.put(Constans.MESSAGE,"remove user address success");
}else {
resMap.put(Constans.RESP_STATUS_CODE_KEY,Constans.RESP_STATUS_CODE_FAIL);
resMap.put(Constans.MESSAGE,"user is not login ,remove user address failure");
}
return resMap;
}
正例:垂直越权漏洞:在调用功能之前,验证当前用户身份是否有权限调用相关功能(推荐使用过滤器,进行统一权限验证)
public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException ,IOException{
if(request.getSession(true).getAttribute("manager")==null){
response.sendRedirect("noright.html");
return;
}
UserManagerService userManagerService=new UserManagerService();
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
String action=request.getParameter("action");
if("add".equals(action)){
String id=request.getParameter("userId");
String name=request.getParameter("userName");
String sex=request.getParameter("userSex");
}
//todo do somethings
}
数据校验-权限管理
- 通过全局过滤器来检测用户是否登录,是否对资源具有访问权限。
- 权限访问规则存入数据库中
- web.xml中配置过滤器
public class PriviegeFilter implements Filter{
@Autowired
private UserManagerService userManagerService;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
List<UserAuthorization> userAuthorizationS=userManagerService.getUserAuthorizationInfo();
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
for(UserAuthorization userAuthorization: userAuthorizationS){
// 从数据库中获取用户授权信息
if(!authen){
throw new RuntimeException("您无权访问页面,请以合适身份登陆后查看");
}
filterChain.doFilter(servletRequest,servletResponse);
}
}
@Override
public void destroy() {
}
}
数据校验-权限管理
SpringMVC:Spring Security提供了“基于URL的访问控制”和“基于Method的访问控制”。
-
在用户进行操作时,从session中获取用户id,将传入的参数与用户的身份做绑定校验。
<sec:http> <sec:intercept-url parttern="/persident_portal/*" access="RILE_PERSIDENT"/> <sec:intercept-url parttern="/manager_portal/*" access="RILE_MANAGER"/> <sec:intercept-url parttern="/**" access="RILE_USER"/> <sec:form-login /> <sec:logout /> </sec:http>
数据校验-不安全的网络传输
http协议属于明文传输协议,交互过程以及数据传输都没有进行加密,通信双方也没有进行任何认证,通信过程非常容易遭遇劫持、监听、篡改,严重情况下,会造成恶意的流量劫持等问题,甚至造成个人隐私泄露(比如银行卡卡号和密码泄露)等严重的安全问题。
HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。
****对称加密
非对称加密
https协议(http+ssl协议),如下图所示为其连接过程:
中间人攻击
数字证书:解决中间人攻击
数字证书是一个经证书授权中心数字签名的包含公开密钥拥有者信息以及公开密钥的文件
客户端拿到证书后,根据证书用第三方的私钥进行上的方法自己生成一个证书编号,如果自己生成的证书编号与证书上的证书编号相同,那
么说明这个证书是真实的。同时,为避免证书编号本身又被调包,所以使加密。
总结
HTTPS要使客户端与服务器端的通信过程得到安全保证,必须使用的对称加密算法,但是协商对称加密算法的过程,需要使用非对称加密算法
来保证安全,然而直接使用非对称加密的过程本身也不安全,会有中间人篡改公钥的可能性,所以客户端与服务器不直接使用公钥,而是使用
数字证书签发机构(CA)颁发的证书来保证非对称加密过程本身的安全,为了保证证书不被篡改,引入数字签名,客户端使用相同的对称加
密算法,来验证证书的真实性,如此,最终解决了客户端与服务器端之间的通信安全问题。
数据校验-不安全的网络传输
规则1.11:敏感数据在跨信任域之间传递采用签名加密传输
敏感数据传输过程中要防止窃取和恶意篡改。使用安全的加密算法加密传输对象可以保护数据。这就是所谓的对对象进行密封。而对密封的对象进行数字签名则可以防止对象被非法篡改,保持其完整性
public static void main(String[] args) throwsIOException,ClassNotFoundException{
// Build map
SerializableMap<String, Integer> map = buildMap();
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(map);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
map = (SerializableMap<String, Integer>) in.readObject();
in.close();
// Inspect map
InspectMap(map);
}
反例:
public static void main(String[] args) throwsIOException,GeneralSecurityException, ClassNotFoundException{
// Build map
SerializableMap<String, Integer> map = buildMap();
// Generate sealing key & seal map
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(newSecureRandom());
Key key= generator.generateKey();
Cipher cipher= Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
SealedObject sealedMap= new SealedObject(map, cipher);
// Serialize map
ObjectOutputStreamout = new ObjectOutputStream(newFileOutputStream("data"));
out.writeObject(sealedMap);
out.close();
// Deserialize map
ObjectInputStream in = newObjectInputStream(newFileInputStream("data"));
sealedMap= (SealedObject) in.readObject();
in.close();
// Unseal map
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
map = (SerializableMap<String, Integer>) sealedMap.getObject(cipher);
// Inspect map
InspectMap(map);
}
public static void main(String[] args) throwsIOException, GeneralSecurityException, ClassNotFoundException{
SerializableMap<String, Integer> map = buildMap();
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(newSecureRandom());
Key key= generator.generateKey();
Cipher cipher= Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
SealedObjectsealedMap= newSealedObject(map, cipher);
KeyPairGeneratorkpg= KeyPairGenerator.getInstance("RSA");
KeyPair kp= kpg.generateKeyPair();
Signature sig = Signature.getInstance("SHA256withRSA");
SignedObject signedMap= newSignedObject(sealedMap, kp.getPrivate(), sig);
ObjectOutputStreamout = newObjectOutputStream(newFileOutputStream("data"));
out.writeObject(signedMap);
out.close();
ObjectInputStream in = newObjectInputStream(newFileInputStream("data"));
signedMap= (SignedObject)
in.readObject();
in.close();
if(!signedMap.verify(kp.getPublic(), sig)){
throw new GeneralSecurityException("Map failed verification");
}
sealedMap= (SealedObject) signedMap.getObject();
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
map = (SerializableMap<String, Integer>) sealedMap.getObject(cipher);
InspectMap(map);
}
正例:
public static void main(String[] args) throws IOException, GeneralSecurityException, ClassNotFoundException{
SerializableMap<String, Integer> map = buildMap();
KeyPairGenerator kpg= KeyPairGenerator.getInstance("RSA");
KeyPair kp= kpg.generateKeyPair();
Signature sig = Signature.getInstance("SHA256withRSA");
SignedObject signedMap= new SignedObject(map, kp.getPrivate(), sig);
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(new SecureRandom());
Key key= generator.generateKey();
Cipher cipher= Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
SealedObject sealedMap = new SealedObject(signedMap, cipher);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(sealedMap);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap= (SealedObject) in.readObject();
in.close();
// Unseal map
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
signedMap= (SignedObject) sealedMap.getObject(cipher);
// Verify signature and retrieve map
if(!signedMap.verify(kp.getPublic(), sig)){
throw new GeneralSecurityException("Map failed verification");
}
map = (SerializableMap<String, Integer>) signedMap.getObject();
// Inspect map
InspectMap(map);
}
敏感信息
敏感信息-常见的敏感信息
规则2.1:禁止在日志中明文保存用户敏感数据
日志中如明文保存用户敏感数据,容易泄露给运维人员或者攻破系统的攻击者
规则2.2:禁止将敏感信息硬编码在程序中
如果将敏感信息(包括口令和加密密钥)硬编码在程序中,可能会将敏感信息暴露给攻击者。任何能够访问到class
文件的人都可以反编译class文件并发现这些敏感信息
...
DriverManager.getConnection(url,"soctt","tiger")
...
Java反编译
javap c Connmngr.class
ldc #36://String jdbc:mysql://ixne.com/rxsql
ldc #38://String scott
ldc #17://String tiger
反例:
public class IPaddress{
private String ipAddress= "172.16.254.1";
public static voidmain(String[] args){
//...
}
}
正例:
public class IPaddress{
public static void main(String[] args) throws IOException{
char[] ipAddress= new char[100];
BufferedReader br= new BufferedReader(newInputStreamReader(newFileInputStream("serveripaddress.txt")));
// Reads the server IP address into the char array,
// returns the number of bytes read
intn = br.read(ipAddress);
// Validate server IP address
// Manually clear out the server IP address
// immediately after use
for(inti= n -1; i>= 0; i--){
ipAddress[i] = 0;
}
br.close();
}
}
规则2.3:加密传输邮件-邮件传输时需使用安全协议SSL/TLS加密传输,避免攻击者在网络上嗅探到用户数据
反例:邮件传输时未使用TLS协议
public class SendMailTLS{
public static void main(String[] args) {
final String username="username@gmail.com";
final String password="password";
Properties props=new Properties();
//使用TLS
//props.put("mail.smtp.auth","true");
//props.put("mail.smtp.startls.enable","true");
//使用SSL
//props.put("mail.smtp.socketFactory,port","465");
//props.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory");
//props.put("mail.smtp.auth","true");
props.put("mail.smtp.host","smtp.gmail.com");
props.put("mail.smtp.port","587");
Session session=Session.getInstance(props,new javax.mail.Authenticator){
protected PasswordAuthentication getPasswordAuthentication(){
return new PasswordAuthentication(username,password);
}
});
}
}
正例:加密使用TLS协议
public class SendMailTLS{
public static void main(String[] args) {
final String username="username@gmail.com";
final String password="password";
Properties props=new Properties();
//使用TLS
props.put("mail.smtp.auth","true");
props.put("mail.smtp.startls.enable","true");
//做服务器证书校验
props.put("mail.smtp.ssl.checkserveridentity","true");
//添加信任的服务器地址,多个地址之间用空格分开
props.put("mail.smtp.ssl.trust","smtp.gmail.com");
props.put("mail.smtp.host","smtp.gmail.com");
props.put("mail.smtp.port","25");
Session session=Session.getInstance(props,new javax.mail.Authenticator){
protected PasswordAuthentication getPasswordAuthentication(){
return new PasswordAuthentication(username,password);
}
});
}
}
规则2.4:基于hash算法的口令存储必须加盐值(salt),并且使用标准的迭代PBKDF2
如果不加盐值,会导致相同的密码得到相同的Hash值
public class PasswordHash{
public static final String PBKDF2_ALGORITHM="PBKDF2WithHmacSHA1";
public static final int SALT_BYTE_SIZE=24;
public static final int HASH_BYTE_SIZE=24;
public static final int PBKDF2_ITERATIONS=1000;
public static String createHash(char[] password)
throws NoSuchAlgorithmException,InvalidKeySpecException{
//Generate a random salt
SecureRandom random =new SecureRandom();
byte[] salt =new btye[SALT_BYTE_SIZE];
random.nextBytes(salt);
// Hash the password
byte[] hash =pbkdf2(password,salt,PBKDF2_ITERATIONS,HASH_BYTE_SIZE);
//format iterations:salt:hash
return PBKDF2_ITERATIONS+":"+toHex(hash);
}
}
加密算法
加密算法-加密算法总览
规则3.1不安全加密算法-禁止使用不安全的加密算法DES\3DES
加密算法应该使用安全的加密算法AES攻击者能够破解不安全的加密算法,获取到敏感信息
反例:使用了不安全算法DES
byte[] result = DES.encrypt(str.getBytes(),password);
//直接将如上内容解密
try{
byte[] decryResult =DES.decrypt(result,password);
System.out.println("解密后:"+new String(decryResult));
}catch(Excption e1){
e1.printStackTrace();
}
规则3.3:对称加密算法AES-禁止使用AES中不安全的分组模式ECB,推荐使用不仅提供加密并且还提供完整性校验的AES-GCM
AES分组密码模式还有:AES-CBC\AES-CFB\AES-OFB\AES-CTR(AES-CTR由于能并行计算,效率最高)
反例:使用了AES中不安全的分组模式ECB
pubilc class AES{
//加密
public static String Encrypt(String sSrc,String sKey) throws Excetion{
if(sKey==null){
System.out.print("key为空null");
return null;
}
//判断key是否为16位
if(key.length()!=16){
System.out.print("key长度不是16位");
return null;
}
byte[] raw =sKey.getBytes("UTF-8");
SecreKeySpec skeySpec=new SecreKeySpec(raw,"AES");
Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");//算法/模式/补码方式
cipher.init(Cipher.ENCRYPT_MODE,skeySpec);
byte[] encrypted= cipher.doFinal(sScr.getByte("utf-8"));
return new Base64().encodeToString(encrypted);//此处使用Base64做转码功能,同时能起到2次加密作用
}
}
正例:使用AES算法中安全的分组模式CBC
pubilc class AES{
//加密
public static String Encrypt(String sSrc,String sKey) throws Excetion{
if(sKey==null){
System.out.print("key为空null");
return null;
}
//判断key是否为16位
if(key.length()!=16){
System.out.print("key长度不是16位");
return null;
}
byte[] raw =sKey.getBytes("UTF-8");
SecreKeySpec skeySpec=new SecreKeySpec(raw,"AES");
Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");//算法/模式/补码方式
IvParameterSpec iv =new IvParameterSpec(sKey.getByte());//使用CBC模式,需要一个向量iv
//可增加加密算法的强度
cipher.init(Cipher.ENCRYPT_MODE,skeySpec,iv);
byte[] encrypted= cipher.doFinal(sScr.getByte("utf-8"));
return new Base64().encodeToString(encrypted);//此处使用Base64做转码功能,同时能起到2次加密作用
}
}
规则3.4:非对称加密算法RSA-非对称加密算法RSA的使用需要注意长度至少为2048位
RSA的密钥长度如果低于2048位,达不到安全标准
反例:RSA算法的密钥长度只有1024位
public static HashMap<String,Object>getKeys()throws NoSuchAlgorithmException{
HashMap<String,Object> map=new HashMap<String,Object>;
KeyPairGenerator keyPairGen=KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(1024);
KeyPair keyPair=keyPairGen.generateKeyPair();
RSAPublicKey pubilcKey=(RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey =(RSAPrivateKey) keyPair.getPrivate();
map.put("public",pubilcKey);
map.put("private",privateKey);
return map;
}
正例:RSA算法的密钥长度至少2048位
public static HashMap<String,Object>getKeys()throws NoSuchAlgorithmException{
HashMap<String,Object> map=new HashMap<String,Object>;
KeyPairGenerator keyPairGen=KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(2048);
KeyPair keyPair=keyPairGen.generateKeyPair();
RSAPublicKey pubilcKey=(RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey =(RSAPrivateKey) keyPair.getPrivate();
map.put("public",pubilcKey);
map.put("private",privateKey);
return map;
}
规则3.5:敏感数据加密使用强随机数
伪随机数生成器具有可移植性和可重复性,攻击者可以在系统的一些安全脆弱点上监听,并构建相应的查询表预测将要使用的seed值,从而去预测相关的敏感数据
伪随机数示例:
import java.util.Random;
public class RandomDemo{
Random random1=new Random(100);
System.out.println(random1.nextInt());
System.out.println(random1.nextFloat());
System.out.println(random1.nextBoolean());
Random random2=new Random(100);
System.out.println(random2.nextInt());
System.out.println(random2.nextFloat());
System.out.println(random2.nextBoolean());
}
import java.io.UnsupporedEncodeingException;
import java.util.Random;
public class SecureRandom{
public static void main(String[] args)throws UnsupporedEncodeingException{
Random ranGen=new Random();
byte[] aesKey=new byte[20];
ranKey.nextBytes(aesKey);
StringBuffer hexString =new StringBuffer();
for (int i=0;i<aesKey.length;i++){
String hex=Integer.toHexString(0xff&aesKey[i]);
if(hex.length()==1)
hexString.append(''0);
hexString.append(hex);
}
System.out.println(hexString);
}
}
强随机数示例:
import java.io.UnsupporedEncodeingException;
import java.util.Random;
import java.security.SecureRandom;
public class SecureRandom{
public static void main(String[] args)throws UnsupporedEncodeingException{
Random ranGen=new SecureRandom();
byte[] aesKey=new byte[20];
ranKey.nextBytes(aesKey);
StringBuffer hexString =new StringBuffer();
for (int i=0;i<aesKey.length;i++){
String hex=Integer.toHexString(0xff&aesKey[i]);
if(hex.length()==1)
hexString.append(''0);
hexString.append(hex);
}
System.out.println(hexString);
}
}
序列化与反序列化
- Java 序列化是指把Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的writeObject()方法可以实现序列化。
- Java 反序列化是指把字节序列恢复为Java 对象的过程,ObjectInputStream 类的readObject() 方法用于反序列化。
public class Test{
public static void main(String[] args) throws Exception{
//定义myObj对象
MyObject myObj=new MyObject();
//创建一个包含对象进行反序列化信息的“object”数据文件
FileOutputStream fos=new FileOutputStream("object");
ObjectOutputStream os =new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入objct文件中
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis=new FileInputStream("object");
ObjectInputStream ois =new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk=(MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close;
}
}
class MyObject implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOExeption{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("open /Application/Calcultor.app")
}
}
序列化与反序列化-反序列化漏洞
类ObjectInputStream在反序列化时,应对生成的对象的类型做限制
反例:不安全的反序列化操作,导致执行任意命令、控制服务器
ServerSocket serverSocket =new ServerSocket(Integer,parseInt("9999"));
while(true){
Socket socket=serverSocket.accpet();
ObjectInputStream objectInputStream=new ObjectInputStream(socket.getInputStream());
try{
Object object=objectInputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
规则4.1:类ObjectInputStream在反序列化时,对生成的对象的类型做限制
public final class SecureObjectInputStram extends ObjectInputStream{
public SecureObjectInputStram() throws IOException{
super();
}
public SecureObjectInputStram( InputStream in) throws IOException{
super(in);
}
portected Class<?>resolveClass(ObjectStreamClass desc) throws ClassNotFoundException,IOException{
if(!desc.getName().equals("java.security.Person")){
throws new ClassNotFoundException(desc.getName()+"not found");
}
return super.resolveClass(desc);
}
}
序列化与反序列化-不安全的反序列化漏洞解决方案
解决方案:
- 使用安全的反序列化插件,对于业界爆出的反序列化漏洞,及时更新插件和打补丁。
- 传输的数据应该加密,并且在后台进行数据校验,保证数据没有被篡改。
- 自定义ObjectInputStream, 重载resolveClass的方法,对className进行白名单校验。
- Java9可以继承java.io.ObjectInputFilter类重写checkInput方法实现自定义的过滤器。
- 通过扩展SecurityManager 来实现禁止JVM 执行外部命令Runtime.exec。
I/O操作
规则5.1:文件上传应根据业务的需要限定文件的格式和大小
反例:未限定格式
@RequestMapping(value="/upload",method=RequestMethod.POST)
public String upload(HttpServletRequest request, @RequestParam("description") String description,
@RequestParam("file") MultipartFile file)throws Exception{
if(!file.isEmpty()){
String path=request.getServletContext().getRealPath("/images/");
//上传文件名
String filename=file.getOriginalFilename();
File filepath=new File(path,filename);
//判断路径是否存在,如果不存在就创建一个
if(!filepath.getParentFile().exists()){
filepath.getParentFile.mkdirs();
}
//将文件上传保存到一个目标文件中
file.transferTo(new File(path+File.separator+filename));
return "success"
}else{
return "error";
}
}
正例:限定文件的上传的格式
@RequestMapping(value="/upload",method=RequestMethod.POST)
public String upload(HttpServletRequest request, @RequestParam("description") String description,
@RequestParam("file") MultipartFile file)throws Exception{
if(!file.isEmpty()){
String path=request.getServletContext().getRealPath("/images/");
//上传文件名
String filename=file.getOriginalFilename();
//还有一种方式filenameUtils.getExtension();
String suffix=filename.substring(filename.lastIndexOf(".")+1)
if(suffix!="jpg"){
File filepath=new File(path,filename);
//判断路径是否存在,如果不存在就创建一个
if(!filepath.getParentFile().exists()){
filepath.getParentFile.mkdirs();
}
//将文件上传保存到一个目标文件中
file.transferTo(new File(path+File.separator+filename));
}
return "success" ;
}else{
return "error";
}
}
规则5.2:路径遍历-文件下载的地方,应对文件的路径进行校验,或者使用文件id映射到文件的方式下载文件
反例:
protected void doGet(HttpServletRequest request,HttpServeltResponse response)
throws ServeltExceptio,IOException{
//获取项目部署绝对路径下的upload文件夹路径,下载upload目录下的文件
String root =request.getServeltContext().getRealPath("/upload");
//获取文件名
String filename=request.getParameter("filename");
//根据文件路径创建输入流
File file=new File(root+"/"+filename);
FileInputStream fis= new FileInputStream(file);
//设置响应头
response.addHeader("Content-Disposition","attachment;filename="+new String(filename.getBytes()));
response.addHeader("Content-Length",""+file.length());
byte[] b =new byte[fis.availabe()];
fis.read(b);
response.getOutStream().write(b);
}
正例:使用白名单校验文件名
protected void doGet(HttpServletRequest request,HttpServeltResponse response)
throws ServeltExceptio,IOException{
//获取项目部署绝对路径下的upload文件夹路径,下载upload目录下的文件
String root =request.getServeltContext().getRealPath("/upload");
//获取文件名
String filename=request.getParameter("filename");
if(filename==FILENAME){
//根据文件路径创建输入流
File file=new File(root+"/"+filename);
FileInputStream fis= new FileInputStream(file);
//设置响应头
response.addHeader("Content-Disposition","attachment;filename="+new String(filename.getBytes()));
response.addHeader("Content-Length",""+file.length());
byte[] b =new byte[fis.availabe()];
fis.read(b);
response.getOutStream().write(b);
}
}
规则5.3:未释放的流资源(Unreleased Resource,比如Streams使用文件、IO流、数据库连接等)主动释放的资源
使用文件、IO流、数据库连接等不会自动释放的资源时,未在使用完毕后马上将其关闭,关闭资源的代码应在try...catch...finally{if (fos != null) {fos.close();}}的finally内执行
private static void TestCloseFileStream(){
String fileName="";
//声明引用
InputStream inputStream=null;
try{
inputStream=new FileInputStream(filename);
}catch(IOException e){
//do something
}finally{
if(inputStream!=null){
try{
//关闭流
inputStream.close();
}catch(IOException e){
//do something
}
}
}
}
规则5.4:临时文件使用完毕应及时删除
反例:
public class TempFile{
public static void main(STring[] args) throws IOExcption{
File f =new File("tempnam.tmp");
if(f.exists()){
System.out.println("This file already exists");
return;
}
FileOutputStream fop=null;
try{
fop=new FileOutputStream(f);
String str="Data";
fop.write(str.getBytes());
}finally{
if(fop!=null){
try{
fop.close();
}catch(IOException e){
// handle error
}
}
}
}
}
正例:
public class TempFile{
public static void main(STring[] args) throws IOExcption{
File f =new File("tempnam.tmp");
if(f.exists()){
System.out.println("This file already exists");
return;
}
FileOutputStream fop=null;
try{
fop=new FileOutputStream(f);
String str="Data";
fop.write(str.getBytes());
}finally{
if(fop!=null){
try{
fop.close();
}catch(IOException e){
// handle error
}
//delete file when finished
if(!f.delete()){
//log the error
}
}
}
}
}
多线程安全(Double-Checked Locking)
规则6.1:多线程下采用加锁机制,防止多线程操作产生的死锁
双重锁机制(多线程下不安全)
public class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton==null){
synchronized (Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
singleton = new Singleton()非原子性
1. memory=allocate();//1:分配对象的内存空间
2. ctorInstance(memory);//2:初始化对象
3. singleton=memory;//3:设置instance指向刚分配的内存地址
指令重排后:
1. memory=allocate();//1:分配对象的内存空间
2. singleton=memory;//3:设置instance指向刚分配的内存地址//注意,此时对象还没有被初始化!
3. ctorInstance(memory);//2:初始化对象
多线程安全(Double-Checked Locking)
对方法使用synchronized关键字
public class Singleton{
private static Singleton singleton;
private Singleton(){}
public static synchronized Singleton getSingleton(){
if(singleton==null){
singleton=new Singleton();
}
return singleton;
}
}
提前初始化
public class Singleton{
private static class SingletonHolder{
private static final Singleton singleton=new Singleton();
}
private Singleton(){}
public static final Singleton getSingleton(){
return SingletonHolder.INSTANCE;
}
}
框架和组件-框架和组件安全
规则7.1:使用安全版本的框架和组件,官网下载,使用框架和组件,先到网上搜扫下是否有公开的漏洞
比如:
--WebLogic、Struts2、Nginx
--Fastjson、ImageMagick、Log4j
规则7.2:禁止使用来源不明的框架或者组件
规则7.3:及时更新框架和组件版本
异常行为-异常信息暴露到外部
规则8.1:异常信息禁止暴露到前端
try{
FileInputStream fis =new FileInputStream(System.getenv("APPDATA")+args[0]);
}catch(FileNotFoundException e){
//Log the exception
throws new IOException ("Unable to retrieve file",e);
}
解析:产生固定的错误信息,未泄露异常信息到外部