主要就是检测,是否在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 在app功能开发完成,提交应用市场时,竟然报高风险,有数据库注入漏洞!什么是数据库注入漏洞,又是怎么检测出来的,要怎样防止呢?SQL注入漏洞检测方式说明:主要就是检测,是否在query()中使用拼接字符串组成SQL语句的形式去查询数据库,此时容易发生SQL注入攻击。举一个例子:有一个输入用户名的EditText,我们在查询数据库的时候使用到了它,是这么使用的:String sql = &quot;SELEC... adb remount adb push libs/armeabi-v7a/libqever.so /data/local/tmp adb push libs/armeabi-v7a/inject /data/local/tmp adb shell cd /data/local/tmp ./inject ..
所谓SQL 注入 ,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的 查询 字符串,最终达到欺骗服务器执行恶意的SQL命令。具体来说,它是利用现有应用程序,将(恶意的)SQL命令 注入 到后台 数据库 引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全 漏洞 的网站上的 数据库 ,而不是按照设计者意图去执行SQL语句。 SQLite 数据库 的SQL 注入 不只是常见的mysql,oracle 数据库 SQLite 数据库 如果出现sql语句书写不规范,也可能出现SQL 注入 。以下是一个登录功能的代.
Android 版本在2.3.7和4.0.1之间有个 sqlite 漏洞 ,这个 漏洞 会导致任何第三方应用可以访问/data/data//databases/文件,这样就能读取你的私密数据,特别里面如果有密码什么的,读取方法如下: public class Sqlite JournalLeakActivity extends Activity { @Override protec
一、写在前文      已经很久没有写过博客了,第一次写博客是在2015年的时候,那时候第一次接触CSDN,感觉很新鲜,所以当时就写了好几篇博 客,那会刚刚好在学C++,所以写的都是关于C++的,而现在已经学习 Android 一年多了,心血来潮打开以前的文章,没想到竟然还 有300多阅读量。而最近项目刚刚赶好,所以决定从新开始记录博客。 二、遇到的问题        项目需求如下:
SQL 注入 是一个老话题,没想到今天有同事在 android framework踩到了这个坑,mark下。 业务需求要修改 查询 mmssms.db的一张表canonical_addresses,看到代码有点懵圈,类似如下: ContentResolver cr = this.getContentResolver(); Curso
数据库 增加字段报错可能有多种原因,以下是几个常见的 解决 方法: 1. 检查SQL语句是否正确:在增加字段时,SQL语句必须要正确,否则会报错。可以检查SQL语句的语法、字段名、表名等是否正确。 2. 检查表是否存在:在增加字段之前,需要先检查表是否存在,如果表不存在,那么增加字段的操作也会失败。 3. 检查权限是否足够:增加字段的操作需要有足够的权限,如果权限不足,那么会报错。需要检查当前用户是否有足够的权限进行该操作。 4. 检查 数据库 版本是否匹配:如果 数据库 版本不匹配,那么增加字段的操作也会失败。需要检查 数据库 版本是否与代码中使用的版本一致。 5. 使用 数据库 升级工具:如果以上方法都无法 解决 问题,可以考虑使用 数据库 升级工具,如GreenDao等,来进行 数据库 升级操作。