1. 程式人生 > >Android解決資料庫注入漏洞風險

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