SQL注入是将Web页面的原URL、表单域或数据包输入的参数,修改拼接成SQL语句,传递给Web服务器,最终达到欺骗服务器执行恶意的SQL命令进而传给数据库服务器以执行数据库命令。如Web应用程序的开发人员对用户所输入的数据或cookie等内容不进行过滤或验证(即存在注入点)就直接传输给数据库,就可能导致拼接的SQL被执行,获取对数据库的信息以及提权,发生SQL注入攻击。
SQL注入使攻击者绕过认证环节,直接控制远程服务器上的数据库。SQL是一种结构化查询语句,SQL语法允许数据库命令与用户数据混杂在一起,那么用户数据极有可能被解释成命令。目前,大多数web应用都在使用SQL数据库来存放应用程序数据。这样攻击者不仅能在web应用上输入数据,还可以在数据库上执行任意命令。
SQL攻击主要分为两种形式:
注入过程的攻击方式是提前终止文本字符串,之后再新追加一个命令。由于插入的命令可能在执行前追加其他字符串,所以,攻击者会用‘- -’注释标记来终止注入的字符串,执行时该段文本将不会被编译和执行。
以Oracle数据库为例。当用户登录时,首先Web页面接收用户输入的用户名(username)和密码(password),并动态生成一个SQL语句,通过用户名(username)和密码(password)查询数据库中userformation表,如果该查询访问成功,将返回一个用户记录信息,且用户登录成功。
以下面的代码演示
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
Insert title here
<%
String userName=request.getParameter("userName").trim();
String psWord=request.getParameter("psWord").trim();
Connection conn=null;
Statement stmt=null;
// PreparedStatement pstmt = null ;
ResultSet rs = null ;
try {
//加载驱动
Class.forName("oracle.jdbc.driver.OracleDriver");
//建立连接
conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:ORCL","scott","116688");
//发送sql语句
stmt=conn.createStatement();
//建立ResultSet结果集对象,执行sql语句
String sql = "select username,password from userformation where username='"+userName+"' AND password='"+psWord+"' ";
rs=stmt.executeQuery(sql);
if(rs.next()){
%>
<%
}
else {
%>
<% }
System.out.print(sql);
rs.close(); //关闭ResultSet对象
}
catch (Exception e) {
out.println(e.getMessage());
}
stmt.close(); //关闭Statement对象
conn.close(); //关闭Connection对象
%>
这里sql = “select username,password from userformation where username=’”+userName+"’ AND password=’"+psWord+"’ ";使用了sql拼接的方式进行用户验证。
首先来一个简单的SQL注入,在用户名栏输入
’ or 1=1-- ,密码栏随便输或者不填,并在控制台输出执行的sql语句和数据库第一个用户的结果System.out.println(sql);
System.out.println(rs.getString(“username”)+" "+rs.getInt(“password”));
其中1=1 是true ,and password=’ ’ 等语句被‘- -’注释了,不会被编译执行,完全跳过了sql 验证,直接输出用户名和密码,此时攻击者就可使用数据库中的第一个用户登录。
堆叠注入。在sql中用分号(;)表示一个语句的结束。在sql中可以执行完一个语句后以‘;’结束后又可继续构造一条语句继续执行。
例如:当攻击者在登录页面输入如下语句:
username:' ; drop table 表名 - -
password:(任意输入)
这时会提示登录失败,但此时数据库中的表也被删除,导致任何用户均不可登录。(堆叠注入只能在 MySQL SQL Server 中使用,Oracle不能使用堆叠注入,当有两条语句执行时将会报‘无效字符’错误,在此就不做演示)。
从以上演示我们知道,某些攻击者会利用这些漏洞进行一些非法操作,导致数据库数据遭到破坏,用户信息泄露等,所以防止sql注入是非常重要的。但怎样才能防止sql注入呢?
PreparedStatement类是java的一个类,准确说是jdbc规范中的一个接口,是一种预处理对象,可防止sql注入,提高安全性,其原理如下:(这里以MySQL数据库为例)
测试代码:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
Insert title here
<%
String userName=request.getParameter("userName").trim();
String psWord=request.getParameter("psWord").trim();
Connection conn=null;
//Statement stmt=null;
PreparedStatement pstmt = null ;
ResultSet rs = null ;
try {
//加载驱动
Class.forName("com.mysql.jdbc.Driver");
//建立连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatebase?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC","root","116688csF");
//发送sql语句
//stmt=conn.createStatement();
//建立ResultSet结果集对象,执行sql语句
String sql = "select username,password from userformation where username=? AND password=?";
pstmt=conn.prepareStatement(sql);
pstmt.setString(1, userName);
pstmt.setString(2,psWord);
rs=pstmt.executeQuery();
String rsq = ((PreparedStatement)pstmt).toString();
System.out.println(rsq);
if(rs.next()){
System.out.println(rs.getString(1)+" "+rs.getString(2));
%>
<%
}
else {
%>
<% }
}
catch (Exception e) {
out.println(e.getMessage());
} finally {
try{
if(rs!=null) rs.close(); //关闭ResultSet对象
if(pstmt!=null) pstmt.close(); //关闭PreparedStatement对象
if(conn!=null) conn.close(); //关闭Connection对象
}catch(Exception s){
out.println(s.getMessage());
}
}
%>
一开始也用 在用户名栏输入 ’ or 1=1-- ,密码栏随意;并打印出执行的sql 语句
String rsq = ((PreparedStatement)pstmt).toString();
System.out.println(rsq);
com.mysql.jdbc.JDBC42PreparedStatement@d7fb614: select username,password from userformation where username='\' or 1=1 --' AND password=''
点击登录提示登录失败,sql 语句如上所示。
’ \ ’ or 1=1 --’ 输出的SQL语句是把整个参数用引号包起来,并把参数中的引号作为转义字符转为\ ',从而避免了参数也作为条件的一部分,避免了sql 注入。下面是preparedstatement 类中setString() 方法的源码。这段代码的作用是进行字符串的转义当出现换行符、引号、斜杠等特殊字符时,对这些特殊字符进行转义。
public void setString(int parameterIndex, String x) throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
if (x == null) {
setNull(parameterIndex, 1);
} else {
checkClosed();
int stringLength = x.length();
if (this.connection.isNoBackslashEscapesSet()) {
boolean needsHexEscape = isEscapeNeededForString(x, stringLength);
if (!needsHexEscape) {
byte[] parameterAsBytes = null;
StringBuilder quotedString = new StringBuilder(x.length() + 2);
quotedString.append('\'');
quotedString.append(x);
quotedString.append('\'');
if (!this.isLoadDataQuery) {
parameterAsBytes = StringUtils.getBytes(quotedString.toString(), this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), getExceptionInterceptor());
}
else {
parameterAsBytes = StringUtils.getBytes(quotedString.toString());
}
setInternal(parameterIndex, parameterAsBytes);
} else {
byte[] parameterAsBytes = null;
if (!this.isLoadDataQuery) {
parameterAsBytes = StringUtils.getBytes(x, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), getExceptionInterceptor());
}
else {
parameterAsBytes = StringUtils.getBytes(x);
}
setBytes(parameterIndex, parameterAsBytes);
}
return;
}
String parameterAsString = x;
boolean needsQuoted = true;
if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
needsQuoted = false;
StringBuilder buf = new StringBuilder((int)(x.length() * 1.1D));
buf.append('\'');
for (int i = 0; i < stringLength; i++) {
char c = x.charAt(i);
switch (c) {
case '\000':
buf.append('\\');
buf.append('0');
break;
case '\n':
buf.append('\\');
buf.append('n');
break;
case '\r':
buf.append('\\');
buf.append('r');
break;
case '\\':
buf.append('\\');
buf.append('\\');
break;
case '\'':
buf.append('\\');
buf.append('\'');
break;
case '"':
if (this.usingAnsiMode) {
buf.append('\\');
}
buf.append('"');
break;
case '\032':
buf.append('\\');
buf.append('Z');
break;
case '?':
case '?':
if (this.charsetEncoder != null) {
CharBuffer cbuf = CharBuffer.allocate(1);
ByteBuffer bbuf = ByteBuffer.allocate(1);
cbuf.put(c);
cbuf.position(0);
this.charsetEncoder.encode(cbuf, bbuf, true);
if (bbuf.get(0) == 92) {
buf.append('\\');
}
}
buf.append(c);
break;
default:
buf.append(c);
break;
}
}
buf.append('\'');
parameterAsString = buf.toString();
}
byte[] parameterAsBytes = null;
if (!this.isLoadDataQuery) {
if (needsQuoted) {
parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), getExceptionInterceptor());
} else {
parameterAsBytes = StringUtils.getBytes(parameterAsString, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), getExceptionInterceptor());
}
} else {
parameterAsBytes = StringUtils.getBytes(parameterAsString);
}
setInternal(parameterIndex, parameterAsBytes);
this.parameterTypes[parameterIndex - 1 + getParameterIndexOffset()] = 12;
}
}
}
(上面所用jdbc版本为5.0,不同版本的转义结果可能有所不一样,比如在这个版本的jdbc中把引号转为了斜杆,而在8.0的版本中把引号转为了双引号)
总以上结果我们可以得出preparedstatement 确实防止了sql注入。不像statement,PreparedStatement不是将参数简单拼凑成sql,而是做了一些预处理,将参数转换为string,两端加单引号,将参数内的一些特殊字符(换行,单双引号,斜杠等)做转义处理,这样就很大限度的避免了sql注入。preparedment在语句中使用了占位符,规定了sql语句的结构。用户可以设置"?"的值,但是不能改变sql语句的结构,因此想在sql语句后面加上如“ 'or 1=1”实现sql注入是行不通的。实际开发中,一般采用PreparedStatement访问数据库,它不仅能防止sql注入,还是预编译的。