PreparedStatment防止SQL注入原理

什么是SQL注入

SQL注入是将Web页面的原URL、表单域或数据包输入的参数,修改拼接成SQL语句,传递给Web服务器,最终达到欺骗服务器执行恶意的SQL命令进而传给数据库服务器以执行数据库命令。如Web应用程序的开发人员对用户所输入的数据或cookie等内容不进行过滤或验证(即存在注入点)就直接传输给数据库,就可能导致拼接的SQL被执行,获取对数据库的信息以及提权,发生SQL注入攻击。

SQL注入原理

  • SQL注入使攻击者绕过认证环节,直接控制远程服务器上的数据库。SQL是一种结构化查询语句,SQL语法允许数据库命令与用户数据混杂在一起,那么用户数据极有可能被解释成命令。目前,大多数web应用都在使用SQL数据库来存放应用程序数据。这样攻击者不仅能在web应用上输入数据,还可以在数据库上执行任意命令。

  • SQL攻击主要分为两种形式:

    • 直接注入式攻击:直接将代码插入到与SQL命令串联在一起并使得其以执行的用户输入变量;
    • 间接注入式攻击:将恶意代码注入要在表中存储或者作为原数据存储的字符串。在存储的字符串中会连接到一个动态的SQL命令中,以执行一些恶意的SQL代码。
  • 注入过程的攻击方式是提前终止文本字符串,之后再新追加一个命令。由于插入的命令可能在执行前追加其他字符串,所以,攻击者会用‘- -’注释标记来终止注入的字符串,执行时该段文本将不会被编译和执行。

演示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拼接的方式进行用户验证。

  1. 首先来一个简单的SQL注入,在用户名栏输入
    ’ or 1=1-- ,密码栏随便输或者不填,并在控制台输出执行的sql语句和数据库第一个用户的结果System.out.println(sql);
    System.out.println(rs.getString(“username”)+" "+rs.getInt(“password”));
    在这里插入图片描述

    其中1=1 是true ,and password=’ ’ 等语句被‘- -’注释了,不会被编译执行,完全跳过了sql 验证,直接输出用户名和密码,此时攻击者就可使用数据库中的第一个用户登录。

  2. 堆叠注入。在sql中用分号(;)表示一个语句的结束。在sql中可以执行完一个语句后以‘;’结束后又可继续构造一条语句继续执行。
    例如:当攻击者在登录页面输入如下语句:

 username:' ; drop table 表名 - -
 password:(任意输入)

这时会提示登录失败,但此时数据库中的表也被删除,导致任何用户均不可登录。(堆叠注入只能在 MySQL SQL Server 中使用,Oracle不能使用堆叠注入,当有两条语句执行时将会报‘无效字符’错误,在此就不做演示)。

从以上演示我们知道,某些攻击者会利用这些漏洞进行一些非法操作,导致数据库数据遭到破坏,用户信息泄露等,所以防止sql注入是非常重要的。但怎样才能防止sql注入呢?

PreparedStatement防止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注入,还是预编译的。

你可能感兴趣的:(web)