/**
* The OWASP CSRFGuard Project, BSD License
* Eric Sheridan (eric@infraredsecurity.com), Copyright (c) 2011
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of OWASP nor the names of its contributors may be used
* to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.owasp.csrfguard.servlet;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.owasp.csrfguard.CsrfGuard;
import org.owasp.csrfguard.CsrfGuardServletContextListener;
import org.owasp.csrfguard.log.LogLevel;
import org.owasp.csrfguard.util.CsrfGuardUtils;
import org.owasp.csrfguard.util.Streams;
import org.owasp.csrfguard.util.Strings;
import org.owasp.csrfguard.util.Writers;
public final class JavaScriptServlet extends HttpServlet {
private static final long serialVersionUID = -1459584282530150483L;
private static final String TOKEN_NAME_IDENTIFIER = "%TOKEN_NAME%";
private static final String TOKEN_VALUE_IDENTIFIER = "%TOKEN_VALUE%";
private static final String DOMAIN_ORIGIN_IDENTIFIER = "%DOMAIN_ORIGIN%";
private static final String DOMAIN_STRICT_IDENTIFIER = "%DOMAIN_STRICT%";
private static final String INJECT_INTO_XHR_IDENTIFIER = "%INJECT_XHR%";
private static final String INJECT_INTO_FORMS_IDENTIFIER = "%INJECT_FORMS%";
private static final String INJECT_GET_FORMS_IDENTIFIER = "%INJECT_GET_FORMS%";
private static final String INJECT_FORM_ATTRIBUTES_IDENTIFIER = "%INJECT_FORM_ATTRIBUTES%";
private static final String INJECT_INTO_ATTRIBUTES_IDENTIFIER = "%INJECT_ATTRIBUTES%";
private static final String CONTEXT_PATH_IDENTIFIER = "%CONTEXT_PATH%";
private static final String SERVLET_PATH_IDENTIFIER = "%SERVLET_PATH%";
private static final String X_REQUESTED_WITH_IDENTIFIER = "%X_REQUESTED_WITH%";
private static final String TOKENS_PER_PAGE_IDENTIFIER = "%TOKENS_PER_PAGE%";
private static ServletConfig servletConfig = null;
public static ServletConfig getStaticServletConfig() {
return servletConfig;
}
@Override
public void init(ServletConfig theServletConfig) {
servletConfig = theServletConfig;
//print again since it might change based on servlet config of javascript servlet
CsrfGuardServletContextListener.printConfigIfConfigured(servletConfig.getServletContext(),
"Printing properties after Javascript servlet, note, the javascript properties have now been initialized: ");
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
String refererHeader = request.getHeader("referer");
boolean hasError = false;
Pattern javascriptRefererPattern = CsrfGuard.getInstance().getJavascriptRefererPattern();
if(refererHeader != null && !javascriptRefererPattern.matcher(refererHeader).matches()) {
CsrfGuard.getInstance().getLogger().log(LogLevel.Error, "Referer domain " + refererHeader + " does not match regex: " + javascriptRefererPattern.pattern());
response.sendError(404);
hasError = true;
}
if (refererHeader != null && CsrfGuard.getInstance().isJavascriptRefererMatchDomain()) {
//this is something like http://something.com/path or https://something.com/path
String url = request.getRequestURL().toString();
String requestProtocolAndDomain = CsrfGuardUtils.httpProtocolAndDomain(url);
String refererProtocolAndDomain = CsrfGuardUtils.httpProtocolAndDomain(refererHeader);
if (!refererProtocolAndDomain.equals(requestProtocolAndDomain)) {
CsrfGuard.getInstance().getLogger().log(LogLevel.Error, "Referer domain " + refererHeader + " does not match request domain: " + url);
hasError = true;
response.sendError(404);
}
}
if (!hasError) {
//save this path so javascript is whitelisted
String javascriptPath = request.getContextPath() + request.getServletPath();
//dont know why there would be more than one... hmmm
if (javascriptUris.size() < 100) {
javascriptUris.add(javascriptPath);
}
writeJavaScript(request, response);
}
}
/**
* whitelist the javascript servlet from csrf errors
* @return the javascriptUris
*/
public static Set<String> getJavascriptUris() {
return javascriptUris;
}
/** whitelist the javascript servlet from csrf errors */
private static Set<String> javascriptUris = new HashSet<String>();
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
CsrfGuard csrfGuard = CsrfGuard.getInstance();
String isFetchCsrfToken = request.getHeader("FETCH-CSRF-TOKEN");
// CUSTOM: workaround for IE8
if (isFetchCsrfToken == null) {
isFetchCsrfToken = request.getParameter("FETCH-CSRF-TOKEN-PARAM");
}
// END CUSTOM
if (csrfGuard != null && isFetchCsrfToken != null){
fetchCsrfToken(request, response);
} else {
if (csrfGuard != null && csrfGuard.isTokenPerPageEnabled()) {
writePageTokens(request, response);
} else {
response.sendError(404);
}
}
}
private void fetchCsrfToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession(true);
@SuppressWarnings("unchecked")
CsrfGuard csrfGuard = CsrfGuard.getInstance();
String token_name = csrfGuard.getTokenName();
String token_value = (String) session.getAttribute(csrfGuard.getSessionKey());
String token_pair = token_name + ":" + token_value;
/** setup headers **/
response.setContentType("text/plain");
/** write dynamic javascript **/
OutputStream output = null;
PrintWriter writer = null;
try {
output = response.getOutputStream();
writer = new PrintWriter(output);
writer.write(token_pair);
writer.flush();
} finally {
Writers.close(writer);
Streams.close(output);
}
}
private void writePageTokens(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession(true);
@SuppressWarnings("unchecked")
Map<String, String> pageTokens = (Map<String, String>) session.getAttribute(CsrfGuard.PAGE_TOKENS_KEY);
String pageTokensString = (pageTokens != null ? parsePageTokens(pageTokens) : Strings.EMPTY);
/** setup headers **/
response.setContentType("text/plain");
response.setContentLength(pageTokensString.length());
/** write dynamic javascript **/
OutputStream output = null;
PrintWriter writer = null;
try {
output = response.getOutputStream();
writer = new PrintWriter(output);
writer.write(pageTokensString);
writer.flush();
} finally {
Writers.close(writer);
Streams.close(output);
}
}
private void writeJavaScript(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession(true);
CsrfGuard csrfGuard = CsrfGuard.getInstance();
/** cannot cache if rotate or token-per-page is enabled **/
if (csrfGuard.isRotateEnabled() || csrfGuard.isTokenPerPageEnabled()) {
response.setHeader("Cache-Control", "no-cache, no-store");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
} else {
response.setHeader("Cache-Control", CsrfGuard.getInstance().getJavascriptCacheControl());
}
response.setContentType("text/javascript");
/** build dynamic javascript **/
String code = CsrfGuard.getInstance().getJavascriptTemplateCode();
code = code.replace(TOKEN_NAME_IDENTIFIER, CsrfGuardUtils.defaultString(csrfGuard.getTokenName()));
code = code.replace(TOKEN_VALUE_IDENTIFIER, CsrfGuardUtils.defaultString((String) session.getAttribute(csrfGuard.getSessionKey())));
code = code.replace(INJECT_INTO_FORMS_IDENTIFIER, Boolean.toString(csrfGuard.isJavascriptInjectIntoForms()));
code = code.replace(INJECT_GET_FORMS_IDENTIFIER, Boolean.toString(csrfGuard.isJavascriptInjectGetForms()));
code = code.replace(INJECT_FORM_ATTRIBUTES_IDENTIFIER, Boolean.toString(csrfGuard.isJavascriptInjectFormAttributes()));
code = code.replace(INJECT_INTO_ATTRIBUTES_IDENTIFIER, Boolean.toString(csrfGuard.isJavascriptInjectIntoAttributes()));
code = code.replace(INJECT_INTO_XHR_IDENTIFIER, String.valueOf(csrfGuard.isAjaxEnabled()));
code = code.replace(TOKENS_PER_PAGE_IDENTIFIER, String.valueOf(csrfGuard.isTokenPerPageEnabled()));
code = code.replace(DOMAIN_ORIGIN_IDENTIFIER, CsrfGuardUtils.defaultString(parseDomain(request.getRequestURL())));
code = code.replace(DOMAIN_STRICT_IDENTIFIER, Boolean.toString(csrfGuard.isJavascriptDomainStrict()));
code = code.replace(CONTEXT_PATH_IDENTIFIER, CsrfGuardUtils.defaultString(request.getContextPath()));
code = code.replace(SERVLET_PATH_IDENTIFIER, CsrfGuardUtils.defaultString(request.getContextPath() + request.getServletPath()));
code = code.replace(X_REQUESTED_WITH_IDENTIFIER, CsrfGuardUtils.defaultString(csrfGuard.getJavascriptXrequestedWith()));
/** write dynamic javascript **/
OutputStream output = null;
PrintWriter writer = null;
try {
output = response.getOutputStream();
writer = new PrintWriter(output);
writer.write(code);
writer.flush();
} finally {
Writers.close(writer);
Streams.close(output);
}
}
private String parsePageTokens(Map<String, String> pageTokens) {
StringBuilder sb = new StringBuilder();
Iterator<String> keys = pageTokens.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
String value = pageTokens.get(key);
sb.append(key);
sb.append(':');
sb.append(value);
if (keys.hasNext()) {
sb.append(',');
}
}
return sb.toString();
}
private String parseDomain(StringBuffer url) {
String token = "://";
int index = url.indexOf(token);
String part = url.substring(index + token.length());
StringBuilder domain = new StringBuilder();
for (int i = 0; i < part.length(); i++) {
char character = part.charAt(i);
if (character == '/' || character == ':') {
break;
}
domain.append(character);
}
return domain.toString();
}
}