CSRF攻击与防卫

CSRF是什么

CSRF在百度百科中是这般说的:“CSRF(Cross-site request
forgery跨站请求伪造,也被称呼“one click attack”或者session
riding,经常缩写为CSRF或者XSRF,是一种对网站的恶心使用。虽然听起来像跨站脚本(XSS),但它与XSS卓殊不同,并且攻击情势几乎相左。XSS利用站点内的相信用户,而CSRF则经过伪装来自受看重用户的哀告来行使受倚重的网站。与XSS攻击相比,CSRF攻击往往不大流行(因而对其举办防范的资源也一定难得)和难以防范,所以被认为比XSS更具危险性。”。

 

CSRF攻击原理

图片 1

从上图可以看到,要做到三次CSRF攻击,受害者必须逐项完成2个步骤

1.登录受信任网站A,并在地头生成Cookie。

2.在不登出A的意况下,访问危险网站B (那多少个进程做到攻击)

 

CSRF 举例

CSRF
攻击可以在事主毫不知情的状态下以被害人名义冒领请求发送给受攻击站点,从而在未曾授权的情景下执行在权力珍视之下的操作。

例如,受害者 鲍勃(Bob) 在银行有一笔存款,通过对银行的网站发送请求
http://bank.example/withdraw?account=bob&amount=1000000&for=bob2可以使
Bob 把 1000000 的存款转到 bob2 的账号下。

普通状态下,该请求发送到网站后,服务器会先验证该请求是否来自一个官方的
session,并且该 session 的用户 鲍勃 已经打响登陆。

黑客 Mallory 自己在该储蓄所也有账户,他通晓上文中的 URL
可以把钱举行转帐操作。

Mallory
可以团结发送一个请求给银行:http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory。但是这个请求来自
Mallory 而非 鲍伯(Bob),他不可能经过平安认证,由此该请求不会起效用。

此时,Mallory 想到利用 CSRF
的攻击形式,他先自己做一个网站,在网站中放入如下代码:<img
src=”http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory
” />,并且经过广告等诱惑 Bob 来访问他的网站。当 Bob访问该网站时,上述 url 就会从 鲍勃(Bob) 的浏览器发向银行,而这一个请求会顺便
鲍伯(Bob) 浏览器中的 cookie
一起发向银行服务器。大多数情形下,该请求会失败,因为她要求 鲍勃的讲明音讯。

然则,就算 Bob 当时恰巧刚访问他的银行后赶紧,他的浏览器与银行网站之间的
session 尚未过期,浏览器的 cookie 之中含有 鲍勃 的求证音信。

这时,喜剧暴发了,这一个 url 请求就会博得响应,钱将从 鲍勃(Bob) 的账号转移到
Mallory 的账号,而 鲍勃(Bob) 当时毫不知情。等以后 鲍伯(Bob)发现账户钱少了,虽然她去银行查询日志,他也不得不发现真正有一个出自于她自己的合法请求改换了资本,没有此外被口诛笔伐的印痕。而
Mallory 则可以拿到钱后逍遥法外。

 

CSRF 防御

对于Web应用来说可以有以下三种方案:

1)验证 HTTP Referer 字段

  遵照 HTTP 协议,在 HTTP
头中有一个字段叫 Referer,它记录了该 HTTP
请求的来源于地址。在平日情状下,访问一个有惊无险受限页面的呼吁来自于同一个网站,比如需要拜访
http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆
bank.example,然后经过点击页面上的按钮来触发转账事件。这时,该转帐请求的
Referer 值就会是转账按钮所在的页面的 URL,平常是以 bank.example
域名起初的地址。而只要黑客要对银行网站实施 CSRF
攻击,他只得在她协调的网站协会请求,当用户通过黑客的网站发送请求到银行时,该请求的
Referer 是指向黑客自己的网站。由此,要守护 CSRF
攻击,银行网站只需要对此每一个转速请求验证其 Referer 值,假使是以
bank.example
起先的域名,则印证该请求是缘于银行网站自己的呼吁,是法定的。假设 Referer
是此外网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。

  这种方法的明确的便宜就是简单易行,网站的平时开发人员不需要操心
CSRF 的漏洞,只需要在结尾给持有平安敏感的呼吁统一扩大一个拦截器来检查
Referer
的值就可以。特别是对此当下幸存的体系,不需要变更最近系统的另外已有代码和逻辑,没有风险,相当轻便。

  不过,这种情势毫无万无一失。Referer 的值是由浏览器提供的,尽管 HTTP
协议上有明确的要求,可是各种浏览器对于 Referer
的实际贯彻可能有出入,并无法担保浏览器自身没有安全漏洞。使用验证 Referer
值的法子,就是把安全性都依靠于第三方(即浏览器)来维系,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如
IE6 或 FF2,最近早已有一部分办法可以篡改 Referer 值。如若 bank.example
网站援助 IE6 浏览器,黑客完全能够把用户浏览器的 Referer 值设为以
bank.example 域名起初的地方,这样就足以通过验证,从而进行 CSRF 攻击。

  固然是拔取最新的浏览器,黑客不可以篡改 Referer
值,那种情势仍旧有题目。因为 Referer
值会记录下用户的访问来源,有些用户认为这样会侵犯到他俩协调的隐私权,特别是有些社团担心
Referer
值会把集体内网中的某些音信泄露到外网中。因而,用户自己能够设置浏览器使其在殡葬请求时不再提供
Referer。当她们健康访问银行网站时,网站会因为请求没有 Referer 值而以为是
CSRF 攻击,拒绝法定用户的造访。

2)在伸手地址中添加 token 并证实

  CSRF
攻击之所以可以成功,是因为黑客可以完全伪造用户的伸手,该请求中装有的用户验证信息都是存在于
cookie 中,因而黑客能够在不精晓这一个注脚音信的景色下直接动用用户自己的
cookie 来因而安全注明。要抵御
CSRF,关键在于在伏乞中放入黑客所不可以伪造的信息,并且该音信不存在于
cookie 之中。可以在 HTTP 请求中以参数的形式进入一个任意发生的
token,并在服务器端建立一个拦截器来证实这一个 token,假如请求中一向不 token
或者 token 内容不正确,则认为可能是 CSRF 攻击而推辞该请求。

  这种方法要比检查 Referer 要安全一些,token
能够在用户登陆后发出并放于 session 之中,然后在每趟请求时把 token 从
session 中拿出,与请求中的 token 举行比对,但这种格局的困难在于怎么着把
token 以参数的样式进入请求。对于 GET 请求,token
将附在请求地址然后,这样 URL 就改为 http://url?csrftoken=tokenvalue。
而对于 POST 请求来说,要在 form 的尾声加上 <input type=”hidden”
name=”csrftoken” value=”tokenvalue”/>,这样就把 token
以参数的花样进入请求了。可是,在一个网站中,可以接受请求的地点十分多,要对此每一个呼吁都丰硕token
是很麻烦的,并且很容易漏掉,经常选取的措施就是在历次页面加载时,使用
javascript 遍历整个 dom 树,对于 dom 中所有的 a 和 form 标签后投入
token。这样能够缓解大部分的伸手,不过对于在页面加载之后动态变化的 html
代码,这种方法就从不效果,还亟需程序员在编码时手动添加 token。

  该措施还有一个败笔是难以保证 token
本身的安全。特别是在一些论坛之类辅助用户自己宣布内容的网站,黑客可以在位置宣布自己个人网站的地点。由于系统也会在这多少个地方后边加上
token,黑客可以在和谐的网站上收获这么些 token,并随即就足以动员 CSRF
攻击。为了避免这点,系统可以在丰盛 token
的时候增添一个论断,假若这么些链接是链到自己本站的,就在末端添加
token,尽管是通往外网则不加。可是,即便这些 csrftoken
不以参数的模式附加在呼吁之中,黑客的网站也一致可以因而 Referer
来获取那个 token 值以发动 CSRF 攻击。这也是有些用户喜好手动关闭浏览器
Referer 效用的原委。

 

3)在 HTTP 头中自定义属性并表明

  这种艺术也是应用 token
并拓展认证,和上一种方法不同的是,那里并不是把 token 以参数的形式置于
HTTP 请求之中,而是把它放到 HTTP 头中自定义的特性里。通过 XMLHttpRequest
那一个类,能够一遍性给拥有此类请求加上 csrftoken 那多少个 HTTP 头属性,并把
token 值放入其中。这样化解了上种方法在伸手中进入 token
的孤苦,同时,通过 XMLHttpRequest
请求的地方不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer
泄露到其他网站中去。

  然则那种方法的局限性分外大。XMLHttpRequest 请求常常用于 Ajax
方法中对于页面局部的异步刷新,并非所有的伏乞都合乎用这些类来倡导,而且通过此类请求获得的页面不能够被浏览器所记录下,从而举行发展,后退,刷新,收藏等操作,给用户带来不便。另外,对于从未开展
CSRF 防护的残留系统的话,要选拔这种艺术来展开预防,要把持有请求都改为
XMLHttpRequest 请求,这样几乎是要重写整个网站,这代价无疑是不可能经受的。

 

Tomcat中的CsrfPreventionFilter

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.filters;

import java.io.IOException;
import java.io.Serializable;
import java.security.SecureRandom;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

/**
 * Provides basic CSRF protection for a web application. The filter assumes
 * that:
 * <ul>
 * <li>The filter is mapped to /*</li>
 * <li>{@link HttpServletResponse#encodeRedirectURL(String)} and
 * {@link HttpServletResponse#encodeURL(String)} are used to encode all URLs
 * returned to the client
 * </ul>
 */
public class CsrfPreventionFilter extends FilterBase {

    private static final Log log =
        LogFactory.getLog(CsrfPreventionFilter.class);

    private String randomClass = SecureRandom.class.getName();

    private Random randomSource;

    private final Set<String> entryPoints = new HashSet<String>();

    private int nonceCacheSize = 5;

    @Override
    protected Log getLogger() {
        return log;
    }

    /**
     * Entry points are URLs that will not be tested for the presence of a valid
     * nonce. They are used to provide a way to navigate back to a protected
     * application after navigating away from it. Entry points will be limited
     * to HTTP GET requests and should not trigger any security sensitive
     * actions.
     * 
     * @param entryPoints   Comma separated list of URLs to be configured as
     *                      entry points.
     */
    public void setEntryPoints(String entryPoints) {
        String values[] = entryPoints.split(",");
        for (String value : values) {
            this.entryPoints.add(value.trim());
        }
    }

    /**
     * Sets the number of previously issued nonces that will be cached on a LRU
     * basis to support parallel requests, limited use of the refresh and back
     * in the browser and similar behaviors that may result in the submission
     * of a previous nonce rather than the current one. If not set, the default
     * value of 5 will be used.
     * 
     * @param nonceCacheSize    The number of nonces to cache
     */
    public void setNonceCacheSize(int nonceCacheSize) {
        this.nonceCacheSize = nonceCacheSize;
    }

    /**
     * Specify the class to use to generate the nonces. Must be in instance of
     * {@link Random}.
     * 
     * @param randomClass   The name of the class to use
     */
    public void setRandomClass(String randomClass) {
        this.randomClass = randomClass;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // Set the parameters
        super.init(filterConfig);

        try {
            Class<?> clazz = Class.forName(randomClass);
            randomSource = (Random) clazz.newInstance();
        } catch (ClassNotFoundException e) {
            ServletException se = new ServletException(sm.getString(
                    "csrfPrevention.invalidRandomClass", randomClass), e);
            throw se;
        } catch (InstantiationException e) {
            ServletException se = new ServletException(sm.getString(
                    "csrfPrevention.invalidRandomClass", randomClass), e);
            throw se;
        } catch (IllegalAccessException e) {
            ServletException se = new ServletException(sm.getString(
                    "csrfPrevention.invalidRandomClass", randomClass), e);
            throw se;
        }
    }


    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        ServletResponse wResponse = null;

        if (request instanceof HttpServletRequest &&
                response instanceof HttpServletResponse) {

            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse res = (HttpServletResponse) response;

            boolean skipNonceCheck = false;

            if (Constants.METHOD_GET.equals(req.getMethod())) {
                String path = req.getServletPath();
                if (req.getPathInfo() != null) {
                    path = path + req.getPathInfo();
                }

                if (entryPoints.contains(path)) {
                    skipNonceCheck = true;
                }
            }

            HttpSession session = req.getSession(false);

            @SuppressWarnings("unchecked")
            LruCache<String> nonceCache = (session == null) ? null
                    : (LruCache<String>) session.getAttribute(
                            Constants.CSRF_NONCE_SESSION_ATTR_NAME);
       // 对请求进行验证
            if (!skipNonceCheck) {
                String previousNonce =
                    req.getParameter(Constants.CSRF_NONCE_REQUEST_PARAM);

                if (nonceCache == null || previousNonce == null ||
                        !nonceCache.contains(previousNonce)) {
                    res.sendError(HttpServletResponse.SC_FORBIDDEN);
                    return;
                }
            }

            if (nonceCache == null) {
                nonceCache = new LruCache<String>(nonceCacheSize);
                if (session == null) {
                    session = req.getSession(true);
                }
                session.setAttribute(
                        Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonceCache);
            }
            // 为下一次请求,生成一个随机数放在URL中
            String newNonce = generateNonce();
            // 同时将随机数存储在session中
            nonceCache.add(newNonce);

            wResponse = new CsrfResponseWrapper(res, newNonce);
        } else {
            wResponse = response;
        }

        chain.doFilter(request, wResponse);
    }


    @Override
    protected boolean isConfigProblemFatal() {
        return true;
    }


    /**
     * Generate a once time token (nonce) for authenticating subsequent
     * requests. This will also add the token to the session. The nonce
     * generation is a simplified version of ManagerBase.generateSessionId().
     * 
     */
    protected String generateNonce() {
        byte random[] = new byte[16];

        // Render the result as a String of hexadecimal digits
        StringBuilder buffer = new StringBuilder();

        randomSource.nextBytes(random);

        for (int j = 0; j < random.length; j++) {
            byte b1 = (byte) ((random[j] & 0xf0) >> 4);
            byte b2 = (byte) (random[j] & 0x0f);
            if (b1 < 10)
                buffer.append((char) ('0' + b1));
            else
                buffer.append((char) ('A' + (b1 - 10)));
            if (b2 < 10)
                buffer.append((char) ('0' + b2));
            else
                buffer.append((char) ('A' + (b2 - 10)));
        }

        return buffer.toString();
    }

    protected static class CsrfResponseWrapper
            extends HttpServletResponseWrapper {

        private String nonce;

        public CsrfResponseWrapper(HttpServletResponse response, String nonce) {
            super(response);
            this.nonce = nonce;
        }

        @Override
        @Deprecated
        public String encodeRedirectUrl(String url) {
            return encodeRedirectURL(url);
        }

        @Override
        public String encodeRedirectURL(String url) {
            return addNonce(super.encodeRedirectURL(url));
        }

        @Override
        @Deprecated
        public String encodeUrl(String url) {
            return encodeURL(url);
        }

        @Override
        public String encodeURL(String url) {
            return addNonce(super.encodeURL(url));
        }

        /**
         * Return the specified URL with the nonce added to the query string. 
         *
         * @param url URL to be modified
         * @param nonce The nonce to add
         */
        private String addNonce(String url) {

            if ((url == null) || (nonce == null))
                return (url);

            String path = url;
            String query = "";
            String anchor = "";
            int pound = path.indexOf('#');
            if (pound >= 0) {
                anchor = path.substring(pound);
                path = path.substring(0, pound);
            }
            int question = path.indexOf('?');
            if (question >= 0) {
                query = path.substring(question);
                path = path.substring(0, question);
            }
            StringBuilder sb = new StringBuilder(path);
            if (query.length() >0) {
                sb.append(query);
                sb.append('&');
            } else {
                sb.append('?');
            }
            sb.append(Constants.CSRF_NONCE_REQUEST_PARAM);
            sb.append('=');
            sb.append(nonce);
            sb.append(anchor);
            return (sb.toString());
        }
    }

    protected static class LruCache<T> implements Serializable {

        private static final long serialVersionUID = 1L;

        // Although the internal implementation uses a Map, this cache
        // implementation is only concerned with the keys.
        private final Map<T,T> cache;

        public LruCache(final int cacheSize) {
            cache = new LinkedHashMap<T,T>() {
                private static final long serialVersionUID = 1L;
                @Override
                protected boolean removeEldestEntry(Map.Entry<T,T> eldest) {
                    if (size() > cacheSize) {
                        return true;
                    }
                    return false;
                }
            };
        }

        public void add(T key) {
            synchronized (cache) {
                cache.put(key, null);
            }
        }

        public boolean contains(T key) {
            synchronized (cache) {
                return cache.containsKey(key);
            }
        }
    }
}

 

相关文章