Web安全 – 预防前端渗透的编码技巧

python基础

浏览数:282

2019-4-27

前端攻击主要包括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;
    }
    
}

先这样吧,不太会写文章,希望大家海涵。

作者:alexnevsky