在app功能开发完成,提交应用市场时,竟然报高风险,有数据库注入漏洞!
什么是数据库注入漏洞,又是怎么检测出来的,要怎样防止呢?

SQL注入漏洞检测方式说明:

主要就是检测,是否在query()中使用拼接字符串组成SQL语句的形式去查询数据库,此时容易发生SQL注入攻击。


举一个例子:

有一个输入用户名的EditText,我们在查询数据库的时候使用到了它,是这么使用的:


String sql = "SELECT * FROM addressbook where name='" + et_name.getText().toString()+"'" ;


Cursor cursor = sqldb.rawQuery(sql, null);



通常的情况,在编辑框里面输入的是姓名,例如:张三,在查询语句就是:


SELECT * FROM addressbook where name='张三'


进行查询,获取到张三的相关数据。


但是,有的人使用了这样的输入:张三' or '1=1


这样,查询语句就变成了这样子:


SELECT * FROM addressbook where name='张三' or '1=1'


由于where中是两个条件相或,而'1=1'总是为真,所以,这个查询语句,会返回所有数据库中的记录!


这,就是数据泄漏!攻击者使用输入的内容,导致我们数据库中信息泄漏,这就是数据库注入攻击。在代码中存在这种风险,就叫数据库注入漏洞。



怎么防止呢?


这种问题早已有之,所以早就有了成熟的防范方法:


参数化查询。


还是用上面的例子来说明:


将代码修改下,特别注意,查询的参数独立出来了:


String sql = "SELECT * FROM addressbook where name=?" ;


sqldb.rawQuery(sql, new String[]{et_name.getText().toString()});


对比rawQuery的调用方法,我们可以发现,差别在于使用到了第二个参数。


在第一个查询语句中,使用占位符“?”代表了要输入的参数,在rawQuery()的第二个参数中,使用字符串数组的形式,送入了实际我们期望送入的参数(用户名)。


为什么使用字符串数组呢?我们前面的查询条件中,可能存在多个查询字段,则会有多个占位符“?”,每一个占位符对应字符串数组中的一个字符串。


这种方式是怎么解决数据库注入问题的?


在这个例子中,它是将 


张三‘ or '1=1


 作为一个用户名的整体,在数据库中进行查询,这样就解决了使用输入来修改查询条件的问题。



当然,实际的情况会复杂一些,例如查询语句,并不只是rawQuery(),还有query(),参数更多。


Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) 


这里,我们关注的是其中两个参数:


String selection, 是查询语句中的where子句


String[] selectionArgs,是where子句对应的条件值


在这种情况下,是否会有数据库注入问题呢?


也会有,举两个例子就明白了:



下面这种是有注入问题的,查询语句是拼接的:



    String selection = "name='" + et_name.getText().toString()+"'";


    Cursor cursor = sqldb.query(


            "addressbook",


            null,


            selection,


            null,


            null,null,null,null);




下面这种是安全的,将参数独立出来了:


    String selection = "name=?";


    String[] selectionArgs = new String[]{et_name.getText().toString()};


    Cursor cursor = sqldb.query(


            "addressbook",


            null,


            selection,


            selectionArgs,


            null,null,null,null);




还有一种情况,就是如果我们使用了第三方的数据库封装,如果遇到注入问题,会不会就没法解决了呢?


我的理解,做封装的人,对数据库的理解远不是我这种小白能比拟的,所以在一般情况下,他们是会考虑到这个问题的。


以我遇到的情况为例:


我使用了郭霖大神的litePal库来操作数据库,下面是介绍:


LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发时最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表、増删改查的操作。


简单理解,就是说,我们不用写sql语句了,直接使用对象的几个方法就搞定全部数据库操作。



经过我的实验,是否存在注入问题,怎么解决,也是非常类似的:


下面这种是有注入问题的,查询语句是拼接的:


String conditions="orderno = '"+et_name.getText().toString()+"'";


List<OrderDetails> listOrderDetailsRead = (List<OrderDetails>)DataSupport.


where(conditions).


find(OrderDetails.class);





下面这种是安全的,将参数独立出来了:


List<OrderDetails> listOrderDetailsRead = (List<OrderDetails>)DataSupport.


where("orderno = ? ",et_name.getText().toString()).


find(OrderDetails.class);




所以说,出了问题,不要先怀疑人家的库是否封装太深,导致没法做灵活的修改,而是应该想想自己是否使用不当。




了解了原理,下面就是一个具体的程序,我们可以进行一下实测了。



关键代码如下:



public class MainActivity extends AppCompatActivity { 


    SQLiteDatabase sqldb; 

    public String DB_NAME = "sql.db"; 

    public String DB_TABLE = "num"; 

    public int DB_VERSION = 1; 

    public EditText et_name; 

    public EditText et_phone; 

    OrderRecord orderRecord; 


    final DbHelper helper = new DbHelper(this, DB_NAME, null, DB_VERSION); 

    // DbHelper类在DbHelper.java文件里面创建的 


    /** Called when the activity is first created. */ 

    @Override 

    public void onCreate(Bundle savedInstanceState) { 

        super.onCreate(savedInstanceState); 

        setContentView(R.layout.activity_main); 

        sqldb = helper.getWritableDatabase(); 

        // 通过helper的getWritableDatabase()得到SQLiteOpenHelper所创建的数据库 

        Button insert = (Button) findViewById(R.id.insert); 

        Button delete = (Button) findViewById(R.id.delete); 

        Button update = (Button) findViewById(R.id.update); 

        Button query = (Button) findViewById(R.id.query); 


        et_name = (EditText) findViewById(R.id.name); 

        et_phone = (EditText) findViewById(R.id.phone); 


//        et_name.setText("tt4"); 

        et_name.setText("zzz' or '1=1");//特殊字符在手机上输入不方便,干脆用做初始值好了 

        orderRecord =new OrderRecord(); 


        final ContentValues cv = new ContentValues(); 

        // ContentValues是“添加”和“更新”两个操作的数据载体 

        updatelistview();// 更新listview 

        // 添加insert 

        insert.setOnClickListener(new View.OnClickListener() { 


            @Override 

            public void onClick(View v) { 

                // TODO Auto-generated method stub 


                cv.put("name", et_name.getText().toString()); 

                cv.put("phone", et_phone.getText().toString()); 

                // name和phone为列名 

                long res = sqldb.insert("addressbook", null, cv);// 插入数据 


                orderRecord.setOrderNo(et_name.getText().toString()); 

                orderRecord.setPhoneNo(et_phone.getText().toString()); 

                boolean bRet = SqlitePalTools.saveRecord(orderRecord); 


                if(!bRet){ 

//                if (res == -1) { 

                    Toast.makeText(MainActivity.this, "添加失败", 

                            Toast.LENGTH_SHORT).show(); 

                } else { 

                    Toast.makeText(MainActivity.this, "添加成功", 

                            Toast.LENGTH_SHORT).show(); 

                } 

                updatelistview();// 更新listview 

            } 

        }); 

        // 删除 

        delete.setOnClickListener(new View.OnClickListener() { 

            @Override 

            public void onClick(View v) { 

                // TODO Auto-generated method stub 

                int res = sqldb.delete("addressbook", "name='大钟'", null); 

                // 删除列名name,行名为“大钟”的,这一行的所有数据,null表示这一行的所有数据 

                // 若第二个参数为null,则删除表中所有列对应的所有行的数据,也就是把table清空了。 

                // name='大钟',大钟要单引号的 

                // 返回值为删除的行数 

                if (res == 0) { 

                    Toast.makeText(MainActivity.this, "删除失败", 

                            Toast.LENGTH_SHORT).show(); 

                } else { 

                    Toast.makeText(MainActivity.this, "成删除了" + res + "行的数据", 

                            Toast.LENGTH_SHORT).show(); 

                } 

                updatelistview();// 更新listview 

            } 

        }); 

        // 更改 

        update.setOnClickListener(new View.OnClickListener() { 

            @Override 

            public void onClick(View v) { 

                // TODO Auto-generated method stub 

                cv.put("name", "大钟"); 

                cv.put("phone", "1361234567"); 

                int res = sqldb.update("addressbook", cv, "name='张三'", null); 

                // 把name=张三所在行的数据,全部更新为ContentValues所对应的数据 

                // 返回时为成功更新的行数 

                Toast.makeText(MainActivity.this, "成功更新了" + res + "行的数据", 

                        Toast.LENGTH_SHORT).show(); 

                updatelistview();// 更新listview 

            } 

        }); 

        // 查询 

        query.setOnClickListener(new View.OnClickListener() { 

            @Override 

            public void onClick(View v) { 

                // TODO Auto-generated method stub 

                try { 

                    int numRecords=0; 

                    LogUtil.logWithMethod(new Exception(), "enter"); 


//1,使用rawQuery 

                    //不安全的查询方式,会被sql注入攻击 

                    String sql = "SELECT * FROM addressbook where name='" + et_name.getText().toString()+"'" ; 

                    LogUtil.logWithMethod(new Exception(),"sql="+sql); 

                    /***这里得到的是一个游标*/ 

                    Cursor cursor = sqldb.rawQuery(sql, null); 

                    if (cursor == null) { 

                        return; 

                    } 

                    numRecords = cursor.getCount(); 

                    LogUtil.logWithMethod(new Exception(), "sqldb.rawQuery with joint string: numRecords="+numRecords); 


                    //安全的查询方式,将参数放入参数列表中了,就不会编译,只是作为参数来比较了(解决sql注入问题) 

                    sql = "SELECT * FROM addressbook where name=?" ; 

                    LogUtil.logWithMethod(new Exception(),"sql="+sql); 

                    cursor = sqldb.rawQuery(sql, new String[]{et_name.getText().toString()}); 

                    if (cursor == null) { 

                        return; 

                    } 

                    numRecords = cursor.getCount(); 

                    LogUtil.logWithMethod(new Exception(), "sqldb.rawQuery with param: numRecords="+numRecords); 


//2,使用query 

                    //不安全的查询方式,会被sql注入攻击 

                    String selection = "name='" + et_name.getText().toString()+"'"; 

                    cursor = sqldb.query( 

                            "addressbook",null, 

                            selection, 

                            null, 

                            null,null,null,null); 

                    if (cursor == null) { 

                        return; 

                    } 

                    numRecords = cursor.getCount(); 

                    LogUtil.logWithMethod(new Exception(), "sqldb.query with joint string: numRecords="+numRecords); 


                    //安全的查询方式,将参数放入参数列表中了,就不会编译,只是作为参数来比较了(解决sql注入问题) 

                    selection = "name=?"; 

                    String[] selectionArgs = new String[]{et_name.getText().toString()}; 

                    cursor = sqldb.query( 

                            "addressbook",null, 

                            selection, 

                            selectionArgs, 

                            null,null,null,null); 

                    if (cursor == null) { 

                        return; 

                    } 

                    numRecords = cursor.getCount(); 

                    LogUtil.logWithMethod(new Exception(), "sqldb.query with param: numRecords="+numRecords); 


                    /***记得操作完将游标关闭*/ 

                    cursor.close(); 



//3,使用LitePal 

                    //不安全的查询方式,会被sql注入攻击 

                    String conditions="orderno = '"+et_name.getText().toString()+"'"; 

                    numRecords = SqlitePalTools.queryRecordsNumWithString(conditions); 

                    LogUtil.logWithMethod(new Exception(), "LitePal select with cond: numRecords="+numRecords); 


                    //安全的查询方式,参数化查询 

                    orderRecord.setOrderNo(et_name.getText().toString()); 

                    orderRecord.setPhoneNo(et_phone.getText().toString()); 

                    numRecords = SqlitePalTools.queryRecordsNumWithParam(orderRecord);//orderno = ? 

                    LogUtil.logWithMethod(new Exception(), "LitePal parameterized query: numRecords="+numRecords); 


                    Toast.makeText(MainActivity.this, 

                            "一共有" + numRecords + "条记录", Toast.LENGTH_SHORT) 

                            .show(); 


                    updatelistview();// 更新listview 

                } catch (Exception e){ 

                    e.printStackTrace(); 

                    LogUtil.logWithMethod(new Exception(), "error:"+e.getMessage()); 

                } 

            } 

        }); 

    } 


    // 更新listview 

    public void updatelistview() { 

        ListView lv = (ListView) findViewById(R.id.lv); 

        final Cursor cr = sqldb.query("addressbook", null, null, null, null, 

                null, null); 

        String[] ColumnNames = cr.getColumnNames(); 

        // ColumnNames为数据库的表的列名,getColumnNames()为得到指定table的所有列名 

        ListAdapter adapter = new SimpleCursorAdapter(this, R.layout.layout, 

                cr, ColumnNames, new int[] { R.id.tv1, R.id.tv2, R.id.tv3 }); 

        // layout为listView的布局文件,包括三个TextView,用来显示三个列名所对应的值 

        // ColumnNames为数据库的表的列名 

        // 最后一个参数是int[]类型的,为view类型的id,用来显示ColumnNames列名所对应的值。view的类型为TextView 

        lv.setAdapter(adapter); 

    } 

}




布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.develop.app.sqlitedemo.MainActivity">

    <LinearLayout android:layout_width="fill_parent"
                  android:layout_height="fill_parent"
                  android:orientation="vertical" >

    <LinearLayout android:layout_width="fill_parent"
                  android:layout_height="wrap_content"
                     >

        <TextView
            android:id="@+id/name_desc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="name:"
            />

        <EditText
            android:id="@+id/name"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <LinearLayout android:layout_width="fill_parent"
                  android:layout_height="wrap_content"
        >

        <TextView
            android:id="@+id/phone_desc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="phone:"
            />
        <EditText
            android:id="@+id/phone"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
</LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout1"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" >

            <Button
                android:id="@+id/insert"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="增加" />

            <Button
                android:id="@+id/delete"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="删除" />

            <Button
                android:id="@+id/update"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="更改" />

            <Button
                android:id="@+id/query"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="查询" />
        </LinearLayout>

        <ListView
            android:id="@+id/lv"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" >
        </ListView>

    </LinearLayout>

</RelativeLayout>

完整的工程代码,见文章末尾的链接地址。



运行起来后,先录入几条记录,如下图:



Android Debug Databa android debug database漏洞_数据库




然后看几种查询的结果:



Android Debug Databa android debug database漏洞_LitePal_02


可以看出,运行的结果,与我们前面的分析是匹配的。

在输入:zzz' or '1=1 ,进行查询时,

对于有注入漏洞的,能查询到所有的记录(3条)。

对于没有注入漏洞的,就是一条记录也查询不到了。这样,就达到解决数据信息在查询时的泄漏问题了。



demo地址:






参考:

SQL注入漏洞检测方式说明:







为什么参数化SQL查询可以防止SQL注入?



https://www.zhihu.com/question/52869762/answer/132614240