Android解决数据库注入漏洞风险


在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>

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

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




然后看几种查询的结果:


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

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

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

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


demo地址:
https://download.csdn.net/download/lintax/10329941


参考:

SQL注入漏洞检测方式说明:
http://blog.csdn.net/u013107656/article/details/53337422

为什么参数化SQL查询可以防止SQL注入?
https://www.zhihu.com/question/52869762/answer/132614240

代码框架借用:
http://blog.csdn.net/conowen/article/details/7306545

郭霖的LitePal使用讲解:
http://blog.csdn.net/column/details/android-database-pro.html
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值