JSF应对Cross-Site Request Forgery (CSRF)攻击

JSF 2.x 内置了CSRF防护,通过javax.faces.ViewState隐藏字段提供服务器端状态保存。相较于JSF 1.x的脆弱性,JSF 2.0改进了这一值,使其更难以预测,从而增强了CSRF防护。在JSF 2.2中,进一步将此功能纳入规范,并引入可配置的AES密钥来加密客户端状态,以增强GET请求的保护。同时,通过CsrfSessionListener、CsrfForm和 CsrfException等组件,以及在filter中验证Referer和Session中的host,实现对遗留JSF 1.x代码的扩展防护。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JSF 2.x has already builtin CSRF prevention in flavor of javax.faces.ViewState hidden field in the form when using server side state saving. In JSF 1.x this value was namely pretty weak and too easy predictable (it was actually never intended as CSRF prevention). In JSF 2.0 this has been improved by using a long and strong autogenerated value instead of a rather predictable sequence value and thus making it a robust CSRF prevention.

In JSF 2.2 this is even be further improved by making it a required part of the JSF specification, along with a configurable AES key to encrypt the client side state, in case client side state saving is enabled. New in JSF 2.2 is CSRF protection on GET requests by <protected-views>.

使用JSF 1.x的遗留代码,可以扩展form,在form内增加一个token,token通过SessionListener生成,保存在Session中,代码如下:

CsrfSessionListener

public class CsrfSessionListener implements HttpSessionListener {
    private static final String CSRF_TOKEN_NAME = "CsrfToken";

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        HttpSession session = se.getSession();
        session.setAttribute(CSRF_TOKEN_NAME, UUID.randomUUID().toString());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {

    }
}

 CsrfForm

public class CsrfForm extends HtmlForm {

    private static final String CSRF_TOKEN_NAME = "CsrfToken";
    private static final String CSRF_TOKEN_ID = "csrf_token";

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        encodeCsrfTokenInput(context);
        super.encodeEnd(context);
    }

    private void encodeCsrfTokenInput(FacesContext context) throws IOException {
        ResponseWriter responseWriter = context.getResponseWriter();
        responseWriter.startElement("input", null);
        responseWriter.writeAttribute("type", "hidden", null);
        responseWriter.writeAttribute("id", getCsrfInputClientId(context), null);
        responseWriter.writeAttribute("name", CSRF_TOKEN_ID, null);
        responseWriter.writeAttribute("value", getToken(context), "value");
        responseWriter.endElement("input");
    }

    @Override
    public void decode(FacesContext context) {
        Map<String, String> requestMap = context.getExternalContext().getRequestParameterMap();
        String value = requestMap.get(CSRF_TOKEN_ID);

        // check if the token exists
        if (value == null || "null".equals(value) || "".equals(value)) {
            throw new CsrfException("CSRFToken is missing!");
        }

        String token = getToken(context);
        // check the values for equality
        if (!value.equalsIgnoreCase(token)) {
            throw new CsrfException("CSRFToken does not match!");
        }
        super.decode(context);
    }

    private String getCsrfInputClientId(FacesContext context) {
        return getClientId(context) + ":" + CSRF_TOKEN_ID;
    }

    private String getToken(FacesContext context) {
        HttpSession session = (HttpSession) context.getExternalContext().getSession(false);
        return (String) session.getAttribute(CSRF_TOKEN_NAME);
    }

}

CsrfException

public class CsrfException extends RuntimeException {
    public CsrfException(String message) {
        super(message);
    }
}

然后在faces-config.xml中增加配置,使用CsrfForm:

<component>
  <component-type>javax.faces.HtmlForm</component-type>
  <component-class>org.iata.csrf.CsrfForm</component-class>
</component>

验证Referer

登录后将host保存在Session中

session.setAttribute("HOST", req.getHeader("host"));

然后在filter中验证:

package org.iata.csrf;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

public class CsrfFilter implements Filter {
    private static final String HTTP = "http://";
    private static final String HTTPS = "https://";
    private static final String REGEX = "https://|http://";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        HttpSession session = req.getSession(false);
        String host = (String) session.getAttribute("HOST");

        String referer = req.getHeader("Referer");
        if (referer == null) {
            chain.doFilter(request, response);
            return;
        }

        if (referer.startsWith(HTTP) || referer.startsWith(HTTPS)) {
            referer = referer.replaceFirst(REGEX, "");
        }
        if (referer.startsWith(host)) {
            chain.doFilter(request, response);
            return;
        }

        res.sendRedirect(req.getContextPath() + "/NoAuthorityError.seam");
    }

    @Override
    public void destroy() {

    }
}

参考文档

CSRF 攻击的应对之道

CSRF的攻击与防御

Java EE 7: Implementing CSRF Protection with JSF 2.2

Preventing CSRF with JSF 2.0

Tomcat CSRF Prevention Filter

Seam Cross Site Request Forgery

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值