下述操作将会以MySql数据库为例
引用的是ums库中的t_user表
表中只有一条数据,username与password的数据均为admin
1.简介
SQL注入攻击是比较常见的网络攻击方式之一
它不是利用操作系统的BUG来实现攻击,而是发生于应用程序之数据库层的安全漏洞。
针对程序员编写时的疏忽通过SQL语句,实现无账号登录,甚至篡改数据库
简单来讲,是在输入的字符串之中注入SQL的指令
那么这些注入进去的指令就会被数据库服务器误认为是正常的SQL指令而运行
因此遭到破坏或是入侵。
最常见的就是我们在应用程序中使用字符串联结方式组合 SQL 指令
有心之人就会写一些特殊的符号,恶意篡改原本的 SQL 语法的作用,达到注入攻击的目的。
// 比如验证用户登录需要 username 和 password,编写的 SQL 语句如下:
select * from t_user where (username = '"+ username +"') and (password = '"+ password +"');
// username 和 password 字段被恶意填入
username = "1' OR '1'='1";
password = "1' OR '1'='1";
// 将导致原本的 SQL 字符串被填为:
select * from t_user where (username = '1' or '1'='1') and (password = '1' or '1'='1');
// 此时实际上运行的sql为
select * from t_user;
// 也就是不再需要 username 和 password 账密即达到登录的目的,结果不言而喻。
public static void main(String[] args) {
// 此时真实数据库中只有一个用户,username与password均为admin
login("admin","admin"); //登录成功
login("admin","111"); // 登录失败
login("1' or '1' = '1","1' or '1' = '1"); //登录成功
}
public static void login(String username,String password){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
String url = "jdbc:mysql://127.0.0.1:3306/ums?useUnicode=true&characterEncoding=utf-8";
User user = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(url,"root","");
String sql = new StringBuffer()
.append(" select id,username,password,phone,address ")
.append(" from t_user ")
.append(" where username = '"+ username +"' ")
.append(" and password = '"+ password +"' ")
.toString();
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while(rs.next()){
System.out.println("登录成功");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("登录失败");
}
2.解决sql注入问题
MySQL的驱动提供了预编译语句的支持,不同的程序语言,都分别有使用预编译语句的方法
实际上,绑定变量使用预编译语句是预防SQL注入的最佳方式
使用预编译的SQL语句语义不会发生改变
在SQL语句中,变量用问号?表示,黑客即使本事再大,也无法改变SQL语句的结构
public static void main(String[] args) {
login("admin","admin"); //登录成功
login("admin","111"); // 登录失败
login("'1' or '1' = '1'","'1' or '1' = '1'"); // 登录失败
}
public static void login(String username,String password){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
String url = "jdbc:mysql://127.0.0.1:3306/ums?useUnicode=true&characterEncoding=utf-8";
User user = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(url,"root","");
String sql = new StringBuffer()
.append(" select id,username,password,phone,address ")
.append(" from t_user ")
.append(" where username = ? ")
.append(" and password = ? ")
.toString();
ps = conn.prepareStatement(sql);
ps.setString(1,username);
ps.setString(2,password);
rs = ps.executeQuery();
while(rs.next()){
System.out.println("登录成功");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("登录失败");
}
3.MyBatis中的Sql注入问题
我们使用 mybatis 编写 SQL 语句时,难免会使用模糊查询的方法,mybatis 提供了两种方式 #{} 和 ${}
当用户使用${param}进行参数传递时,表示使用拼接字符串的方式进行参数的注入
会将接受到参数的内容不加任何修饰符拼接在 SQL 中,将引起 SQL 注入问题。
<!-- sql注入问题 -->
<select id="selectByUsernameAndPassword4" resultType="user">
select <include refid="userColumn"></include>
from t_user
where username = ${username}
and password = ${password}
</select>
// 在遇到sql注入后,相当于查询了所有
public List<User> selectByUsernameAndPassword4(@Param("username") String username, @Param("password") String password);
// 测试
SqlSession session = null;
try {
session = MyBatisUtil.getSession();
UserDao userDao = session.getMapper(UserDao.class);
List<User> users = userDao.selectByUsernameAndPassword4("'1' or '1'='1'","'1' or '1'='1'");
// 此时查询了所有的t_user表中的数据
// 可以将此时的sql语句看做:select * from user
System.out.println(users);
session.commit();
} catch (Exception e) {
e.printStackTrace();
session.rollback();
} finally {
MyBatisUtil.close();
}
运行后控制台效果如下
根据最终展现效果我们可以发现
此时是将参数作为字符串拼接的形式存在
在原本代码中只存在两个查询条件
但是拼接之后最终将会产生四个条件
由于多的两个条件是通过or连接的
最终运行效果相当于select * from t_user
4.MyBatis解决sql注入问题
使用#{param}的方式进行参数传递时
在预处理的时候会把参数部分用一个占位符?替代
其中 param表示接受输入参数的名称。
这样能够有效的解决 SQL 注入问题
从而避免SQL注入的问题出现
<!-- 避免sql注入 -->
<select id="selectByUsernameAndPassword5" resultType="user">
select <include refid="userColumn"></include>
from t_user
where username = #{username}
and password = #{password}
</select>
SqlSession session = null;
try {
session = MyBatisUtil.getSession();
UserDao userDao = session.getMapper(UserDao.class);
List<User> users = userDao.selectByUsernameAndPassword5("'1' or '1'='1'","'1' or '1'='1'");
// 此时将传递的参数整体作为一个字符串
// 即username的值为:'1' or '1'='1'
// password的值为:'1' or '1'='1'
// 因此没有查询到对应的数据
session.commit();
} catch (Exception e) {
e.printStackTrace();
session.rollback();
} finally {
MyBatisUtil.close();
}
运行后控制台效果如下
根据最终展现效果我们可以发现
此时在预处理的时候会把参数部分用一个占位符?替代
传递的参数作为一个整体存在
最终在运行时仍然是原本的两个查询结果
从而避免SQL注入的问题出现