防御sql注入
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了防御sql注入相关的知识,希望对你有一定的参考价值。
1. 领域驱动安全
领域驱动安全是一种代码设计方法。其思想是将一个隐式的概念转化为显示,个人认为即是面向对象的方法,将一个概念抽象成一个类,在该类中通过方法对类的属性进行约束。是否是字符串,包含什么字母等。对原始的字符串进行封装。
好处是只需要对类做单元测试,并且保证在代码中需要用到该隐式概念时都创建这个类来进行处理。最重要的是将输入数据固定位数据。就使用参数化语句进行预处理。参数化语句是将sql语句中的参数用占位符替代,在数据库完成编译工作只签参数的时候,程序再调用相应接口将参数传递给数据库。所以这样就可以保证用户输入的数据就一定是数据不会被误当成sql语句来执行。但是有很多不能用预处理的情况
对于不能使用预处理的情况,如limit,常在分页,搜索等业务逻辑中都会用到。则不能使用预处理查询,需要对输入数据进行编码或者严格的过滤。比如数字就只能是0-9,采用白名单原则进行过滤。
ps:参数化只能参数化数据部分,无法对关键字和标示符来进行参数化。这是预处理的局限,从攻击者来看,应寻找无法使用预处理的点来进行测试。从防来说,就得用编码,字符过滤,正则匹配,白名单黑名单策略来对输入进行过滤。
2. 各个语言的预处理
(1) java预处理:
java连接数据库有两个用法,第一是通过jdbc+sql语句的方式来连接操作数据库。或者通过Hibernat轻量级映射框架来连接操作数据库。
两种方式的理解:
1. 通过jdbc+sql的方式用于小项目,可以灵活的控制业务逻辑。但是会特别冗余,dao层会出现很多冗余的sql语句。使得代码变得不易维护和debug。但是现在都是使用spring框架。独立架构DAO层根据自己的业务来封装持久层的操作。此种做法比较多。
以下是一个完整的预处理查询方法,未经封装.
public ResultSet prepareQuery(String sql,Object []param){
int num=findNum(sql);
try{
getconnection();
pst=connection.prepareStatement(sql);
for(int i=1;i<=num;i++)
pst.setObject(1,param[i-1]);
res=pst.executeQuery();
}catch(Exception e){
System.out.println(e.getMessage());
}
return res;
}
2. 也有通过hibernat来通过对象映射数据库。其实hibernat是对jdbc的近一步封装,它是一种轻量级框架不需要像struts那种继承某个类或者接口之类的工作,在配置上相对简单明了。它用自己的映射配置文件来代替了jdbc+sql的模式。主要有*.properties,*cfg.xml核心配置文件,*hbm.xml映射文件和.java的映射类组成。但直接使用hibernat会造成查询攻击问题。虽然它也有自己的预处理。
package com.liang.hibernate;
import java.util.Date;
//创建映射类
public class User {
private String id;
private String name;
private String password;
private Date createTime;
private Date expireTime;
}
//提供User.hbm.xml文件,完成实体类映射
<span style="font-size:12px;"><?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<!--生成默认为user的数据库表-->
<class name="com.liang.hibernate.User">
<id name="id">
<!-- 算法的核心思想是结合机器的网卡、当地时间一个随机数来生成GUID --
<gnerator class="uuid"></generator>
</id>
<property name="name"></property>
<property name="password"></property>
<property name="createTime" type="date"></property>
<property name="expireTime" type="date"></property>
</class>
</hibernate-mapping></span>
将User.hbm.xml文件加入到hibernate.cfg.xml文件中
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3..dtd
<hibernate-configuration>
<session-factory>
<!-- 设置数据库驱动 -->
<propertyname="hibernate.connection.driver_class">com.mysql.jdbc.Driver<property>
<!-- 设置数据库URL -->
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/hibernate_first</property>
<!-- 数据库用户名 -->
<property name="hibernate.connection.username">root</property>
<!-- 数据库密码 -->
<property name="hibernate.connection.password">123456</property>
<!-- 指定对应数据库的方言,hibernate为了更好适配各种关系数据库,针对每种数据库都指定了一个方言dialect -->
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<!-- 映射文件 -->
<mapping resource="com/liang/hibernate/User.hbm.xml"/>
</session-factory>
</hibernate-configuration>
编写工具类ExportDB.java,将hbm生成ddl,也就是hbm2ddl
package com.liang.hibernate;
import org.hibernate.cfg.Configuration;
import org.hibernate.tool.hbm2ddl.SchemaExport;
/**
* 将hbm生成ddl
* @author liang
*
*/
public class ExportDB{
public static void main(String[]args){
//默认读取hibernate.cfg.xml文件
Configuration cfg = new Configuration().configure();
////生成并输出sql到文件(当前目录)和数据库
SchemaExport export = new SchemaExport(cfg);
export.create(true, true);
}
}
测试之前,要提前建立数据库hibernate_first,测试如下:
控制台打印的SQL语句:
drop table if exists User
create table User (id varchar(255) not null, name varchar(255), password varchar(255), createTime date, expireTime date, primary key (id)
(5)建立客户端类Client,添加用户数据到mySQL
package com.liang.hibernate;
import java.util.Date;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class Client {
public static void main(String[]args){
//读取hibernate.cfg.xml文件
Configuration cfg = new Configuration().configure();
//建立SessionFactory
SessionFactory factory =cfg.buildSessionFactory();
//取得session
Session session = null;
try{
//开启session
session = factory.openSession();
//开启事务
session.beginTransaction();
User user = new User();
user.setName("jiuqiyuliang");
user.setPassword("123456");
user.setCreateTime(new Date());
user.setExpireTime(new Date());
//保存User对象
session.save(user);
//提交事务
session.getTransaction().commit();
}catch(Exception e){
e.printStackTrace();
//回滚事务
session.getTransaction().rollback();
}finally{
if(session != null){
if(session.isOpen()){
//关闭session
session.close();
}
}
}
}
}
预处理查询:
string sql="select * from users where username=? and password=?"
Query lookupUser=session.createQuery(sql);
lookupUser.setString(0,username);
lookipUser.setString(1,password);
List rs=lookupUser.list();
右键debug运行,测试完成之后,我们查询一下测试结果:
5、为了在调试过程中能观察到Hibernate的日志输出,最好加入log4j.properties配置文件、在CLASSPATH中新建log4j.properties配置文件或将该配置文件拷贝到src下,便于程序调试。
3. 内容如下:
<span style="font-size:12px;">### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p c{1}:%L - %m%n
### direct messages to file hibernate.log ###
#log4j.appender.file=org.apache.log4j.FileAppender
#log4j.appender.file.File=hibernate.log
#log4j.appender.file.layout=org.apache.log4j.PatternLayout
#log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### set log levels - for more verbose logging change ‘info‘ to ‘debug‘###
log4j.rootLogger=warn, stdout</span>
(2).NET(C#)
与java的语法大致相同
Sqlconnection sql=new sqlconnection(connectionString)//获取连接
String sql="select * from userswhere @[email protected]"cmd=new Sqlcommand(Sql,con); //用占位符代替参数的sql语句
cmd.Parament.Add("参数名",数据类型,长度)//添加参数属性
cmd.Parament.value["@username"]=username; //添加参数属性
reader=cmd.executeReader();//执行sql
(2) php的参数化语句(mysqli,PEAR:MDB2,PDO)
mysqli包
$con=new mysqli("localhost","username","password","db");
$sql="select * from users where username=? and password=?";
$cmd=$con->prepare($sql);
//将参数添加到sql查询中
$cmd->bind_param("ss",$username,$password);
$cmd->execute();
或PHP5.5后更简单的预处理 $result=pg_query_params("SELECT * FROM users WHERE username=$1 AND password=$2",Array($username,$password));
PEAR:MDB2
$mdb2=&MDB2::factory($dsn);
$sql="SELECT * FROM users WHERE username=? AND password=?";
$types=array(‘text‘,‘text‘);
$cmd=$mdb->prepare($sql,$types,MDS2_PREPARE_MANIP);
$data=array($username,$password);
$result=$cmd->execute($data);
PDO
$sql="SELECT * FROM uses WHERE username=:username AND"+"password=:password";
$stmt=$dbh->prepare($sql);
//绑定值和数据类型
$stmt->bindParam(‘:username‘,$username,PDO::PARAM_STR,12);//占位符,值,类型,长度
$stmt->bindParam(‘:username‘,$username,PDO::PARAM_STR,12);
$stmt->execute();
除了以上的web开发语言,还有ios,android,H5都可以对数据库进行预处理操作。
3.输入验证
白名单:
白名单原则需要考虑以下因素,已知的值,数据类型,数据大小,数据范围,数据内容。这几个方面最好都得到约束来限制输入数据,对于大字符级的限制则稍微困难。但是sql注入主要是英文字母或者百分号和数字。
尽量使用白名单,在客户端浏览器的安全过滤不可靠,因为数据会被篡改。可以在WAF层使用黑白名单验证。保证参数化语句的使用。数据库进行编码,读取数据编码。
(1) 用已知值:形成列表与输入值进行校验,在数据库中可以describe tablename;来获取该表的列,若需要对列参数化却无法预处理的时候便可以通过此方法。这是针对列表名情况。
(2) 间接输入:在接受时只传输数据对应的索引,在银行业务中常常使用这种方式,针对用户卡号,不直接传输卡号,而是传输数据库的索引,通过列表库再对应其卡号。若被操作索引则会造成严重影响
黑名单:
黑名单即过滤不安全的字符,也可以利用正则表达式,‘|%|--|;|/\\*|\\\\\\*|_|\\[@|xp_
不能孤立使用黑名单,因为非安全字符多而复杂,无法及时更新。尽管使用了参数化查询也不能没有输入过滤。因为参数化会导致不安全字符直接被注入到数据库,引起二阶注入等其他问题。
PS:对于验证失败的处理,恢复或者错误,恢复不太可取,黑客会构造让过滤器无法迭代引起的多字符攻击,错误就是将页面重定向到一个错误页面,影响了用户体验,但是可以在前台加上验证,保证真正的用户不会被重定向到错误页面。
java中的数据输入验证
JSF框架,一个没有流行起来的框架。自带一些验证机制。类似开发桌面程序一样开发web
String regEx="[^a-zA-Z0-9]"; Pattern p=Pattern.compile(regEx);
if(username!=null){ m=p.matcher(username);
username=m.replaceAll("").trim(); } else if(password!=null){
m=p.matcher(password); password=m.replaceAll("").trim(); }
所有程序的验证主要是通过正则表达式来完成验证,只是调用的方法不同。
PHP中的输入验证:
preg_match(regex,matchstring):使用正则表达式regex对matchstring执行正则表达式匹配,
is_<type>(input):检查输入是否为<type>,如is_numeric
strlen(input):检查输入的长度。
Demo
$username=$_POST[‘username‘];
if(!preg_match("/^[a-zA-Z] {8,12}$/D",$username){
//验证失败
}
一般php框架thinkphp中都不需要用这种方式,而是使用I(input)函数进行过滤,并且其全局过滤是默认进行的。I函数最后一个参数是过滤方法,可以对IP,EMAIL,表达式等进行过滤,全局过滤器var_filters是一个可迭代的过滤器。只有当最后一个参数为NULL时,才是不进行任何过滤。
编码:
无法使用预处理的地方使用编码进行过滤
针对ORACLE:将单个单引号‘替换成两个‘‘,在PL/SQL中需要替换成4个‘‘‘‘,因为单引号是作为字符串结束符,需要一个引用符。使用dbms-assert也可进行输入验证。提供7个函数验证不同的输入
excute immediate ‘select‘ || sys.dbms_assert.SIMPLE_SQL_NAME(FIELD) || ‘from‘ || (sys.dbms_assert.SCHEMA_NAME(OWNER),FALSE) || ‘.‘ ||sys.dbms._assert.QUALIFIED_SQL_NAME(TABLE);
dbms_assert提供的函数
针对SQLserver:select * from userinfo where username like ‘a\\%‘ escape ‘\\‘
escape()指定转义字符,其他的也是替换单引号或者用eacape转义单引号但是在esacpe的过程中escape本身就会被单引号所污染变成字符串,所以该做法不可取,PL/SQL与oracle一样。
针对mysql:也是对‘进行\\‘转义,存储过程中replace(@sql,‘\\‘‘,‘\\\\\\‘‘)
总体来讲,在对输入进行处理的需要注意一下几点:
1. 使用参数化预处理
2. 对敏感字符进行编码转义
3. 对输入进行白名单黑名单结合过滤
4. 对非数据的关键字和标示符在进入数据库前利用数据库函数和自定义函数进行验证。
5. 整体项目使用统一化的编码。
6. 在网络层通过WAF对攻击进行过滤拦截
在设计上避免sql注入
1. 通过存储过程访问数据库:
在数据库访问中,由程序构造的sql语句传入数据库执行,这样的动态sql语句拥有比存储过程更大的权限,所以可以只对应用程序提供存储过程的接口,然后存储过程不能对数据库数据进行insert,update,delete操作。也就减轻了sql注入的影响。而且这样攻击者就无法调用系统的存储过程,对提权和内网渗透将更加困难。
缺点就是处处使用存储过程编程开发将非常繁琐,需要架构一个完善且实用的持久层比较复杂。
2. 使用抽象层访问数据库:
Hibernate,AciveRecord,Entity Framework,ADO.net,JDBC,PDO这样的抽象层访问数据库,程序员无需编写sql语句,通过参数来传递进行与数据库的数据交换。
3.处理敏感数据降低风险
在数据库中的口令,用户个人信息,等感敏数据应隔离保存,处理这些敏感数据时与正常有所不同。
口令:对用户密码等信息采用SHA256等单向加盐不可逆算法进行加密,在登陆过程中将明文加密与密文进行比对来完成验证,并且将salt与哈希分开保存。类似这样的敏感信息可以采取如上方式。
财务信息:使用FIPS认证后的算法进行加密,
存档:定期存档和清除数据库信息
避免明显的对象名:如password就叫password,kennword(德),Motdepass,mdp(法),Wachtwoord(荷兰),Senha(葡萄牙),Haslo(波兰)
honeypot:蜜罐,创建假信息表引诱攻击,将第一时间通知管理员
参数化语句是通过预编译实现参数化,所以关键字和标示符无法在编译后再插入
在应用程序中使用白名单,在web防火墙中使用黑名单
安全防御平台WAF
1. 可配置规则集:拥有自己的指令来配置防御策略,如正则匹配,黑白名单等
2. 请求覆盖范围:针对cookie,url,request heard,等进行保护,覆盖范围大
3. 针对编码攻击url,WAF提供大量编码转换函数来应对,并匹配每个规则,支持LUA语言来自定义规则
4. 响应分析:根据规则集带外规则匹配异常响应,阻塞其返回给用户
5. 入侵检测:通过日志来对sql注入之后的详细信息做检查,该过程不依赖web应用程序。
锁定应用程序数据:
1. 使用较低权限登陆数据库:
2. 隔离数据库登陆:将不同应用程序的权限设定死
3. 撤销public许可
4. 使用存储过程
5. 使用加密技术
高风险数据库函数与存储过程
以上是关于防御sql注入的主要内容,如果未能解决你的问题,请参考以下文章