参数绑定是数据库编程中常用的一种方法,通过这种方法,数据库系统可以减少编译次数,快速执行,提升效率;但这种编程方法将对数据库的审计带来挑战,在笔者所见到的若干数据库审计产品中,在这种情况下都出了不少的错误。有的是漏审了语句,有的是记录下了操作的语句,但将具体执行时所使用的参数记错了或漏记了。这些缺陷对于审计产品无疑很是致命。
为了详解这种情况,我们来看一下参数绑定的基本概念。我们在常规的图形化或命令行工具中,往往都是直接写上SQL语句,比如:
Select * from person_info where id=’12XXXXX6722’;
在这里查询条件是***号码。根据***号码查询个人信息,是一种常用功能,也是会重复使用的语句,为了提升效率,编程中可以这么写:
String sql1=’Select * from person_info where id=?;’
PreparedStatement pStmt = testConn.getConnection().prepareStatement(sql);
pStmt.setInt(1, ’12XXXXX6722’);
pStmt.execute();
下一次再使用时,就不用再发送语句了,可以直接发送:
pStmt.setInt(1, ’22XXXXX5399’);
pStmt.execute();
对于数据库审计系统而言,单纯地记录下来‘Select * from person_info where id=?’是存在缺陷的,因为你无法明确额操作人员到底访问了哪个用户的信息,必须明确下来具体的参数才行。
这就要求将设定的参数,与Prepare的语句有效的关联,形成可视化的审计记录展现:
Select * from person_info where id=’12XXXXX6722’;
Select * from person_info where id=’22XXXXX5399’;
这实际上要求审计系统比起单纯的记录语句要完成更多的工作;其中一个重要任务的就是句柄追踪,本质上SQL语句的执行过程追踪就是句柄追踪过程。在上面显示的例子中
pStmt.execute(),在通讯过程中并不发送具体的语句,而仅是告知服务器要执行哪个语句句柄,服务器端会根据内部记录的句柄所对应的已经编译完成的SQL语句的执行计划,进行语句执行。数据库审计要完成相应的工作,需要执行类似的过程,在系统的内部也维护这样的映射关系;同时由于大多数数据库的句柄,是在会话级的,句柄是可重用的,因此在数据库审计中还要有效地维护句柄与session的关联,以及句柄的消亡。
在句柄维护之外,另一个有挑战的工作就是参数的还原。参数的还原,首要的是要明确参数所对应的句柄;在调用pStmt.setInt(1, ’22XXXXX5399’)时,在网络中发送的包,会标明这个参数是针对哪个句柄的,是针对第几个参数的。作为数据库审计产品,需要将参数与语句进行映射;更重要地要准确地填回参数所在的位置,上面这个例子由于只有一个参数,没有什么挑战性。但参数的绑定情况远比这个复杂,下面我们将提供一些例子来考验相关的数据库审计产品。
用例1:一个基本的示例
String sql = "select PID,NAME from PERFORMANCE_C t where pid=:1 and balance>:2 and persionid>:3 and datefield>TO_DATE(:4,'YYYY-MM-DD')";
PreparedStatement pStmt_2 = testConn.getConnection().prepareStatement(sql);
pStmt_2.setInt(1, 84);
pStmt_2.setInt(2, 5555);
pStmt_2.setString(3, "120");
pStmt_2.setString(4, "1900-1-1");
pStmt_2.execute();
在这个示例中关键是看审计产品是否能将语句审计为:
select PID,NAME from PERFORMANCE_C t where pid=84 and balance>5555 and persionid>'120' and datefield>TO_DATE('1900-1-1','YYYY-MM-DD')
用例2:这是一个略有挑战的例子,但这个挑战已经有数据库审计产品无法将参数还原了
String sql = "select PID,NAME from PERFORMANCE_C t where pid=:pid and balance>:balance and personid>:personid and datefield>TO_DATE(:datefield,'YYYY-MM-DD')";
PreparedStatement pStmt_2 = testConn.getConnection().prepareStatement(sql);
pStmt_2.setInt(1, 84);
pStmt_2.setInt(2, 5555);
pStmt_2.setString(3, "120");
pStmt_2.setString(4, "1900-1-1");
pStmt_2.execute();
这里的挑战在于参数并未使用常规的‘?’号或者‘:1’,这样的方式,而是采用了字符串的方式;验证的答案依然是:
select PID,NAME from PERFORMANCE_C t where pid=84 and balance>5555 and personid>'120' and datefield>TO_DATE('1900-1-1','YYYY-MM-DD')
用例3:这个挑战同样有数据库审计产品出错了
String sql = "select PID,NAME from PERFORMANCE_C t where personid>:1 and pid=:2 and balance>:3 and personid< 32 + :1;
PreparedStatement pStmt_2 = testConn.getConnection().prepareStatement(sql);
pStmt_2.setInt(1, "120");
pStmt_2.setInt(2, 84);
pStmt_2.setInt(3, 5555);
pStmt_2.setInt(4,84);
pStmt_2.execute();
这里的挑战在于参数1和参数4用的是同参数编号;验证的答案是:
select PID,NAME from PERFORMANCE_C t where personid>120 and pid=84 and balance>5555 and personid < 32 + 120;
用例4:若单纯地采用’:’进行分割的参数还原肯定会出错
String sql = "select PID,NAME from PERFORMANCE_C t where pid>:1 and personid =:2 and content like ‘Name:3%’ and balance>:3";
PreparedStatement pStmt_2 = testConn.getConnection().prepareStatement(sql);
pStmt_2.setInt(1, 84);
pStmt_2.setString (2, "120");
pStmt_2.setString (3, 5555);
pStmt_2.execute();
验证的答案是:
select PID,NAME from PERFORMANCE_C t where personid>120 and pid=84 and content like ‘’Name:3%’ and balance>5555;
用例5:这个挑战是验证对于多prepared语句下是否能准确地将执行的语句和对应的参数都审计下来,这里我们将提供两条语句进行交互的执行:
public void TestCase_PrepareSelect_3()
{
String sql_1 = "select pid,balance,account from F10_USER t where pid=:pid";
//String sql_2 = "select NAME,PID,balance from PERFORMANCE_C t where pid=:1 and
balance>:2 and persionid>:3 and datefield>TO_DATE(:4,'YYYY-MM-DD')";
String sql_2 = "select NAME,PID,balance from F10_USER t where pid=:1 andbalance>:3 and persionid>:2 and datefield>TO_DATE(:4,'YYYY-MM-DD')";
try {
pStmt = testConn.getConnection().prepareStatement(sql_1);
pStmt_2 = testConn.getConnection().prepareStatement(sql_2);
//第一轮执行
pStmt_2.setInt(1, 94);
pStmt_2.setInt(2, 5555);
pStmt_2.setString(3, "120");
pStmt_2.setString(4, "1900-1-1");
pStmt_2.execute();
testRest_2 = pStmt_2.getResultSet();
while(testRest_2.next())
{
pid = testRest_2.getInt("PID");
System.out.println("PID="+pid);
}
pStmt.setInt(1, 84);
pStmt.execute();
testRest = pStmt.getResultSet();
while(testRest.next())
{
pid = testRest.getInt("PID");
System.out.println("PID="+pid);
}
//第2轮执行
pStmt.setInt(1, 85);
pStmt.execute();
testRest = pStmt.getResultSet();
while(testRest.next())
{
pid = testRest.getInt("PID");
System.out.println("PID="+pid);
}
pStmt_2.setInt(1, 95);
pStmt_2.setInt(2, 1555);
pStmt_2.setString(3, "1305");
pStmt_2.setString(4, "1920-1-1");
pStmt_2.execute();
testRest_2 = pStmt_2.getResultSet();
while(testRest_2.next())
{
pid = testRest_2.getInt("PID");
System.out.println("PID="+pid);
}
//第3轮执行
pStmt_2.setInt(1, 96);
pStmt_2.setInt(2, 5123);
pStmt_2.setString(3, "1201");
pStmt_2.setString(4, "1930-1-1");
pStmt_2.execute();
testRest_2 = pStmt_2.getResultSet();
while(testRest_2.next())
{
pid = testRest_2.getInt("PID");
System.out.println("PID="+pid);
}
pStmt.setInt(1, 86);
pStmt.execute();
testRest = pStmt.getResultSet();
while(testRest.next())
{
pid = testRest.getInt("PID");
System.out.println("PID="+pid);
}
// testRest.close();
// pStmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
应该来讲,这是一个并不复杂的多语句执行示例,但某知名厂商的数据库审计产品,将sql_1被审计到了4次,sql_2被审计到了1次。这仅仅是一种简单的多语句执行情况,我们现实中的应用要更为复杂。
大家可以访问这个链接,该链接将提供更为一个完整的参数绑定示例;我们可以测测所使用的数据库审计产品能否准确审计:http://www.dbsec.cn/a/shujukuanquan/shujukufanghushouduan/shujukushenj/index.html
下面我们不再对这些简单的情况进行讨论,我们将讨论一种更为复杂的参数绑定情况,也就是批量参数绑定。
批量参数绑定是DBMS提供商为了提供更为高效的数据操作速度,而提供的一批参数发送,语句多次执行;该方式通过更少的网络通讯次数、更少的SQL层的语境切换等大幅度提高编程效率。这种执行方式在一般的编程中很少使用,但在类似于电信运营商的计费系统这样的环境,对性能的要求很高,往往需要每秒接近上万条的插入或更新,则是一种较为常见的使用方式;因此对于运行商、证券金融、互联网这样的瞬时交易量非常大,需要采用最为高效的数据处理方式的应用环境需要对这种参数绑定方式兼容。
下面是一个批量插入的示例,仅仅是为了检验审计产品是否支持该功能:
PreparedStatement statement = connection.prepareStatement("INSERT INTO Tbl1 VALUES(?, ?)");
//记录1
statement.setInt(1, 1);
statement.setString(2, "Cujo");
statement.addBatch();
//记录2
statement.setInt(1, 2);
statement.setString(2, "Fred");
statement.addBatch();
//记录3
statement.setInt(1, 3);
statement.setString(2, "Mark");
statement.addBatch();
//批量执行上面3条语句.
int [] counts = statement.executeBatch();
现在我们需要考察所使用的数据库审计产品,能否准确地审计下3条插入语句并且每条语句都能准确地匹配上相应的参数;或者审计为1条插入语句,3组参数。
这只是批量插入中一个轻微的挑战,更为复杂的是当批量更为大情况,Oracle和SQL Server这样的考虑更为精细的产品,会对数据采用压缩的处理方式,比如对重复值的表达,对Null值的表达等;数据库审计产品是否能有效地还原每个批量成员的值,将会是一个挑战。
当前我们所见到的绝大多数数据库审计产品在批量参数绑定的执行语句的情况下,都无法准确审计下批量查询中所执行的语句和对应的参数。