SQL注入攻击是黑客攻击网站最常用的手段。开源Web应用程序安全项目(OWASP)在最近发布的OWASP Top 10 - 2017中继续将注入攻击(包含SQL注入及其它注入方式)作为十大最严重的Web应用程序安全风险榜的榜首。
SQL注入攻击之所以成为最常见的攻击方式,通常是因为数据库中存储了应用程序所有重要的、有价值的数据,比如用户信息。在过去的几年里,有百分之九十的数据泄漏事件都与这种手段有关。
虽然SQL注入攻击如此常见,但要避免代码中出现SQL注入漏洞却是极其简单的。
SQL注入漏洞产生的原因是,程序员们在动态创建数据库查询语句时直接拼接了未经检查的用户输入数据。例如:
场景1:应用程序在创建下面的SQL查询语句时直接拼接不可信数据:
String query = "SELECT * FROM accounts WHERE custID='" + request.getParameter("id") + "'";
场景2:同样的,在使用框架(如Hibernate(HQL))创建SQL查询语句时直接拼接不可信的用户输入数据,也可能导致查询语句的漏洞。
Query HQLQuery= session.createQuery("FROM accounts WHERE custID='" + request.getParameter("id") + "'");
在这两个案例中,攻击者在浏览器中将“id”参数的值修改成:' or '1'='1。例如:
http://example.com/app/accountView?id=' or '1'='1
这样查询语句的意义就变成了从accounts表中返回所有的记录。更危险的攻击可能导致数据被篡改甚至是存储过程被调用。
要避免产生SQL注入漏洞,只需要做到以下两点之一即可:
- 不要使用动态查询
- 防止用户输入的数据包含恶意SQL
主要的防护方法有:
- 使用预处理语句(参数化查询)
- 使用存储过程
- 使用白名单输入校验
- 对所有用户输入数据进行编码
以下方法可作为辅助防护:
- 强制使用最小权限
- 使用白名单输入校验作为辅助防护
下面来看一些具体的例子:
JDBC的安全使用
不安全的代码示例:
Connection conn = [...]; | |
Statement stmt = con.createStatement(); | |
ResultSet rs = stmt.executeQuery("update COFFEES set SALES = "+nbSales+" where COF_NAME = '"+coffeeName+"'"); |
安全的写法:使用prepareStatement预处理语句执行参数化查询
Connection conn = [...]; | |
conn.prepareStatement("update COFFEES set SALES = ? where COF_NAME = ?"); | |
updateSales.setInt(1, nbSales); | |
updateSales.setString(2, coffeeName); |
Spring JDBC的安全使用
不安全的代码示例:
JdbcTemplate jdbc = new JdbcTemplate(); | |
int count = jdbc.queryForObject("select count(*) from Users where name = '"+paramName+"'", Integer.class); |
安全的写法:绑定变量,参数化查询
JdbcTemplate jdbc = new JdbcTemplate(); | |
int count = jdbc.queryForObject("select count(*) from Users where name = ?", Integer.class, paramName); |
Hibernate(HQL)的安全使用
不安全的代码示例:
Session session = sessionFactory.openSession(); | |
Query q = session.createQuery("select t from UserEntity t where id = " + input); | |
q.execute(); |
安全的写法1:绑定变量,参数化查询
Session session = sessionFactory.openSession(); | |
Query q = session.createQuery("select t from UserEntity t where id = :userId"); | |
q.setString("userId",input); | |
q.execute(); |
安全的写法2:使用Hibernate Criteria实现like查询条件
Session session = sessionFactory.openSession(); | |
Query q = session.createCriteria(UserEntity.class) | |
.add( Restrictions.like("id", input) ) | |
.list(); | |
q.execute(); |
JDO的安全使用
不安全的代码示例:
PersistenceManager pm = getPM(); | |
Query q = pm.newQuery("select * from Users where name = " + input); | |
q.execute(); |
安全的写法:参数化查询
PersistenceManager pm = getPM(); | |
Query q = pm.newQuery("select * from Users where name = nameParam"); | |
q.declareParameters("String nameParam"); | |
q.execute(input); |
JPA的安全使用
不安全的代码示例:
EntityManager pm = getEM(); | |
TypedQuery<UserEntity> q = em.createQuery( | |
String.format("select * from Users where name = %s", username), | |
UserEntity.class); | |
UserEntity res = q.getSingleResult(); |
安全的写法:参数化查询
TypedQuery<UserEntity> q = em.createQuery( | |
"select * from Users where name = usernameParam",UserEntity.class) | |
.setParameter("usernameParam", username); | |
UserEntity res = q.getSingleResult(); |
Android SQL的安全使用
不安全的代码示例:
String query = "SELECT * FROM messages WHERE uid= '"+userInput+"'" ; | |
Cursor cursor = this.getReadableDatabase().rawQuery(query,null); |
安全的写法:
String query = "SELECT * FROM messages WHERE uid= ?" ; | |
Cursor cursor = this.getReadableDatabase().rawQuery(query,new String[] {userInput}); |