前端攻击主要包括XSS(跨站脚本攻击)、CSRF(跨站请求伪造)、SQL注入。“Noodles”的技术周刊 中有详细解释。
一、XSS&SQL注入
它们的发生是在用户恶意输入和抓包修改情况下,由于前后端没有做字符过滤,导致恶意代码的执行。
1、XSS&SQL注入编码技巧
前端的转义是必不可少的,为了防止抓包修改参数值,我们重点放在后端。网上有个XSSProject,地址为:http://yunjiechao-163-com.ite...,其中封装好了一些功能,特别方便。。。
也可以用下面的代码:
/**
*
* XSS过滤
* @author Alex
*
*/
public class XSSFilter implements Filter{
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
// TODO Auto-generated method stub
HttpServletRequest request = (HttpServletRequest) arg0;
HttpServletResponse response = (HttpServletResponse) arg1;
XssAndSqlHttpServletRequestWrapper xssRequest = new XssAndSqlHttpServletRequestWrapper(request);//采用包装器过滤掉恶意字符
arg2.doFilter(xssRequest, response);
}
}
/**
*
* XSS包装器
* @author Alex
*
*/
public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrapper {
private Logger log = Logger.getLogger(getClass());
private HttpServletRequest orgRequest = null;
public XssAndSqlHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
orgRequest = request;
}
@Override
public String getParameter(String name) {
String value = null;
try {
//不过滤菜单
if(!name.equals("menuHtml")){//自己的菜单,无视掉
value = super.getParameter(xssEncode(name));
if (value != null) {
value = URLDecoder.decode(value, Constant.UTF);//处理中文乱码
value = xssEncode(value);
}
}else{
value = super.getParameter(name);
}
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
log.error(e.getMessage());
}
return value;
}
@Override
public String getHeader(String name) {//请求头也可能插入
String value = null;
try {
value = super.getHeader(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
log.error(e.getMessage());
}
return value;
}
private static String xssEncode(String s) {//替换成中文字符
if (s == null || s.isEmpty()) {
return s;
}else{
s = stripXSSAndSql(s);
}
StringBuilder sb = new StringBuilder(s.length() + 16);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '>':
sb.append(">");// 转义大于号
break;
case '<':
sb.append("<");// 转义小于号
break;
case '\'':
sb.append("'");// 转义单引号
break;
case '\"':
sb.append(""");// 转义双引号
break;
case '&':
sb.append("&");// 转义&
break;
case '#':
sb.append("#");// 转义#
break;
default:
sb.append(c);
break;
}
}
return sb.toString();
}
public HttpServletRequest getOrgRequest() {
return orgRequest;
}
public static HttpServletRequest getOrgRequest(HttpServletRequest req) {
if (req instanceof XssAndSqlHttpServletRequestWrapper) {
return ((XssAndSqlHttpServletRequestWrapper) req).getOrgRequest();
}
return req;
}
public static String stripXSSAndSql(String value) {
if (value != null) {
// Avoid anything between script tags
Pattern scriptPattern = Pattern.compile("<[\r\n| | ]*script[\r\n| | ]*>(.*?)</[\r\n| | ]*script[\r\n| | ]*>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid anything in a src="http://www.yihaomen.com/article/java/..." type of e-xpression
scriptPattern = Pattern.compile("src[\r\n| | ]*=[\r\n| | ]*[\\\"|\\\'](.*?)[\\\"|\\\']", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Remove any lonesome </script> tag
scriptPattern = Pattern.compile("</[\r\n| | ]*script[\r\n| | ]*>", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Remove any lonesome <script ...> tag
scriptPattern = Pattern.compile("<[\r\n| | ]*script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid eval(...) expressions
scriptPattern = Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid e-xpression(...) expressions
scriptPattern = Pattern.compile("e-xpression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid javascript:... expressions
scriptPattern = Pattern.compile("javascript[\r\n| | ]*:[\r\n| | ]*", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid vbscript:... expressions
scriptPattern = Pattern.compile("vbscript[\r\n| | ]*:[\r\n| | ]*", Pattern.CASE_INSENSITIVE);
value = scriptPattern.matcher(value).replaceAll("");
// Avoid onload= expressions
scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
value = scriptPattern.matcher(value).replaceAll("");
}
return value;
}
}
web.xml中添加
<filter>
<filter-name>XSSFilter</filter-name>
<filter-class>com.xxx.filter.XSSFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>XSSFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2、举例
用Python的flask框架演示,代码很简单,顺便演示下flask模板的注入
服务端代码:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from flask import Flask, request, render_template_string, render_template, make_response
app = Flask(__name__)
@app.route('/hello-injection')
def hello_inject():
person = {'name': 'asd', 'secret': 'aaaaaaaaaaa'}
if request.args.get('name'):
person['name'] = request.args.get('name')
template = '''<h2>Hello %s</h2>''' % person['name']
return render_template_string(template, person=person)
def get_user_file(f_name):
with open(f_name) as f:
return f.readlines()
app.jinja_env.globals['get_user_file'] = get_user_file
if __name__ == "__main__":
app.run(debug=True)
运行好后,访问如下几个地址:
(1)模板字符串中字符串拼接或替换引发的安全隐患
http://127.0.0.1:5000/hello-injection?name=ForrestX386.{{person.secret}}
,页面显示:Hello ForrestX386.aaaaaaaaaaa,秘钥aaaaaaaaaaa被泄漏。
http://127.0.0.1:5000/hello-injection?name=ForrestX386.{{get_user_file('E:\ssd.txt')}}
,显示ssd.txt文件中的内容。
原因:Jinja2在模板渲染的时候将person['name']的值替换掉{{person['name']}}, 而不会对person['name']内容进行二次渲染(这样即使`person['name']中含有{{}}也不会进行渲染,而只是把它当做普通字符串)。
(2)render_template_string 的安全隐患
我们知道Flask render_template函数在模板渲染(使用Jinja2模板引擎)的时候,会自动对模板(常见的模板后缀才进行自动HTML转码,比如.html,.htm等,不常见的模板后缀是不会进行HTML自动编码的,下面会介绍到)内容进行HTML实体编码,从而避免XSS漏洞的发生,但是Flask中的render_template_string 函数却不会对要渲染的模板字符串进行自动HTML实体编码,存在XSS安全隐患
http://127.0.0.1:5000/hello-injection?name=ForrestX386.<script>alert(1)</script>
,如果Chrome没有显示弹出,请设置:
上图的设置会取消浏览器的xss-auditor启动(一种浏览器内建的xss防御模块,可阻止大多数反射型xss)。如果不想使用上图的方式启动,可以将服务端代码改为:
@app.route('/hello-injection')
def hello_inject():
person = {'name': 'asd', 'secret': 'aaaaaaaaaaa'}
if request.args.get('name'):
person['name'] = request.args.get('name')
template = '''<h2>Hello %s</h2>''' % person['name']
response = make_response(person['name'])
response.headers['X-XSS-Protection'] = '0' # xss auditor关闭
return response
将xss-auditor关闭可以弹出。如果开启呢?还能弹出吗?修改成以下代码:
@app.route('/hello-injection')
def hello_inject():
person = {'name': 'asd', 'secret': 'aaaaaaaaaaa'}
if request.args.get('name'):
person['name'] = request.args.get('name')
template = '''<h2>Hello %s</h2>''' % person['name']
response = make_response(person['name'].replace('>', '>;'))
response.headers['X-XSS-Protection'] = '1' # xss auditor开启
return response
竟然可以弹出。。。这里介绍下auditor的:
通常情况下,我们都会对用户提交的数据进行一些处理,如果这些处理导致和提交的内容不一样了,但是仍然可以执行,比如像本例一样。那么xss auditor 就无能为力了。不过xss auditor本身的智能度也挺高,像字符编码,大小写变化这种变化依然躲不过xss auditor。
二、CSRF
1、Referer
在过滤器或拦截器中判读请求头的Referer值,如果同域名就可以继续访问,否则请求被拦截。
String referer = request.getHeader("Referer");
if(referer.startsWith("http://www.xxx.com")){
chain.doFilter(request, response);
}else{
return ;
}
弊端:有些网站或用户会停用Referer,所以上面这种方式会导致正常用户也不能访问系统。
2、Token
这种方式用的最多,是将Token作为每次请求的参数来验证请求是否有效。
弊端:黑客可通过发送用户链接,盗取Token值。
3、JWT
为何用JWT?
白话意思是用户的信息可放在客户端保存,代替之前服务器session的保存方式。为了契合前后分离的说法。所以请不要认为JWT可以预防CSRF,这是一种错误的理解!
目前存储JWT的方式有以下几种:
1、Cookie存取
优点:不易遭受XSS(可设置HttpOnly)
弊端:易遭受CSRF。
2、LocalStorage
优点:不产生CSRF、存储量大
缺点:易遭受XSS、难清除(Android机很难清除)
3、
YOU stick the (JWT) token in the Authorization HTTP header of a request.
这是http://stackoverflow.com/描述的一种方法。
总结起来就是各有优缺点。个人觉得CSRF较难防御,看个人轻重程度了。
下面是代码:
其中有些是自己项目的逻辑,请需修改。
/**
*
* @author Alex
*
*/
public class JWTFilter implements Filter{
private Logger log = Logger.getLogger(getClass());
public void destroy() {
// TODO Auto-generated method stub
}
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
// TODO Auto-generated method stub
HttpServletRequest request = (HttpServletRequest) arg0;
HttpServletResponse response = (HttpServletResponse) arg1;
AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
try {
String jwt = null;
Cookie[] cookies = request.getCookies();
for(Cookie cookie:cookies){
if(cookie.getName().equals("jwt")){
jwt = cookie.getValue();
break;
}
}
String referer = request.getHeader("Referer");
String userName = (String) principal.getName();
if(referer!=null&&referer.indexOf("/XXX/login")!=-1&&jwt==null){//登录时设置JWT及判断Cookie中有无JWT
jwt = JWTWrapper.createJWT(userName);//userName单点登录用户名
response.addHeader("Set-Cookie", "jwt="+jwt+";Path=/;HttpOnly");//防止JS获取Cookie
}else{
int judge = JWTWrapper.judgeJWT(jwt, userName);//判断JWT是否过期
if(judge==-1){//被篡改
return ;
}else if(judge==0){//过期或将要过期
String jwt_new = JWTWrapper.createJWT(userName);
response.addHeader("Set-Cookie", "jwt="+jwt_new+";Path=/;HttpOnly");
}
}
arg2.doFilter(request, response);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
log.error(e.getMessage());
}
}
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub
}
}
/**
*
* JWT包装器
* @author Alex
*
*/
public class JWTWrapper {
private static String iss = "XXX";//签发者
private static Long exp_add = Long.valueOf(30*60*1000);//过期时间半小时
private static String des_key = "XXXXXXX";//des密钥
/**
* 创建JWT
* @param aud 接收方
*/
public static String createJWT(String aud){
String jwt = null;
try {
Long iat = System.currentTimeMillis();//签发时间,应该用秒
String header = "{\"typ\":\"JWT\",\"alg\":\"DES\"}";//头部
String payload = "{\"iss\":\""+iss+"\",\"aud\":\""+aud+"\",\"iat\":"+iat+",\"exp\":"+(iat+exp_add)+"}";//载荷
String signature = null;//签名
header = Base64.encodeBase64URLSafeString(header.getBytes(Constant.UTF));
payload = Base64.encodeBase64URLSafeString(payload.getBytes(Constant.UTF));
signature = DesUtil.encrypt(header+"."+payload, des_key);//可用其它加密
jwt = header+"."+payload+"."+signature;
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
System.out.println(e.getMessage());
}
return jwt;
}
/**
* 验证JWT
* @param jwt
* @param aud 接收方
* @return
*/
public static int judgeJWT(String jwt,String aud){
int judge = -1;//-1:篡改的JWT
try {
if(jwt!=null&&jwt.indexOf(".")!=-1){//分割JWT
String[] strs = jwt.split("\\.");
String signature_new = strs[0]+"."+strs[1]+"."+DesUtil.encrypt(strs[0]+"."+strs[1], des_key);//签名
if(signature_new.startsWith(jwt)){//未被篡改
String payload = new String(Base64.decodeBase64(strs[1]), Constant.UTF);//载荷
JSONObject JO = JSONObject.fromObject(payload);
if(JO.getString("iss").equals(iss)&&JO.getString("aud").equals(aud)){
Long exp = JO.getLong("exp");
Long iat = System.currentTimeMillis();//签发时间
if(exp>iat&&(exp-iat)>1*60*1000){//过期时间>1分钟
judge = 1;//正常,不需更新JWT
}else{
judge = 0;//JWT过期或将要过期
}
}
}
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
System.out.println(e.getMessage());
}
return judge;
}
}
先这样吧,不太会写文章,希望大家海涵。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。