为什么通过正则表达式过滤防止 sql 注入不是个好主意?
最近在做一个系统,这个系统可以看做一个巨大的数据库,用户可以通过 REST 请求来进行增删改查的操作。除了查询的其他操作都是通过构造 http request body 来定义查询,后台接收到数据后做一下拼接(在库名、表名、字段名等地方简单粗暴的过滤掉所有的特殊符号)。而关于查询的部分就比较有趣了:
怎么能通过 json 完整的描述 sql 的 where 并且能够防止 sql 注入还要便于使用(不能像 ES 那样!)
答案是不能!sql 的 where 条件很灵活而且可以非常复杂,就算我们能有一个能够解析复杂 json 的方法,拼接 json 的人肯定也想杀了你。
那么能不能直接写 where 条件啊?问题是怎么保证安全!
通过条件过滤
用户传过来一条 where 语句,我们通过判断 where 语句中有没有危险的操作,类似于这样(代码来自sql-injection-safe):
public class SqlSafeUtil {
private static final String SQL_TYPES =
"TABLE, TABLESPACE, PROCEDURE, FUNCTION, TRIGGER, KEY, VIEW, MATERIALIZED VIEW, LIBRARY" +
"DATABASE LINK, DBLINK, INDEX, CONSTRAINT, TRIGGER, USER, SCHEMA, DATABASE, PLUGGABLE DATABASE, BUCKET, " +
"CLUSTER, COMMENT, SYNONYM, TYPE, JAVA, SESSION, ROLE, PACKAGE, PACKAGE BODY, OPERATOR" +
"SEQUENCE, RESTORE POINT, PFILE, CLASS, CURSOR, OBJECT, RULE, USER, DATASET, DATASTORE, " +
"COLUMN, FIELD, OPERATOR";
private static final String[] SQL_REGEXPS = {
"(?i)(.*)(\\b)+(OR|AND)(\\s)+(true|false)(\\s)*(.*)",
"(?i)(.*)(\\b)+(OR|AND)(\\s)+(\\w)(\\s)*(\\=)(\\s)*(\\w)(\\s)*(.*)",
"(?i)(.*)(\\b)+(OR|AND)(\\s)+(equals|not equals)(\\s)+(true|false)(\\s)*(.*)",
"(?i)(.*)(\\b)+(OR|AND)(\\s)+([0-9A-Za-z_'][0-9A-Za-z\\d_']*)(\\s)*(\\=)(\\s)*([0-9A-Za-z_'][0-9A-Za-z\\d_']*)(\\s)*(.*)",
"(?i)(.*)(\\b)+(OR|AND)(\\s)+([0-9A-Za-z_'][0-9A-Za-z\\d_']*)(\\s)*(\\!\\=)(\\s)*([0-9A-Za-z_'][0-9A-Za-z\\d_']*)(\\s)*(.*)",
"(?i)(.*)(\\b)+(OR|AND)(\\s)+([0-9A-Za-z_'][0-9A-Za-z\\d_']*)(\\s)*(\\<\\>)(\\s)*([0-9A-Za-z_'][0-9A-Za-z\\d_']*)(\\s)*(.*)",
"(?i)(.*)(\\b)+SELECT(\\b)+\\s.*(\\b)(.*)",
// "(?i)(.*)(\\b)+SELECT(\\b)+\\s.*(\\b)+FROM(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+INSERT(\\b)+\\s.*(\\b)+INTO(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+UPDATE(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+DELETE(\\b)+\\s.*(\\b)+FROM(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+UPSERT(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+SAVEPOINT(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+CALL(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+ROLLBACK(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+KILL(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+DROP(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+CREATE(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+ALTER(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+TRUNCATE(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+LOCK(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+UNLOCK(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+RELEASE(\\b)+(\\s)*(" + SQL_TYPES.replaceAll(",", "|") + ")(\\b)+\\s.*(.*)",
"(?i)(.*)(\\b)+DESC(\\b)+(\\w)*\\s.*(.*)",
"(?i)(.*)(\\b)+DESCRIBE(\\b)+(\\w)*\\s.*(.*)",
"(.*)(/\\*|\\*/|;){1,}(.*)",
"(.*)(-){2,}(.*)",
};
... ...
}
我不是数据库专家,SQL_TYPES
里面有挺多我听都没听过的骚操作。那么问题来了,你认为这个 SqlSafeUtil
能够保证我们系统的安全性吗?这是一个根本无法证明的问题,你怎么知道你 cover 到了所有的情况?你怎么证明你 cover 到了所有情况?你所能证明的不过是你能 cover 到已知的情况而已!
我非常认同 stackoverflow 上的这个回答:
It simply isn’t possible to build a filter that’s smarter than the people who hack SQL for a living.
为什么通过 parser 之后的 sql 是安全的
最简单的情况
假如我们有一个 where 语句:
where name = "peter"
sealed trait Clause
case class FieldValue[T](field: String, operator: Operator.Value, value: T) extends Clause
... ...
case class And(left: Clause, right: Clause) extends Clause
case class Or(left: Clause, right: Clause) extends Clause
... ...
所以上面那条 sql 用 case class 来描述的话应该是
FieldValue("name", EQ, "peter")
更复杂一点
where name = "peter" and (active = true or age >= 30)
用 case class 来描述的话应该是:
And(FieldValue("name", EQ, "peter"), Or(FieldValue("active", EQ, value = true), FieldValue("age", GE, 30)))
写成一行不太直观,画个图吧:
这个其实就是表达式树了。然后我们再把表达式树再翻译成 sql就好了。
所以整个过程是这样的:
sql => case class 组成的表达式树 => 再翻译成 sql
更进一步
我们给所有的表名、字段名都加上“``”,然后表达式树生成参数化的 sql,最终结果像这样:
WHERE `name` != ?
然后把值“peter”当做参数,这样就更加安全了!
证明安全性
证明很简单:
在 “sql => case class” 的过程中我们会对库名、表名、字段名对一下简单的过滤(比如不能有特殊符号),然后把 sql 解析为表达式树,如果这个过程能正确处理,那么 sql 就是合法的。因为语法都是我们自己定义的,所以不合法的操作根本无法通过我们的 parser,所以通过 parser 来检测 sql 注入是安全的
举个极端的例子:
WHERE UserId = 105; DROP TABLE Suppliers;
上面的语法根本通不过我们 parser 的检查,因为我们压根儿就没定义 DROP TABLE
的操作!
自己写 parser 的缺点
自己定义的语法可能不够用,需要加功能。其实加起来还算容易,不过为了避免重复造轮子我们可以基于 spark-sql
的库来搞,而且 spark-sql
已经有了完善的 sql 语法了。
还有吗?
如果只是查数据库,又对灵活性要求非常高的话,可以这样:
- read-only sql connection
dataSource.setReadOnly(true);
- read-only user
通过只有只读权限的用户连接数据库
Original link:https://izhangzhihao.github.io//2018/01/29/为什么通过正则表达式过滤防止-sql-注入不是个好主意/