This is an automated email from the ASF dual-hosted git repository. jleroux pushed a commit to branch release18.12 in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
commit 0add8bedbca231ffd839eb733f1041ce5487e9d6 Author: Jacques Le Roux <[email protected]> AuthorDate: Sat Apr 4 17:58:07 2020 +0200 Merge branch 'JacquesLeRoux-POC-for-CSRF-Token-OFBIZ-11306' into trunk Because of GitHub message on PR56: This branch cannot be rebased due to conflicts Conflicts handled by hand RequestHandler.java --- .../humanres/template/category/CategoryTree.ftl | 16 +- .../category/ftl/CatalogAltUrlSeoTransform.java | 8 +- .../product/template/category/CategoryTree.ftl | 2 +- .../java/org/apache/ofbiz/common/CommonEvents.java | 3 +- .../common/webcommon/WEB-INF/common-controller.xml | 4 +- framework/security/config/security.properties | 22 +- .../apache/ofbiz/security/CsrfDefenseStrategy.java | 93 +++++ .../java/org/apache/ofbiz/security/CsrfUtil.java | 358 +++++++++++++++++ .../ofbiz/security/ICsrfDefenseStrategy.java | 55 +++ .../ofbiz/security/NoCsrfDefenseStrategy.java | 50 +++ .../org/apache/ofbiz/security/CsrfUtilTests.java | 264 +++++++++++++ .../webapp/config/freemarkerTransforms.properties | 2 + framework/webapp/dtd/site-conf.xsd | 14 + .../ofbiz/webapp/control/ConfigXMLReader.java | 3 + .../ofbiz/webapp/control/ControlEventListener.java | 3 + .../ofbiz/webapp/control/RequestHandler.java | 438 +++++++++++---------- .../ofbiz/webapp/ftl/CsrfTokenAjaxTransform.java | 75 ++++ .../webapp/ftl/CsrfTokenPairNonAjaxTransform.java | 76 ++++ .../webtools/groovyScripts/entity/CheckDb.groovy | 7 +- .../webtools/groovyScripts/entity/EntityRef.groovy | 6 + framework/webtools/template/entity/CheckDb.ftl | 28 +- .../webtools/template/entity/EntityRefList.ftl | 9 +- framework/webtools/template/entity/ViewGeneric.ftl | 1 + .../widget/renderer/macro/MacroFormRenderer.java | 14 +- themes/bluelight/template/Header.ftl | 6 +- .../common-theme/template/includes/ListLocales.ftl | 2 +- .../template/macro/CsvFormMacroLibrary.ftl | 2 +- .../template/macro/FoFormMacroLibrary.ftl | 2 +- .../template/macro/HtmlFormMacroLibrary.ftl | 8 +- .../template/macro/TextFormMacroLibrary.ftl | 2 +- .../template/macro/XlsFormMacroLibrary.ftl | 2 +- .../template/macro/XmlFormMacroLibrary.ftl | 2 +- .../webapp/common/js/util/OfbizUtil.js | 12 +- themes/flatgrey/template/Header.ftl | 6 +- themes/rainbowstone/template/includes/Header.ftl | 4 + .../rainbowstone/template/includes/TopAppBar.ftl | 2 +- themes/tomahawk/template/AppBarClose.ftl | 2 +- themes/tomahawk/template/Header.ftl | 4 + 38 files changed, 1344 insertions(+), 263 deletions(-) diff --git a/applications/humanres/template/category/CategoryTree.ftl b/applications/humanres/template/category/CategoryTree.ftl index 10a08ac..f14bbfc 100644 --- a/applications/humanres/template/category/CategoryTree.ftl +++ b/applications/humanres/template/category/CategoryTree.ftl @@ -61,18 +61,18 @@ var rawdata = [ "plugins" : [ "themes", "json_data","ui" ,"cookies", "types", "crrm", "contextmenu"], "json_data" : { "data" : rawdata, - "ajax" : { "url" : "<@ofbizUrl>getHRChild</@ofbizUrl>", "type" : "POST", - "data" : function (n) { - return { + "ajax" : { "url" : "<@ofbizUrl>getHRChild</@ofbizUrl>", "type" : "POST", + "data" : function (n) { + return { "partyId" : n.attr ? n.attr("id").replace("node_","") : 1 , "additionParam" : "','category" , "hrefString" : "viewprofile?partyId=" , "onclickFunction" : "callDocument" - }; + }; }, - success : function(data) { - return data.hrTree; - } + success : function(data) { + return data.hrTree; + } } }, "types" : { @@ -92,7 +92,7 @@ var rawdata = [ } function callDocument(id,type) { - window.location = "viewprofile?partyId=" + id; + window.location = "viewprofile?partyId=" + id + "&<@csrfTokenPair>viewprofile</@csrfTokenPair>"; } function callEmplDocument(id,type) { diff --git a/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java b/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java index c986f1e..8b56c55 100644 --- a/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java +++ b/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java @@ -25,12 +25,14 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.apache.ofbiz.security.CsrfUtil; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.UtilValidate; import org.apache.ofbiz.base.util.template.FreeMarkerWorker; import org.apache.ofbiz.entity.Delegator; import org.apache.ofbiz.entity.GenericEntityException; import org.apache.ofbiz.entity.GenericValue; +import org.apache.ofbiz.entity.util.EntityQuery; import org.apache.ofbiz.entity.util.EntityUtilProperties; import org.apache.ofbiz.product.category.CatalogUrlFilter; import org.apache.ofbiz.product.category.CategoryContentWrapper; @@ -48,7 +50,6 @@ import freemarker.template.SimpleNumber; import freemarker.template.SimpleScalar; import freemarker.template.TemplateModelException; import freemarker.template.TemplateTransformModel; -import org.apache.ofbiz.entity.util.EntityQuery; public class CatalogAltUrlSeoTransform implements TemplateTransformModel { public final static String module = CatalogUrlSeoTransform.class.getName(); @@ -126,6 +127,11 @@ public class CatalogAltUrlSeoTransform implements TemplateTransformModel { url = CatalogUrlFilter.makeCategoryUrl(request, previousCategoryId, productCategoryId, productId, viewSize, viewIndex, viewSort, searchString); } } + + // add / update csrf token to link when required + String tokenValue = CsrfUtil.generateTokenForNonAjax(request, "product"); + url = CsrfUtil.addOrUpdateTokenInUrl(url, tokenValue); + // make the link if (fullPath) { try { diff --git a/applications/product/template/category/CategoryTree.ftl b/applications/product/template/category/CategoryTree.ftl index dce62c7..dd4ca21 100644 --- a/applications/product/template/category/CategoryTree.ftl +++ b/applications/product/template/category/CategoryTree.ftl @@ -65,7 +65,7 @@ var rawdata = [ "plugins" : [ "themes", "json_data","ui" ,"cookies", "types"], "json_data" : { "data" : rawdata, - "ajax" : { "url" : "<@ofbizUrl>getChild</@ofbizUrl>", + "ajax" : { "url" : "getChild", "type" : "POST", "data" : function (n) { return { diff --git a/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java b/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java index d6b104c..34fbb3b 100644 --- a/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java +++ b/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java @@ -77,7 +77,8 @@ public class CommonEvents { "thisRequestUri", "org.apache.tomcat.util.net.secure_protocol_version", "userLogin", - "impersonateLogin" + "impersonateLogin", + "requestMapMap" // requestMapMap is used by CSRFUtil }; /** Simple event to set the users per-session locale setting. The user's locale diff --git a/framework/common/webcommon/WEB-INF/common-controller.xml b/framework/common/webcommon/WEB-INF/common-controller.xml index 80407c6..b2cd339 100644 --- a/framework/common/webcommon/WEB-INF/common-controller.xml +++ b/framework/common/webcommon/WEB-INF/common-controller.xml @@ -75,7 +75,7 @@ under the License. <response name="error" type="view" value="login"/> </request-map> <request-map uri="logout"> - <security https="true" auth="true"/> + <security https="true" auth="true" csrf-token="false"/> <event type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="logout"/> <response name="success" type="request-redirect" value="main"/> <response name="error" type="view" value="main"/> @@ -317,7 +317,7 @@ under the License. <!-- Set TimeZone from user's browser --> <!-- XXX The auth setting is inconsistent with the one in the service for a good reason, see OFBIZ-10471 for an explanation --> - <request-map uri="SetTimeZoneFromBrowser"> + <request-map uri="SetTimeZoneFromBrowser" method="post"> <security https="false" auth="false"/> <event type="service" invoke="SetTimeZoneFromBrowser"/> <response name="success" type="request" value="json"/> diff --git a/framework/security/config/security.properties b/framework/security/config/security.properties index b9e0b2e..2a639c5 100644 --- a/framework/security/config/security.properties +++ b/framework/security/config/security.properties @@ -1,4 +1,4 @@ -############################################################################### +############################################################################## # 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 @@ -149,9 +149,27 @@ security.jwt.token.expireTime=1800 # -- To make this work you also have to configure a secret key with security.token.key security.internal.sso.enabled=false -# -- The secret key for the JWT token signature. Read Passwords and JWT (JSON Web Tokens) usage documentation to choose the way you want to store this key +# -- The secret key for the JWT token signature. Read Passwords and JWT (JSON Web Tokens) usage documentation to choose the way you want to store this key security.token.key=security.token.key # -- By default the SameSite value in SameSiteFilter is strict. This allows to change it ot lax if needed SameSiteCookieAttribute= +# -- The cache size for the Tokens Maps that stores the CSRF tokens. +# -- RemoveEldestEntry is used when it's get above csrf.cache.size +# -- Default is 5000 +# -- TODO: separate tokenMap from partyTokenMap +csrf.cache.size= + +# -- Parameter name for CSRF token. Default is "csrf" if not specified +csrf.tokenName.nonAjax= + +# -- The csrf.entity.request.limit is used to show how to avoid cluttering the Tokens Maps cache with URIs starting with "entity/" +# -- It can be useful with large Database contents, ie with a large numbers of tuples, like "entity/edit/Agreement/10000, etc. +# -- The same principle can be extended to other cases similar to "entity/" URIs (harcoded or using similar properties). +# -- Default is 3 +csrf.entity.request.limit= + +# csrf defense strategy. Default is org.apache.ofbiz.security.CsrfDefenseStrategy if not specified. +# use org.apache.ofbiz.security.NoCsrfDefenseStrategy to disable CSRF check totally. +csrf.defense.strategy= \ No newline at end of file diff --git a/framework/security/src/main/java/org/apache/ofbiz/security/CsrfDefenseStrategy.java b/framework/security/src/main/java/org/apache/ofbiz/security/CsrfDefenseStrategy.java new file mode 100644 index 0000000..5b72990 --- /dev/null +++ b/framework/security/src/main/java/org/apache/ofbiz/security/CsrfDefenseStrategy.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * 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.ofbiz.security; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.ofbiz.base.util.Debug; +import org.apache.ofbiz.base.util.UtilProperties; +import org.apache.ofbiz.webapp.control.RequestHandlerExceptionAllowExternalRequests; + +public class CsrfDefenseStrategy implements ICsrfDefenseStrategy { + + public static final String module = CsrfDefenseStrategy.class.getName(); + private static SecureRandom secureRandom = null; + private static final String prng = "SHA1PRNG"; + private static final String CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static int csrfEntityErequestLimit = (int) Long.parseLong(UtilProperties.getPropertyValue("security", "csrf.entity.request.limit", "3")); + + static{ + try { + secureRandom = SecureRandom.getInstance(prng); + } catch (NoSuchAlgorithmException e) { + Debug.logError(e, module); + } + } + + @Override + public String generateToken() { + StringBuilder sb = new StringBuilder(); + for (int i = 1; i < 12 + 1; i++) { + int index = secureRandom.nextInt(CHARSET.length()); + char c = CHARSET.charAt(index); + sb.append(c); + } + return sb.toString(); + } + + @Override + public int maxSubFolderInRequestUrlForTokenMapLookup(String requestUri){ + if (requestUri.startsWith("entity/")){ + return csrfEntityErequestLimit; + } + return 0; + } + + @Override + public boolean modifySecurityCsrfToken(String requestUri, String requestMapMethod, String securityCsrfToken) { + // main request URI is exempted from CSRF token check + if (requestUri.equals("main")) { + return false; + } else { + return !"false".equals(securityCsrfToken); + } + } + + + @Override + public boolean keepTokenAfterUse(String requestUri, String requestMethod) { + // to allow back and forth browser buttons to work, + // token value is unchanged when request.getMethod is GET + if ("GET".equals(requestMethod)) { + return true; + } + return false; + } + + @Override + public void invalidTokenResponse(String requestUri, HttpServletRequest request) throws RequestHandlerExceptionAllowExternalRequests { + request.setAttribute("_ERROR_MESSAGE_", + "Invalid or missing CSRF token to path '" + request.getPathInfo() + "'. Click <a href='" + + request.getContextPath() + "'>here</a> to continue."); + throw new RequestHandlerExceptionAllowExternalRequests(); + } +} diff --git a/framework/security/src/main/java/org/apache/ofbiz/security/CsrfUtil.java b/framework/security/src/main/java/org/apache/ofbiz/security/CsrfUtil.java new file mode 100644 index 0000000..9d400b8 --- /dev/null +++ b/framework/security/src/main/java/org/apache/ofbiz/security/CsrfUtil.java @@ -0,0 +1,358 @@ +/******************************************************************************* + * 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.ofbiz.security; + +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.ws.rs.core.MultivaluedHashMap; + +import org.apache.commons.lang.StringUtils; +import org.apache.cxf.jaxrs.model.URITemplate; +import org.apache.ofbiz.base.component.ComponentConfig; +import org.apache.ofbiz.base.util.Debug; +import org.apache.ofbiz.base.util.UtilGenerics; +import org.apache.ofbiz.base.util.UtilProperties; +import org.apache.ofbiz.base.util.UtilValidate; +import org.apache.ofbiz.entity.GenericValue; +import org.apache.ofbiz.webapp.control.ConfigXMLReader; +import org.apache.ofbiz.webapp.control.RequestHandler; +import org.apache.ofbiz.webapp.control.RequestHandlerException; +import org.apache.ofbiz.webapp.control.RequestHandlerExceptionAllowExternalRequests; +import org.apache.ofbiz.webapp.control.WebAppConfigurationException; + +public class CsrfUtil { + + public static final String module = CsrfUtil.class.getName(); + public static String tokenNameNonAjax = UtilProperties.getPropertyValue("security", "csrf.tokenName.nonAjax", "csrf"); + public static ICsrfDefenseStrategy strategy; + private static int cacheSize = (int) Long.parseLong(UtilProperties.getPropertyValue("security", "csrf.cache.size", "5000")); + private static LinkedHashMap<String, Map<String, Map<String, String>>> csrfTokenCache = new LinkedHashMap<String, Map<String, Map<String, String>>>() { + private static final long serialVersionUID = 1L; + protected boolean removeEldestEntry(Map.Entry<String, Map<String, Map<String, String>>> eldest) { + return size() > cacheSize; // TODO use also csrf.cache.size here? + } + }; + + private CsrfUtil() { + } + + static { + try { + String className = UtilProperties.getPropertyValue("security", "csrf.defense.strategy", CsrfDefenseStrategy.class.getCanonicalName()); + Class<?> c = Class.forName(className); + strategy = (ICsrfDefenseStrategy)c.newInstance(); + } catch (Exception e){ + Debug.logError(e, module); + strategy = new CsrfDefenseStrategy(); + } + } + + public static Map<String, String> getTokenMap(HttpServletRequest request, String targetContextPath) { + + HttpSession session = request.getSession(); + GenericValue userLogin = (GenericValue) session.getAttribute("userLogin"); + String partyId = null; + if (userLogin != null && userLogin.get("partyId") != null) { + partyId = userLogin.getString("partyId"); + } + + Map<String, String> tokenMap = null; + if (UtilValidate.isNotEmpty(partyId)) { + Map<String, Map<String, String>> partyTokenMap = csrfTokenCache.get(partyId); + if (partyTokenMap == null) { + partyTokenMap = new HashMap<String, Map<String, String>>(); + csrfTokenCache.put(partyId, partyTokenMap); + } + + tokenMap = partyTokenMap.get(targetContextPath); + if (tokenMap == null) { + tokenMap = new LinkedHashMap<String, String>() { + private static final long serialVersionUID = 1L; + protected boolean removeEldestEntry(Map.Entry<String, String> eldest) { + return size() > cacheSize; + } + }; + partyTokenMap.put(targetContextPath, tokenMap); + } + } else { + tokenMap = UtilGenerics.cast(session.getAttribute("CSRF-Token")); + if (tokenMap == null) { + tokenMap = new LinkedHashMap<String, String>() { + private static final long serialVersionUID = 1L; + protected boolean removeEldestEntry(Map.Entry<String, String> eldest) { + return size() > cacheSize; + } + }; + session.setAttribute("CSRF-Token", tokenMap); + } + } + return tokenMap; + } + + private static String generateToken() { + return strategy.generateToken(); + } + + /** + * Reduce number of subfolder from request uri, if needed, before using it to generate CSRF token. + * @param requestUri + * @return + */ + static String getRequestUriWithSubFolderLimit(String requestUri){ + int limit = CsrfUtil.strategy.maxSubFolderInRequestUrlForTokenMapLookup(requestUri); + if (limit<1){ + return requestUri; + } + while(StringUtils.countMatches(requestUri, "/")+1>limit){ + requestUri = requestUri.substring(0, requestUri.lastIndexOf("/")); + } + return requestUri; + } + + static String getRequestUriFromPath(String pathOrRequestUri){ + String requestUri = pathOrRequestUri; + // remove any query string + if (requestUri.contains("?")) { + // e.g. "/viewprofile?partyId=Company" to "/viewprofile" + requestUri = requestUri.substring(0, requestUri.indexOf("?")); + } + String controlServletPart = "/control/"; // TODO remove with OFBIZ-11229 + if (requestUri.contains(controlServletPart)) { + // e.g. "/partymgr/control/viewprofile" to "viewprofile" + requestUri = requestUri.substring(requestUri.indexOf(controlServletPart) + controlServletPart.length()); + } + if (requestUri.startsWith("/")) { + // e.g. "/viewprofile" to "viewprofile" + requestUri = requestUri.substring(1); + } + if (requestUri.contains("#")){ + // e.g. "view/entityref_main#org.apache.ofbiz.accounting.budget" to "view/entityref_main" + requestUri = requestUri.substring(0, requestUri.indexOf("#")); + } + return requestUri; + } + + /** + * Generate CSRF token for non-ajax request if required and add it as key to token map in session When token map + * size limit is reached, the eldest entry will be deleted each time a new entry is added. + * Token only generated for up to 3 subfolders in the path so 'entity/find/Budget/0001' & 'entity/find/Budget/0002' + * should share the same CSRF token. + * + * @param request + * @param pathOrRequestUri + * @return csrf token + */ + public static String generateTokenForNonAjax(HttpServletRequest request, String pathOrRequestUri) { + if (UtilValidate.isEmpty(pathOrRequestUri) + || pathOrRequestUri.startsWith("javascript") + || pathOrRequestUri.startsWith("#") ) { + return ""; + } + + if (pathOrRequestUri.contains("/")) { + pathOrRequestUri = pathOrRequestUri.replaceAll("/", "/"); + } + + String requestUri = getRequestUriWithSubFolderLimit(getRequestUriFromPath(pathOrRequestUri)); + + Map<String, String> tokenMap = null; + + ConfigXMLReader.RequestMap requestMap = null; + // TODO when OFBIZ-11354 will be done this will need to be removed even if it should be OK as is + if (pathOrRequestUri.contains("/control/")) { + tokenMap = getTokenMap(request, "/" + RequestHandler.getRequestUri(pathOrRequestUri)); + requestMap = findRequestMap(pathOrRequestUri); + } else { + tokenMap = getTokenMap(request, request.getContextPath()); + Map<String, ConfigXMLReader.RequestMap> requestMapMap = UtilGenerics + .cast(request.getAttribute("requestMapMap")); + requestMap = findRequestMap(requestMapMap, pathOrRequestUri); + } + if (requestMap == null) { + Debug.logError("Cannot find the corresponding request map for path: " + pathOrRequestUri, module); + } + String tokenValue = ""; + if (requestMap != null && requestMap.securityCsrfToken) { + if (tokenMap.containsKey(requestUri)) { + tokenValue = tokenMap.get(requestUri); + } else { + tokenValue = generateToken(); + tokenMap.put(requestUri, tokenValue); + } + } + return tokenValue; + } + + static ConfigXMLReader.RequestMap findRequestMap(String _urlWithControlPath){ + + String requestUri = getRequestUriFromPath(_urlWithControlPath); + + List<ComponentConfig.WebappInfo> webappInfos = ComponentConfig.getAllWebappResourceInfos().stream() + .filter(line -> line.contextRoot.contains(RequestHandler.getRequestUri(_urlWithControlPath))) + .collect(Collectors.toList()); + + ConfigXMLReader.RequestMap requestMap = null; + if (UtilValidate.isNotEmpty(webappInfos)) { + try { + if (StringUtils.countMatches(requestUri, "/")==1){ + requestMap = ConfigXMLReader.getControllerConfig(webappInfos.get(0)).getRequestMapMap() + .get(requestUri.substring(0, requestUri.indexOf("/"))); + } else { + requestMap = ConfigXMLReader.getControllerConfig(webappInfos.get(0)).getRequestMapMap() + .get(requestUri); + } + } catch (WebAppConfigurationException | MalformedURLException e) { + Debug.logError(e, module); + } + } + return requestMap; + } + + static ConfigXMLReader.RequestMap findRequestMap(Map<String, ConfigXMLReader.RequestMap> requestMapMap, + String _urlWithoutControlPath) { + String path = _urlWithoutControlPath; + if (_urlWithoutControlPath.startsWith("/")) { + path = _urlWithoutControlPath.substring(1); + } + int charPos = path.indexOf("?"); + if (charPos != -1) { + path = path.substring(0, charPos); + } + MultivaluedHashMap<String, String> vars = new MultivaluedHashMap<>(); + for (Map.Entry<String, ConfigXMLReader.RequestMap> entry : requestMapMap.entrySet()) { + URITemplate uriTemplate = URITemplate.createExactTemplate(entry.getKey()); + // Check if current path the URI template exactly. + if (uriTemplate.match(path, vars) && vars.getFirst(URITemplate.FINAL_MATCH_GROUP).equals("/")) { + return entry.getValue(); + } + } + // the path could be request uri with orderride + if (path.contains("/")) { + return requestMapMap.get(path.substring(0, path.indexOf("/"))); + } + return null; + } + + /** + * generate csrf token for AJAX and add it as value to token cache + * + * @param request + * @return csrf token + */ + public static String generateTokenForAjax(HttpServletRequest request) { + HttpSession session = request.getSession(); + String tokenValue = (String) session.getAttribute("X-CSRF-Token"); + if (tokenValue == null) { + tokenValue = generateToken(); + session.setAttribute("X-CSRF-Token", tokenValue); + } + return tokenValue; + } + + /** + * get csrf token for AJAX + * + * @param session + * @return csrf token + */ + public static String getTokenForAjax(HttpSession session) { + return (String) session.getAttribute("X-CSRF-Token"); + } + + public static String addOrUpdateTokenInUrl(String link, String csrfToken) { + if (link.contains(CsrfUtil.tokenNameNonAjax)) { + return link.replaceFirst("\\b"+CsrfUtil.tokenNameNonAjax+"=.*?(&|$)", CsrfUtil.tokenNameNonAjax+"=" + csrfToken + "$1"); + } else if (!"".equals(csrfToken)) { + if (link.contains("?")) { + return link + "&"+CsrfUtil.tokenNameNonAjax+"=" + csrfToken; + } else { + return link + "?"+CsrfUtil.tokenNameNonAjax+"=" + csrfToken; + } + } + return link; + } + + public static String addOrUpdateTokenInQueryString(String link, String csrfToken) { + if (UtilValidate.isNotEmpty(link)) { + if (link.contains(CsrfUtil.tokenNameNonAjax)) { + return link.replaceFirst("\\b"+CsrfUtil.tokenNameNonAjax+"=.*?(&|$)", CsrfUtil.tokenNameNonAjax+"=" + csrfToken + "$1"); + } else { + if (UtilValidate.isNotEmpty(csrfToken)) { + return link + "&"+CsrfUtil.tokenNameNonAjax+"=" + csrfToken; + } else { + return link; + } + } + } else { + return CsrfUtil.tokenNameNonAjax+"=" + csrfToken; + } + } + + public static void checkToken(HttpServletRequest request, String _path) + throws RequestHandlerException, RequestHandlerExceptionAllowExternalRequests { + String path = _path; + if (_path.startsWith("/")) { + path = _path.substring(1); + } + if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With")) && !"GET".equals(request.getMethod())) { + String csrfToken = request.getHeader("X-CSRF-Token"); + HttpSession session = request.getSession(); + if ((UtilValidate.isEmpty(csrfToken) || !csrfToken.equals(CsrfUtil.getTokenForAjax(session))) + && !"/SetTimeZoneFromBrowser".equals(request.getPathInfo())) { // TODO maybe this can be improved... + throw new RequestHandlerException( + "Invalid or missing CSRF token for AJAX call to path '" + request.getPathInfo() + "'"); + } + } else { + Map<String, String> tokenMap = CsrfUtil.getTokenMap(request, request.getContextPath()); + String csrfToken = request.getParameter(CsrfUtil.tokenNameNonAjax); + String limitPath = getRequestUriWithSubFolderLimit(path); + if (UtilValidate.isNotEmpty(csrfToken) && tokenMap.containsKey(limitPath) + && csrfToken.equals(tokenMap.get(limitPath))) { + if (!CsrfUtil.strategy.keepTokenAfterUse(path,request.getMethod())) { + tokenMap.remove(limitPath); + } + } else { + CsrfUtil.strategy.invalidTokenResponse(path, request); + } + } + } + + public static void cleanupTokenMap(HttpSession session) { + GenericValue userLogin = (GenericValue) session.getAttribute("userLogin"); + String partyId = null; + if (userLogin != null && userLogin.get("partyId") != null) { + partyId = userLogin.getString("partyId"); + Map<String, Map<String, String>> partyTokenMap = csrfTokenCache.get(partyId); + if (partyTokenMap != null) { + String contextPath = session.getServletContext().getContextPath(); + partyTokenMap.remove(contextPath); + if (partyTokenMap.isEmpty()) { + csrfTokenCache.remove(partyId); + } + } + } + } +} diff --git a/framework/security/src/main/java/org/apache/ofbiz/security/ICsrfDefenseStrategy.java b/framework/security/src/main/java/org/apache/ofbiz/security/ICsrfDefenseStrategy.java new file mode 100644 index 0000000..322afb5 --- /dev/null +++ b/framework/security/src/main/java/org/apache/ofbiz/security/ICsrfDefenseStrategy.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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.ofbiz.security; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.ofbiz.webapp.control.RequestHandlerExceptionAllowExternalRequests; + +public interface ICsrfDefenseStrategy { + + String generateToken(); + + /** + * Limit the number of subfolders in request uri to reduce the number of CSRF tokens needed. + * @param requestUri + * @return + */ + int maxSubFolderInRequestUrlForTokenMapLookup(String requestUri); + + /** + * Override security csrf-token value in request map + * @param requestUri + * @param requestMapMethod get, post or all + * @param securityCsrfToken + * @return + */ + boolean modifySecurityCsrfToken(String requestUri, String requestMapMethod, String securityCsrfToken); + + /** + * Whether to reuse the token after it is consumed + * @param requestUri + * @param requestMethod GET, POST, or PUT + * @return + */ + boolean keepTokenAfterUse(String requestUri, String requestMethod); + + void invalidTokenResponse(String requestUri, HttpServletRequest request) throws RequestHandlerExceptionAllowExternalRequests; + +} \ No newline at end of file diff --git a/framework/security/src/main/java/org/apache/ofbiz/security/NoCsrfDefenseStrategy.java b/framework/security/src/main/java/org/apache/ofbiz/security/NoCsrfDefenseStrategy.java new file mode 100644 index 0000000..279310c --- /dev/null +++ b/framework/security/src/main/java/org/apache/ofbiz/security/NoCsrfDefenseStrategy.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * 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.ofbiz.security; + +import javax.servlet.http.HttpServletRequest; + +public class NoCsrfDefenseStrategy implements ICsrfDefenseStrategy { + + @Override + public String generateToken() { + return null; + } + + @Override + public int maxSubFolderInRequestUrlForTokenMapLookup(String requestUri){ + return 0; + } + + @Override + public boolean modifySecurityCsrfToken(String requestUri, String requestMapMethod, String securityCsrfToken) { + // all SecurityCsrfToken checks in request maps are read as false + return false; + } + + @Override + public boolean keepTokenAfterUse(String requestUri, String requestMethod) { + return false; + } + + @Override + public void invalidTokenResponse(String requestUri, HttpServletRequest request) { + + } +} \ No newline at end of file diff --git a/framework/security/src/test/java/org/apache/ofbiz/security/CsrfUtilTests.java b/framework/security/src/test/java/org/apache/ofbiz/security/CsrfUtilTests.java new file mode 100644 index 0000000..53d0096 --- /dev/null +++ b/framework/security/src/test/java/org/apache/ofbiz/security/CsrfUtilTests.java @@ -0,0 +1,264 @@ +/* + * 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.ofbiz.security; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.ofbiz.entity.GenericValue; +import org.apache.ofbiz.webapp.control.ConfigXMLReader; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public class CsrfUtilTests { + + @Test + public void testGetTokenMap(){ + HttpServletRequest request = mock(HttpServletRequest.class); + HttpSession session = mock(HttpSession.class); + when(request.getSession()).thenReturn(session); + + // prepare the token map to be retrieved from session + Map<String,String> tokenMap = new LinkedHashMap<String, String>(); + tokenMap.put("uri_1","abcd"); + when(session.getAttribute("CSRF-Token")).thenReturn(tokenMap); + + // without userLogin in session, test token map is retrieved from session + Map<String, String> resultMap = CsrfUtil.getTokenMap(request, ""); + assertEquals("abcd", resultMap.get("uri_1")); + + // add userLogin to session + GenericValue userLogin = mock(GenericValue.class); + when(userLogin.get("partyId")).thenReturn("10000"); + when(userLogin.getString("partyId")).thenReturn("10000"); + when(session.getAttribute("userLogin")).thenReturn(userLogin); + + // with userLogin in session, test token map is not retrieved from session + resultMap = CsrfUtil.getTokenMap(request, "/partymgr"); + assertNull(resultMap.get("uri_1")); + + } + + @Test + public void testGetRequestUriWithSubFolderLimit(){ + CsrfUtil.strategy = new CsrfDefenseStrategy(); + + // limit only when request uri starts with 'entity' + String limitRequestUri = CsrfUtil.getRequestUriWithSubFolderLimit("entity/find/Budget/0002"); + assertEquals("entity/find/Budget", limitRequestUri); + + limitRequestUri = CsrfUtil.getRequestUriWithSubFolderLimit("a/b/c/d"); + assertEquals("a/b/c/d", limitRequestUri); + } + + @Test + public void testGetRequestUriFromPath(){ + String requestUri = CsrfUtil.getRequestUriFromPath("/viewprofile?partyId=Company"); + assertEquals("viewprofile", requestUri); + + requestUri = CsrfUtil.getRequestUriFromPath("/partymgr/control/viewprofile"); + assertEquals("viewprofile", requestUri); + + requestUri = CsrfUtil.getRequestUriFromPath("view/entityref_main#org.apache.ofbiz.accounting.budget"); + assertEquals("view/entityref_main", requestUri); + } + + + @Test + public void testGenerateTokenForNonAjax() throws ParserConfigurationException { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpSession session = mock(HttpSession.class); + when(request.getSession()).thenReturn(session); + + // add userLogin to session + GenericValue userLogin = mock(GenericValue.class); + when(userLogin.get("partyId")).thenReturn("10000"); + when(userLogin.getString("partyId")).thenReturn("10000"); + when(session.getAttribute("userLogin")).thenReturn(userLogin); + + String token = CsrfUtil.generateTokenForNonAjax(request, ""); + assertEquals("", token); + + token = CsrfUtil.generateTokenForNonAjax(request, "javascript:"); + assertEquals("", token); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = dbf.newDocumentBuilder(); + Document doc = builder.newDocument(); + + Map<String, ConfigXMLReader.RequestMap> requestMapMap = new HashMap<>(); + { + Element requestMapElement = doc.createElement("request-map"); + requestMapElement.setAttribute("uri", "checkLogin"); + ConfigXMLReader.RequestMap requestMap = new ConfigXMLReader.RequestMap(requestMapElement); + requestMapMap.put(requestMap.uri, requestMap); + } + { + Element requestMapElement = doc.createElement("request-map"); + requestMapElement.setAttribute("uri", "entity/find/{entityName}/{pkValues: .*}"); + ConfigXMLReader.RequestMap requestMap = new ConfigXMLReader.RequestMap(requestMapElement); + requestMapMap.put(requestMap.uri, requestMap); + } + when(request.getAttribute("requestMapMap")).thenReturn(requestMapMap); + + token = CsrfUtil.generateTokenForNonAjax(request, "checkLogin"); + assertNotEquals("", token); + + CsrfUtil.strategy = new CsrfDefenseStrategy(); + + token = CsrfUtil.generateTokenForNonAjax(request, "entity/find/Budget/0001"); + assertNotEquals("", token); + + String token2 = CsrfUtil.generateTokenForNonAjax(request, "entity/find/Budget/0001"); + // test support for treating "/" as "/" + assertEquals(token2, token); + + token2 = CsrfUtil.generateTokenForNonAjax(request, "entity/find/Budget/0002"); + // token only generated for up to 3 subfolders in the path + assertEquals(token2, token); + } + + @Test + public void testFindRequestMapWithoutControlPath() throws ParserConfigurationException { + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = dbf.newDocumentBuilder(); + Document doc = builder.newDocument(); + + Map<String, ConfigXMLReader.RequestMap> requestMapMap = new HashMap<>(); + { + Element requestMapElement = doc.createElement("request-map"); + requestMapElement.setAttribute("uri", "checkLogin"); + ConfigXMLReader.RequestMap requestMap = new ConfigXMLReader.RequestMap(requestMapElement); + requestMapMap.put(requestMap.uri, requestMap); + } + // REST request like /entity/find/AccommodationClass + { + Element requestMapElement = doc.createElement("request-map"); + requestMapElement.setAttribute("uri", "entity/find/{entityName}"); + ConfigXMLReader.RequestMap requestMap = new ConfigXMLReader.RequestMap(requestMapElement); + requestMapMap.put(requestMap.uri, requestMap); + } + // View override like /view/ModelInduceFromDb + { + Element requestMapElement = doc.createElement("request-map"); + requestMapElement.setAttribute("uri", "view"); + ConfigXMLReader.RequestMap requestMap = new ConfigXMLReader.RequestMap(requestMapElement); + requestMapMap.put(requestMap.uri, requestMap); + } + { + Element requestMapElement = doc.createElement("request-map"); + requestMapElement.setAttribute("uri", "ModelInduceFromDb"); + ConfigXMLReader.RequestMap requestMap = new ConfigXMLReader.RequestMap(requestMapElement); + requestMapMap.put(requestMap.uri, requestMap); + } + + // test usual request + ConfigXMLReader.RequestMap requestMap = CsrfUtil.findRequestMap(requestMapMap, "/checkLogin"); + assertEquals(requestMap.uri, "checkLogin"); + + // test usual request + requestMap = CsrfUtil.findRequestMap(requestMapMap, "checkLogin"); + assertEquals(requestMap.uri, "checkLogin"); + + // test REST request + requestMap = CsrfUtil.findRequestMap(requestMapMap, "/entity/find/AccommodationClass"); + assertEquals(requestMap.uri, "entity/find/{entityName}"); + + // test view orderride + requestMap = CsrfUtil.findRequestMap(requestMapMap, "/view/ModelInduceFromDb"); + assertEquals(requestMap.uri, "view"); + + } + + @Test + public void testGenerateTokenForAjax() { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpSession session = mock(HttpSession.class); + when(request.getSession()).thenReturn(session); + when(session.getAttribute("X-CSRF-Token")).thenReturn("abcd"); + + String token = CsrfUtil.generateTokenForAjax(request); + assertEquals("abcd", token); + } + + @Test + public void testGetTokenForAjax(){ + HttpSession session = mock(HttpSession.class); + when(session.getAttribute("X-CSRF-Token")).thenReturn("abcd"); + + String token = CsrfUtil.getTokenForAjax(session); + assertEquals("abcd", token); + } + + @Test + public void testAddOrUpdateTokenInUrl(){ + CsrfUtil.tokenNameNonAjax = "csrfToken"; + + // test link without csrfToken + String url = CsrfUtil.addOrUpdateTokenInUrl("https://localhost:8443/catalog/control/login", "abcd"); + assertEquals("https://localhost:8443/catalog/control/login?csrfToken=abcd", url); + + // test link with query string and without csrfToken + url = CsrfUtil.addOrUpdateTokenInUrl("https://localhost:8443/partymgr/control/EditCommunicationEvent?communicationEventId=10000", "abcd"); + assertEquals("https://localhost:8443/partymgr/control/EditCommunicationEvent?communicationEventId=10000&csrfToken=abcd", url); + + // test link with csrfToken + url = CsrfUtil.addOrUpdateTokenInUrl("https://localhost:8443/catalog/control/login?csrfToken=abcd", "efgh"); + assertEquals("https://localhost:8443/catalog/control/login?csrfToken=efgh", url); + + // test link with csrfToken amd empty csrfToken replacement + url = CsrfUtil.addOrUpdateTokenInUrl("https://localhost:8443/catalog/control/login?csrfToken=abcd", ""); + assertEquals("https://localhost:8443/catalog/control/login?csrfToken=", url); + } + + @Test + public void testAddOrUpdateTokenInQueryString(){ + CsrfUtil.tokenNameNonAjax = "csrfToken"; + + String queryString = CsrfUtil.addOrUpdateTokenInQueryString("", "abcd"); + assertEquals(queryString, "csrfToken=abcd"); + + queryString = CsrfUtil.addOrUpdateTokenInQueryString("csrfToken=abcd&a=b", "efgh"); + assertEquals(queryString, "csrfToken=efgh&a=b"); + + queryString = CsrfUtil.addOrUpdateTokenInQueryString("csrfToken=abcd&a=b", ""); + assertEquals(queryString, "csrfToken=&a=b"); + + queryString = CsrfUtil.addOrUpdateTokenInQueryString("a=b", "abcd"); + assertEquals(queryString, "a=b&csrfToken=abcd"); + + queryString = CsrfUtil.addOrUpdateTokenInQueryString("a=b", ""); + assertEquals(queryString, "a=b"); + } +} diff --git a/framework/webapp/config/freemarkerTransforms.properties b/framework/webapp/config/freemarkerTransforms.properties index 535e48d..f6a2f6b 100644 --- a/framework/webapp/config/freemarkerTransforms.properties +++ b/framework/webapp/config/freemarkerTransforms.properties @@ -28,3 +28,5 @@ ofbizAmount=org.apache.ofbiz.webapp.ftl.OfbizAmountTransform setRequestAttribute=org.apache.ofbiz.webapp.ftl.SetRequestAttributeMethod renderWrappedText=org.apache.ofbiz.webapp.ftl.RenderWrappedTextTransform setContextField=org.apache.ofbiz.webapp.ftl.SetContextFieldTransform +csrfTokenAjax=org.apache.ofbiz.webapp.ftl.CsrfTokenAjaxTransform +csrfTokenPair=org.apache.ofbiz.webapp.ftl.CsrfTokenPairNonAjaxTransform diff --git a/framework/webapp/dtd/site-conf.xsd b/framework/webapp/dtd/site-conf.xsd index fc9a966..01d0046 100644 --- a/framework/webapp/dtd/site-conf.xsd +++ b/framework/webapp/dtd/site-conf.xsd @@ -305,6 +305,20 @@ under the License. </xs:documentation> </xs:annotation> </xs:attribute> + <xs:attribute name="csrf-token" use="optional" default=""> + <xs:annotation> + <xs:documentation> + If true csrf token is expected. If false no csrf token check. Default to "". + </xs:documentation> + </xs:annotation> + <xs:simpleType> + <xs:restriction base="xs:token"> + <xs:enumeration value=""/> + <xs:enumeration value="true"/> + <xs:enumeration value="false"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> </xs:attributeGroup> <xs:element name="metric"> <xs:annotation> diff --git a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java index 8181eb8..e350b95 100644 --- a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java +++ b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java @@ -51,6 +51,7 @@ import org.apache.ofbiz.base.util.cache.UtilCache; import org.apache.ofbiz.base.util.collections.MapContext; import org.apache.ofbiz.base.util.collections.MultivaluedMapContext; import org.apache.ofbiz.base.util.collections.MultivaluedMapContextAdapter; +import org.apache.ofbiz.security.CsrfUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -465,6 +466,7 @@ public class ConfigXMLReader { public Event event; public boolean securityHttps = true; public boolean securityAuth = false; + public boolean securityCsrfToken = true; public boolean securityCert = false; public boolean securityExternalView = true; public boolean securityDirectRequest = true; @@ -496,6 +498,7 @@ public class ConfigXMLReader { this.securityCert = "true".equals(securityElement.getAttribute("cert")); this.securityExternalView = !"false".equals(securityElement.getAttribute("external-view")); this.securityDirectRequest = !"false".equals(securityElement.getAttribute("direct-request")); + this.securityCsrfToken = CsrfUtil.strategy.modifySecurityCsrfToken(this.uri, this.method, securityElement.getAttribute("csrf-token")); } // Check for event Element eventElement = UtilXml.firstChildElement(requestMapElement, "event"); diff --git a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlEventListener.java b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlEventListener.java index 353b56b..2bb0aab 100644 --- a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlEventListener.java +++ b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlEventListener.java @@ -26,6 +26,7 @@ import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; +import org.apache.ofbiz.security.CsrfUtil; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.UtilDateTime; import org.apache.ofbiz.base.util.UtilGenerics; @@ -69,6 +70,8 @@ public class ControlEventListener implements HttpSessionListener { public void sessionDestroyed(HttpSessionEvent event) { HttpSession session = event.getSession(); + CsrfUtil.cleanupTokenMap(session); + // Finalize the Visit boolean beganTransaction = false; try { diff --git a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java index e1d1745..b18fa8d 100644 --- a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java +++ b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java @@ -18,12 +18,11 @@ *******************************************************************************/ package org.apache.ofbiz.webapp.control; -import java.net.MalformedURLException; -import org.apache.ofbiz.base.location.FlexibleLocation; import static org.apache.ofbiz.base.util.UtilGenerics.checkMap; import java.io.IOException; import java.io.Serializable; +import java.net.MalformedURLException; import java.net.URL; import java.security.cert.X509Certificate; import java.util.Collection; @@ -33,13 +32,20 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import javax.ws.rs.core.MultivaluedHashMap; +import org.apache.cxf.jaxrs.model.URITemplate; +import org.apache.ofbiz.base.location.FlexibleLocation; +import org.apache.ofbiz.security.CsrfUtil; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.SSLUtil; import org.apache.ofbiz.base.util.StringUtil; @@ -50,13 +56,13 @@ import org.apache.ofbiz.base.util.UtilMisc; import org.apache.ofbiz.base.util.UtilObject; import org.apache.ofbiz.base.util.UtilProperties; import org.apache.ofbiz.base.util.UtilValidate; -import org.apache.ofbiz.base.util.collections.MultivaluedMapContext; import org.apache.ofbiz.entity.Delegator; import org.apache.ofbiz.entity.GenericEntityException; import org.apache.ofbiz.entity.GenericValue; import org.apache.ofbiz.entity.util.EntityQuery; import org.apache.ofbiz.entity.util.EntityUtilProperties; import org.apache.ofbiz.webapp.OfbizUrlBuilder; +import org.apache.ofbiz.webapp.control.ConfigXMLReader.ControllerConfig; import org.apache.ofbiz.webapp.control.ConfigXMLReader.RequestMap; import org.apache.ofbiz.webapp.event.EventFactory; import org.apache.ofbiz.webapp.event.EventHandler; @@ -75,8 +81,6 @@ import org.apache.ofbiz.widget.model.ThemeFactory; public class RequestHandler { public static final String module = RequestHandler.class.getName(); - private final static String defaultStatusCodeString = - UtilProperties.getPropertyValue("requestHandler", "status-code", "302"); private final ViewFactory viewFactory; private final EventFactory eventFactory; private final URL controllerConfigURL; @@ -84,66 +88,6 @@ public class RequestHandler { private final boolean trackVisit; private ControllerConfig ccfg; - static class ControllerConfig { - private final MultivaluedMapContext<String, RequestMap> requestMapMap; - private final Map<String, ConfigXMLReader.ViewMap> viewMapMap; - private String statusCodeString; - private final String defaultRequest; - private final Map<String, ConfigXMLReader.Event> firstVisitEventList; - private final Map<String, ConfigXMLReader.Event> preprocessorEventList; - private final Map<String, ConfigXMLReader.Event> postprocessorEventList; - private final String protectView; - - ControllerConfig(ConfigXMLReader.ControllerConfig ccfg) throws WebAppConfigurationException { - preprocessorEventList = ccfg.getPreprocessorEventList(); - postprocessorEventList = ccfg.getPostprocessorEventList(); - requestMapMap = ccfg.getRequestMapMultiMap(); - viewMapMap = ccfg.getViewMapMap(); - defaultRequest = ccfg.getDefaultRequest(); - firstVisitEventList = ccfg.getFirstVisitEventList(); - protectView = ccfg.getProtectView(); - - String status = ccfg.getStatusCode(); - statusCodeString = UtilValidate.isEmpty(status) ? defaultStatusCodeString : status; - } - - public MultivaluedMapContext<String, RequestMap> getRequestMapMap() { - return requestMapMap; - } - - public Map<String, ConfigXMLReader.ViewMap> getViewMapMap() { - return viewMapMap; - } - - public String getStatusCodeString() { - return statusCodeString; - } - - public String getDefaultRequest() { - return defaultRequest; - } - - public void setStatusCodeString(String statusCodeString) { - this.statusCodeString = statusCodeString; - } - - public Map<String, ConfigXMLReader.Event> getFirstVisitEventList() { - return firstVisitEventList; - } - - public Map<String, ConfigXMLReader.Event> getPreprocessorEventList() { - return preprocessorEventList; - } - - public Map<String, ConfigXMLReader.Event> getPostprocessorEventList() { - return postprocessorEventList; - } - - public String getProtectView() { - return protectView; - } - } - public static RequestHandler getRequestHandler(ServletContext servletContext) { RequestHandler rh = (RequestHandler) servletContext.getAttribute("_REQUEST_HANDLER_"); if (rh == null) { @@ -180,7 +124,7 @@ public class RequestHandler { } /** - * Find a collection of request maps in {@code ccfg} matching {@code req}. + * Finds a collection of request maps in {@code ccfg} matching {@code req}. * Otherwise fall back to matching the {@code defaultReq} field in {@code ccfg}. * * @param ccfg The controller containing the current configuration @@ -188,22 +132,25 @@ public class RequestHandler { * @return a collection of request maps which might be empty */ static Collection<RequestMap> resolveURI(ControllerConfig ccfg, HttpServletRequest req) { - Map<String, List<RequestMap>> requestMapMap = ccfg.getRequestMapMap(); - Map<String, ConfigXMLReader.ViewMap> viewMapMap = ccfg.getViewMapMap(); - String defaultRequest = ccfg.getDefaultRequest(); - String path = req.getPathInfo(); - String requestUri = getRequestUri(path); - String viewUri = getOverrideViewUri(path); - Collection<RequestMap> rmaps; - if (requestMapMap.containsKey(requestUri) - // Ensure that overridden view exists. - && (viewUri == null || viewMapMap.containsKey(viewUri) - || ("SOAPService".equals(requestUri) && "wsdl".equalsIgnoreCase(req.getQueryString())))){ - rmaps = requestMapMap.get(requestUri); - } else if (defaultRequest != null) { - rmaps = requestMapMap.get(defaultRequest); - } else { - rmaps = null; + Map<String, List<RequestMap>> requestMapMap = ccfg.getRequestMapMultiMap(); + Collection<RequestMap> rmaps = resolveTemplateURI(requestMapMap, req); + if (rmaps.isEmpty()) { + Map<String, ConfigXMLReader.ViewMap> viewMapMap = ccfg.getViewMapMap(); + String defaultRequest = ccfg.getDefaultRequest(); + String path = req.getPathInfo(); + String requestUri = getRequestUri(path); + String overrideViewUri = getOverrideViewUri(path); + if (requestMapMap.containsKey(requestUri) + // Ensure that overridden view exists. + && (overrideViewUri == null || viewMapMap.containsKey(overrideViewUri) + || ("SOAPService".equals(requestUri) && "wsdl".equalsIgnoreCase(req.getQueryString())))){ + rmaps = requestMapMap.get(requestUri); + req.setAttribute("overriddenView", overrideViewUri); + } else if (defaultRequest != null) { + rmaps = requestMapMap.get(defaultRequest); + } else { + rmaps = null; + } } return rmaps != null ? rmaps : Collections.emptyList(); } @@ -232,6 +179,33 @@ public class RequestHandler { } } + /** + * Finds the request maps matching a segmented path. + * + * <p>A segmented path can match request maps where the {@code uri} attribute + * contains an URI template like in the {@code foo/bar/{baz}} example. + * + * @param rMapMap the map associating URIs to a list of request maps corresponding to different HTTP methods + * @param request the HTTP request to match + * @return a collection of request maps which might be empty but not {@code null} + */ + private static Collection<RequestMap> resolveTemplateURI(Map<String, List<RequestMap>> rMapMap, + HttpServletRequest request) { + // Retrieve the request path without the leading '/' character. + String path = request.getPathInfo().substring(1); + MultivaluedHashMap<String, String> vars = new MultivaluedHashMap<>(); + for (Map.Entry<String, List<RequestMap>> entry : rMapMap.entrySet()) { + URITemplate uriTemplate = URITemplate.createExactTemplate(entry.getKey()); + // Check if current path the URI template exactly. + if (uriTemplate.match(path, vars) && vars.getFirst("FINAL_MATCH_GROUP").equals("/")) { + // Set attributes from template variables to be used in context. + uriTemplate.getVariables().forEach(var -> request.setAttribute(var, vars.getFirst(var))); + return entry.getValue(); + } + } + return Collections.emptyList(); + } + public void doRequest(HttpServletRequest request, HttpServletResponse response, String chain, GenericValue userLogin, Delegator delegator) throws RequestHandlerException, RequestHandlerExceptionAllowExternalRequests { @@ -242,7 +216,7 @@ public class RequestHandler { // Parse controller config. try { - ccfg = new ControllerConfig(getControllerConfig()); + ccfg = ConfigXMLReader.getControllerConfig(controllerConfigURL); } catch (WebAppConfigurationException e) { Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); throw new RequestHandlerException(e); @@ -267,18 +241,27 @@ public class RequestHandler { String path = request.getPathInfo(); String requestUri = getRequestUri(path); - String overrideViewUri = getOverrideViewUri(path); Collection<RequestMap> rmaps = resolveURI(ccfg, request); if (rmaps.isEmpty()) { if (throwRequestHandlerExceptionOnMissingLocalRequest) { - throw new RequestHandlerException(requestMissingErrorMessage); + if (path.contains("/checkLogin/")) { + // Nested requests related with checkLogin uselessly clutter the log. There is nothing to worry about, better remove this wrong error message. + return; + } else if (path.contains("/images/") || path.contains("d.png")) { + if (Debug.warningOn()) Debug.logWarning("You should check if this request is really a problem or a false alarm: " + request.getRequestURL(), module); + throw new RequestHandlerException(requestMissingErrorMessage); + } else { + throw new RequestHandlerException(requestMissingErrorMessage); + } } else { - throw new RequestHandlerExceptionAllowExternalRequests(); + throw new RequestHandlerExceptionAllowExternalRequests(); } } + // The "overriddenView" attribute is set by resolveURI when necessary. + String overrideViewUri = (String) request.getAttribute("overriddenView"); - String method = request.getMethod(); + String method = UtilHttp.getRequestMethod(request); RequestMap requestMap = resolveMethod(method, rmaps).orElseThrow(() -> { String msg = UtilProperties.getMessage("WebappUiLabels", "RequestMethodNotMatchConfig", UtilMisc.toList(requestUri, method), UtilHttp.getLocale(request)); @@ -297,7 +280,7 @@ public class RequestHandler { // Check for chained request. if (chain != null) { String chainRequestUri = RequestHandler.getRequestUri(chain); - requestMap = ccfg.getRequestMapMap().getFirst(chainRequestUri); + requestMap = ccfg.getRequestMapMap().get(chainRequestUri); if (requestMap == null) { throw new RequestHandlerException("Unknown chained request [" + chainRequestUri + "]; this request does not exist"); } @@ -321,11 +304,11 @@ public class RequestHandler { // Check to make sure we are allowed to access this request directly. (Also checks if this request is defined.) // If the request cannot be called, or is not defined, check and see if there is a default-request we can process if (!requestMap.securityDirectRequest) { - if (ccfg.getDefaultRequest() == null || !ccfg.getRequestMapMap().getFirst(ccfg.getDefaultRequest()).securityDirectRequest) { + if (ccfg.getDefaultRequest() == null || !ccfg.getRequestMapMap().get(ccfg.getDefaultRequest()).securityDirectRequest) { // use the same message as if it was missing for security reasons, ie so can't tell if it is missing or direct request is not allowed throw new RequestHandlerException(requestMissingErrorMessage); } else { - requestMap = ccfg.getRequestMapMap().getFirst(ccfg.getDefaultRequest()); + requestMap = ccfg.getRequestMapMap().get(ccfg.getDefaultRequest()); } } // Check if we SHOULD be secure and are not. @@ -367,7 +350,7 @@ public class RequestHandler { String newUrl = RequestHandler.makeUrl(request, response, urlBuf.toString()); if (newUrl.toUpperCase().startsWith("HTTPS")) { // if we are supposed to be secure, redirect secure. - callRedirect(newUrl, response, request, ccfg.getStatusCodeString()); + callRedirect(newUrl, response, request, ccfg.getStatusCode()); return; } } @@ -375,33 +358,7 @@ public class RequestHandler { // Check for HTTPS client (x.509) security if (request.isSecure() && requestMap.securityCert) { - X509Certificate[] clientCerts = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); // 2.2 spec - if (clientCerts == null) { - clientCerts = (X509Certificate[]) request.getAttribute("javax.net.ssl.peer_certificates"); // 2.1 spec - } - if (clientCerts == null) { - Debug.logWarning("Received no client certificates from browser", module); - } - - // check if the client has a valid certificate (in our db store) - boolean foundTrustedCert = false; - - if (clientCerts == null) { - throw new RequestHandlerException(requestMissingErrorMessage); - } else { - if (Debug.infoOn()) { - for (int i = 0; i < clientCerts.length; i++) { - Debug.logInfo(clientCerts[i].getSubjectX500Principal().getName(), module); - } - } - - // check if this is a trusted cert - if (SSLUtil.isClientTrusted(clientCerts, null)) { - foundTrustedCert = true; - } - } - - if (!foundTrustedCert) { + if (!checkCertificates(request, certs -> SSLUtil.isClientTrusted(certs, null))) { Debug.logWarning(requestMissingErrorMessage, module); throw new RequestHandlerException(requestMissingErrorMessage); } @@ -472,13 +429,20 @@ public class RequestHandler { if (Debug.verboseOn()) Debug.logVerbose("[Processing Request]: " + requestMap.uri + showSessionId(request), module); request.setAttribute("thisRequestUri", requestMap.uri); // store the actual request URI + // Store current requestMap map to be referred later when generating csrf token + request.setAttribute("requestMapMap", getControllerConfig().getRequestMapMap()); + + // Perform CSRF token check when request not on chain + if (chain==null && originalRequestMap.securityCsrfToken) { + CsrfUtil.checkToken(request, path); + } // Perform security check. if (requestMap.securityAuth) { // Invoke the security handler // catch exceptions and throw RequestHandlerException if failed. if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler]: AuthRequired. Running security check. " + showSessionId(request), module); - ConfigXMLReader.Event checkLoginEvent = ccfg.getRequestMapMap().getFirst("checkLogin").event; + ConfigXMLReader.Event checkLoginEvent = ccfg.getRequestMapMap().get("checkLogin").event; String checkLoginReturnString = null; try { @@ -491,9 +455,9 @@ public class RequestHandler { eventReturn = checkLoginReturnString; // if the request is an ajax request we don't want to return the default login check if (!"XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) { - requestMap = ccfg.getRequestMapMap().getFirst("checkLogin"); + requestMap = ccfg.getRequestMapMap().get("checkLogin"); } else { - requestMap = ccfg.getRequestMapMap().getFirst("ajaxCheckLogin"); + requestMap = ccfg.getRequestMapMap().get("ajaxCheckLogin"); } } } @@ -502,9 +466,7 @@ public class RequestHandler { // we know this is the case if the _PREVIOUS_PARAM_MAP_ attribute is there, but the _PREVIOUS_REQUEST_ attribute has already been removed if (request.getSession().getAttribute("_PREVIOUS_PARAM_MAP_FORM_") != null && request.getSession().getAttribute("_PREVIOUS_REQUEST_") == null) { Map<String, Object> previousParamMap = UtilGenerics.checkMap(request.getSession().getAttribute("_PREVIOUS_PARAM_MAP_FORM_"), String.class, Object.class); - for (Map.Entry<String, Object> previousParamEntry: previousParamMap.entrySet()) { - request.setAttribute(previousParamEntry.getKey(), previousParamEntry.getValue()); - } + previousParamMap.forEach(request::setAttribute); // to avoid this data being included again, now remove the _PREVIOUS_PARAM_MAP_ attribute request.getSession().removeAttribute("_PREVIOUS_PARAM_MAP_FORM_"); @@ -599,6 +561,7 @@ public class RequestHandler { for (Map.Entry<String, Object> entry: preRequestMap.entrySet()) { String key = entry.getKey(); if ("_ERROR_MESSAGE_LIST_".equals(key) || "_ERROR_MESSAGE_MAP_".equals(key) || "_ERROR_MESSAGE_".equals(key) || + "_WARNING_MESSAGE_LIST_".equals(key) || "_WARNING_MESSAGE_".equals(key) || "_EVENT_MESSAGE_LIST_".equals(key) || "_EVENT_MESSAGE_".equals(key)) { request.setAttribute(key, entry.getValue()); } @@ -624,8 +587,13 @@ public class RequestHandler { if (UtilValidate.isNotEmpty(queryString)) { redirectTarget += "?" + queryString; } - - callRedirect(makeLink(request, response, redirectTarget), response, request, ccfg.getStatusCodeString()); + String link = makeLink(request, response, redirectTarget); + + // add / update csrf token to link when required + String tokenValue = CsrfUtil.generateTokenForNonAjax(request,redirectTarget); + link = CsrfUtil.addOrUpdateTokenInUrl(link, tokenValue); + + callRedirect(link, response, request, ccfg.getStatusCode()); return; } } @@ -683,31 +651,44 @@ public class RequestHandler { } } - String responseStatusCode = nextRequestResponse.statusCode; - if(UtilValidate.isNotEmpty(responseStatusCode)) - ccfg.setStatusCodeString(responseStatusCode); - + // The status code used to redirect the HTTP client. + String redirectSC = UtilValidate.isNotEmpty(nextRequestResponse.statusCode) + ? nextRequestResponse.statusCode + : ccfg.getStatusCode(); + if ("url".equals(nextRequestResponse.type)) { if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a URL redirect." + showSessionId(request), module); - callRedirect(nextRequestResponse.value, response, request, ccfg.getStatusCodeString()); + callRedirect(nextRequestResponse.value, response, request, redirectSC); } else if ("url-redirect".equals(nextRequestResponse.type)) { // check for a cross-application redirect if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a URL redirect with redirect parameters." + showSessionId(request), module); callRedirect(nextRequestResponse.value + this.makeQueryString(request, nextRequestResponse), response, - request, ccfg.getStatusCodeString()); + request, redirectSC); } else if ("cross-redirect".equals(nextRequestResponse.type)) { // check for a cross-application redirect if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a Cross-Application redirect." + showSessionId(request), module); String url = nextRequestResponse.value.startsWith("/") ? nextRequestResponse.value : "/" + nextRequestResponse.value; - callRedirect(url + this.makeQueryString(request, nextRequestResponse), response, request, ccfg.getStatusCodeString()); + callRedirect(url + this.makeQueryString(request, nextRequestResponse), response, request, redirectSC); } else if ("request-redirect".equals(nextRequestResponse.type)) { if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a Request redirect." + showSessionId(request), module); - callRedirect(makeLinkWithQueryString(request, response, "/" + nextRequestResponse.value, nextRequestResponse), response, request, ccfg.getStatusCodeString()); + String link = makeLinkWithQueryString(request, response, "/" + nextRequestResponse.value, nextRequestResponse); + + // add / update csrf token to link when required + String tokenValue = CsrfUtil.generateTokenForNonAjax(request, nextRequestResponse.value); + link = CsrfUtil.addOrUpdateTokenInUrl(link, tokenValue); + + callRedirect(link, response, request, redirectSC); } else if ("request-redirect-noparam".equals(nextRequestResponse.type)) { if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a Request redirect with no parameters." + showSessionId(request), module); - callRedirect(makeLink(request, response, nextRequestResponse.value), response, request, ccfg.getStatusCodeString()); + String link = makeLink(request, response, nextRequestResponse.value); + + // add token to link when required + String tokenValue = CsrfUtil.generateTokenForNonAjax(request, nextRequestResponse.value); + link = CsrfUtil.addOrUpdateTokenInUrl(link, tokenValue); + + callRedirect(link, response, request, redirectSC); } else if ("view".equals(nextRequestResponse.type)) { if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a view." + showSessionId(request), module); @@ -724,13 +705,13 @@ public class RequestHandler { Map<String, Object> urlParams = null; if (session.getAttribute("_SAVED_VIEW_NAME_") != null) { viewName = (String) session.getAttribute("_SAVED_VIEW_NAME_"); - urlParams = UtilGenerics.<String, Object>checkMap(session.getAttribute("_SAVED_VIEW_PARAMS_")); + urlParams = UtilGenerics.cast(session.getAttribute("_SAVED_VIEW_PARAMS_")); } else if (session.getAttribute("_HOME_VIEW_NAME_") != null) { viewName = (String) session.getAttribute("_HOME_VIEW_NAME_"); - urlParams = UtilGenerics.<String, Object>checkMap(session.getAttribute("_HOME_VIEW_PARAMS_")); + urlParams = UtilGenerics.cast(session.getAttribute("_HOME_VIEW_PARAMS_")); } else if (session.getAttribute("_LAST_VIEW_NAME_") != null) { viewName = (String) session.getAttribute("_LAST_VIEW_NAME_"); - urlParams = UtilGenerics.<String, Object>checkMap(session.getAttribute("_LAST_VIEW_PARAMS_")); + urlParams = UtilGenerics.cast(session.getAttribute("_LAST_VIEW_PARAMS_")); } else if (UtilValidate.isNotEmpty(nextRequestResponse.value)) { viewName = nextRequestResponse.value; } @@ -775,7 +756,7 @@ public class RequestHandler { Map<String, Object> urlParams = null; if (session.getAttribute("_HOME_VIEW_NAME_") != null) { viewName = (String) session.getAttribute("_HOME_VIEW_NAME_"); - urlParams = UtilGenerics.<String, Object>checkMap(session.getAttribute("_HOME_VIEW_PARAMS_")); + urlParams = UtilGenerics.cast(session.getAttribute("_HOME_VIEW_PARAMS_")); } if (urlParams != null) { for (Map.Entry<String, Object> urlParamEntry: urlParams.entrySet()) { @@ -808,7 +789,7 @@ public class RequestHandler { try { String errorPageLocation = getControllerConfig().getErrorpage(); errorPage = FlexibleLocation.resolveLocation(errorPageLocation); - } catch (WebAppConfigurationException | MalformedURLException e) { + } catch (MalformedURLException e) { Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); } if (errorPage == null) { @@ -819,14 +800,8 @@ public class RequestHandler { /** Returns the default status-code for this request. */ public String getStatusCode(HttpServletRequest request) { - String statusCode = null; - try { - statusCode = getControllerConfig().getStatusCode(); - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - } - if (UtilValidate.isNotEmpty(statusCode)) return statusCode; - return null; + String statusCode = getControllerConfig().getStatusCode(); + return UtilValidate.isNotEmpty(statusCode) ? statusCode : null; } /** Returns the ViewFactory Object. */ @@ -871,11 +846,11 @@ public class RequestHandler { return nextPage; } - private void callRedirect(String url, HttpServletResponse resp, HttpServletRequest req, String statusCodeString) throws RequestHandlerException { + private static void callRedirect(String url, HttpServletResponse resp, HttpServletRequest req, String statusCodeString) throws RequestHandlerException { if (Debug.infoOn()) Debug.logInfo("Sending redirect to: [" + url + "]. " + showSessionId(req), module); // set the attributes in the session so we can access it. Enumeration<String> attributeNameEnum = UtilGenerics.cast(req.getAttributeNames()); - Map<String, Object> reqAttrMap = new HashMap<String, Object>(); + Map<String, Object> reqAttrMap = new HashMap<>(); Integer statusCode; try { statusCode = Integer.valueOf(statusCodeString); @@ -968,13 +943,7 @@ public class RequestHandler { req.getSession().removeAttribute("_SAVED_VIEW_PARAMS_"); } - ConfigXMLReader.ViewMap viewMap = null; - try { - viewMap = (view == null ? null : getControllerConfig().getViewMapMap().get(view)); - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - throw new RequestHandlerException(e); - } + ConfigXMLReader.ViewMap viewMap = (view == null) ? null : getControllerConfig().getViewMapMap().get(view); if (viewMap == null) { throw new RequestHandlerException("No definition found for view with name [" + view + "]"); } @@ -1099,7 +1068,7 @@ public class RequestHandler { } } - private void addNameValuePairToQueryString(StringBuilder queryString, String name, String value) { + private static void addNameValuePairToQueryString(StringBuilder queryString, String name, String value) { if (UtilValidate.isNotEmpty(value)) { if (queryString.length() > 1) { queryString.append("&"); @@ -1124,6 +1093,10 @@ public class RequestHandler { } public String makeLink(HttpServletRequest request, HttpServletResponse response, String url, boolean fullPath, boolean secure, boolean encode) { + return makeLink(request, response, url, fullPath, secure, encode, ""); + } + + public String makeLink(HttpServletRequest request, HttpServletResponse response, String url, boolean fullPath, boolean secure, boolean encode, String targetControlPath) { WebSiteProperties webSiteProps = null; try { webSiteProps = WebSiteProperties.from(request); @@ -1135,13 +1108,7 @@ public class RequestHandler { String requestUri = RequestHandler.getRequestUri(url); ConfigXMLReader.RequestMap requestMap = null; if (requestUri != null) { - try { - requestMap = getControllerConfig().getRequestMapMap().get(requestUri); - } catch (WebAppConfigurationException e) { - // If we can't read the controller.xml file, then there is no point in continuing. - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - return null; - } + requestMap = getControllerConfig().getRequestMapMap().get(requestUri); } boolean didFullSecure = false; boolean didFullStandard = false; @@ -1173,8 +1140,12 @@ public class RequestHandler { return null; } } - // create the path to the control servlet - String controlPath = (String) request.getAttribute("_CONTROL_PATH_"); + + String controlPath = targetControlPath; + if (UtilValidate.isEmpty(controlPath)){ + // create the path to the control servlet + controlPath = (String) request.getAttribute("_CONTROL_PATH_"); + } //If required by webSite parameter, surcharge control path if (webSiteProps.getWebappPath() != null) { @@ -1197,7 +1168,7 @@ public class RequestHandler { try { GenericValue webSiteValue = EntityQuery.use(delegator).from("WebSite").where("webSiteId", webSiteId).cache().queryOne(); if (webSiteValue != null) { - ServletContext application = ((ServletContext) request.getAttribute("servletContext")); + ServletContext application = (request.getServletContext()); String domainName = request.getLocalName(); if (application.getAttribute("MULTI_SITE_ENABLED") != null && UtilValidate.isNotEmpty(webSiteValue.getString("hostedPathAlias")) && !domainName.equals(webSiteValue.getString("httpHost"))) { newURL.append('/'); @@ -1228,9 +1199,9 @@ public class RequestHandler { return makeUrl(request, response, url, false, false, false); } - public static String makeUrl(HttpServletRequest request, HttpServletResponse response, String url, boolean fullPath, boolean secure, boolean encode) { - ServletContext ctx = (ServletContext) request.getAttribute("servletContext"); - RequestHandler rh = (RequestHandler) ctx.getAttribute("_REQUEST_HANDLER_"); + public static String makeUrl(HttpServletRequest request, HttpServletResponse response, String url, boolean fullPath, + boolean secure, boolean encode) { + RequestHandler rh = from(request); return rh.makeLink(request, response, url, fullPath, secure, encode); } @@ -1277,55 +1248,58 @@ public class RequestHandler { runEvents(req, resp, prod, "before-logout"); } - public boolean trackStats(HttpServletRequest request) { - if (trackServerHit) { - String uriString = RequestHandler.getRequestUri(request.getPathInfo()); - if (uriString == null) { - uriString=""; - } - ConfigXMLReader.RequestMap requestMap = null; - try { - requestMap = getControllerConfig().getRequestMapMap().get(uriString); - if (requestMap == null) { - requestMap = getControllerConfig().getRequestMapMap().get(getControllerConfig().getDefaultRequest()); - if (requestMap == null) { - return false; - } - } - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - } - return requestMap.trackServerHit; - } else { + /** + * Checks if a request must be tracked according a global toggle and a request map predicate. + * + * @param request the request that can potentially be tracked + * @param globalToggle the global configuration toggle + * @param pred the predicate checking if each individual request map must be tracked or not. + * @return {@code true} when the request must be tracked. + * @throws NullPointerException when either {@code request} or {@code pred} is {@code null}. + */ + private boolean track(HttpServletRequest request, boolean globalToggle, Predicate<RequestMap> pred) { + if (!globalToggle) { return false; } + // XXX: We are basically re-implementing `resolveURI` poorly, It would be better + // to take a `request-map` as input but it is not currently not possible because this method + // is used outside `doRequest`. + String uriString = RequestHandler.getRequestUri(request.getPathInfo()); + if (uriString == null) { + uriString= ""; + } + Map<String, RequestMap> rmaps = getControllerConfig().getRequestMapMap(); + RequestMap requestMap = rmaps.get(uriString); + if (requestMap == null) { + requestMap = rmaps.get(getControllerConfig().getDefaultRequest()); + if (requestMap == null) { + return false; + } + } + return pred.test(requestMap); } + /** + * Checks if server hits must be tracked for a given request. + * + * @param request the HTTP request that can potentially be tracked + * @return {@code true} when the request must be tracked. + */ + public boolean trackStats(HttpServletRequest request) { + return track(request, trackServerHit, rmap -> rmap.trackServerHit); + } + + /** + * Checks if visits must be tracked for a given request. + * + * @param request the HTTP request that can potentially be tracked + * @return {@code true} when the request must be tracked. + */ public boolean trackVisit(HttpServletRequest request) { - if (trackVisit) { - String uriString = RequestHandler.getRequestUri(request.getPathInfo()); - if (uriString == null) { - uriString=""; - } - ConfigXMLReader.RequestMap requestMap = null; - try { - requestMap = getControllerConfig().getRequestMapMap().get(uriString); - if (requestMap == null) { - requestMap = getControllerConfig().getRequestMapMap().get(getControllerConfig().getDefaultRequest()); - if (requestMap == null) { - return false; - } - } - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - } - return requestMap.trackVisit; - } else { - return false; - } + return track(request, trackVisit, rmap -> rmap.trackVisit); } - private String showSessionId(HttpServletRequest request) { + private static String showSessionId(HttpServletRequest request) { Delegator delegator = (Delegator) request.getAttribute("delegator"); boolean showSessionIdInLog = EntityUtilProperties.propertyValueEqualsIgnoreCase("requestHandler", "show-sessionId-in-log", "Y", delegator); if (showSessionIdInLog) { @@ -1333,4 +1307,42 @@ public class RequestHandler { } return " Hidden sessionId by default."; } + + /** + * Checks that the request contains some valid certificates. + * + * @param request the request to verify + * @param validator the predicate applied the certificates found + * @return true if the request contains some valid certificates, otherwise false. + */ + static boolean checkCertificates(HttpServletRequest request, Predicate<X509Certificate[]> validator) { + return Stream.of("javax.servlet.request.X509Certificate", // 2.2 spec + "javax.net.ssl.peer_certificates") // 2.1 spec + .map(request::getAttribute) + .filter(Objects::nonNull) + .map(X509Certificate[].class::cast) + .peek(certs -> { + if (Debug.infoOn()) { + for (X509Certificate cert : certs) { + Debug.logInfo(cert.getSubjectX500Principal().getName(), module); + } + } + }) + .map(validator::test) + .findFirst().orElseGet(() -> { + Debug.logWarning("Received no client certificates from browser", module); + return false; + }); + } + + /** + * Retrieves the request handler which is stored inside an HTTP request. + * + * @param request the HTTP request containing the request handler + * @return a request handler or {@code null} when absent + * @throws NullPointerException when {@code request} or the servlet context is {@code null}. + */ + public static RequestHandler from(HttpServletRequest request) { + return UtilGenerics.cast(request.getServletContext().getAttribute("_REQUEST_HANDLER_")); + } } diff --git a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/CsrfTokenAjaxTransform.java b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/CsrfTokenAjaxTransform.java new file mode 100644 index 0000000..6a2d89e --- /dev/null +++ b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/CsrfTokenAjaxTransform.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * 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.ofbiz.webapp.ftl; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.ofbiz.security.CsrfUtil; + +import freemarker.core.Environment; +import freemarker.ext.beans.BeanModel; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateTransformModel; + +/** + * CsrfTokenAjaxTransform - Freemarker Transform for csrf token in Ajax call + */ +public class CsrfTokenAjaxTransform implements TemplateTransformModel { + + public final static String module = CsrfTokenAjaxTransform.class.getName(); + + @Override + public Writer getWriter(Writer out, @SuppressWarnings("rawtypes") Map args) + throws TemplateModelException, IOException { + + return new Writer(out) { + + @Override + public void close() throws IOException { + try { + Environment env = Environment.getCurrentEnvironment(); + BeanModel req = (BeanModel) env.getVariable("request"); + if (req != null) { + HttpServletRequest request = (HttpServletRequest) req.getWrappedObject(); + String tokenValue = CsrfUtil.generateTokenForAjax(request); + out.write(tokenValue); + } + return; + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void write(char cbuf[], int off, int len) { + + } + }; + + } +} diff --git a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/CsrfTokenPairNonAjaxTransform.java b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/CsrfTokenPairNonAjaxTransform.java new file mode 100644 index 0000000..d51bd61 --- /dev/null +++ b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/CsrfTokenPairNonAjaxTransform.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * 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.ofbiz.webapp.ftl; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.ofbiz.security.CsrfUtil; + +import freemarker.core.Environment; +import freemarker.ext.beans.BeanModel; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateTransformModel; + +/** + * CsrfTokenPairNonAjaxTransform - Freemarker Transform for csrf token in non-Ajax call + */ +public class CsrfTokenPairNonAjaxTransform implements TemplateTransformModel { + + public final static String module = CsrfTokenPairNonAjaxTransform.class.getName(); + + @Override + public Writer getWriter(Writer out, @SuppressWarnings("rawtypes") Map args) + throws TemplateModelException, IOException { + + final StringBuffer buf = new StringBuffer(); + + return new Writer(out) { + + @Override + public void close() throws IOException { + try { + Environment env = Environment.getCurrentEnvironment(); + BeanModel req = (BeanModel) env.getVariable("request"); + if (req != null) { + HttpServletRequest request = (HttpServletRequest) req.getWrappedObject(); + String tokenValue = CsrfUtil.generateTokenForNonAjax(request, buf.toString()); + out.write(CsrfUtil.tokenNameNonAjax +"="+tokenValue); + } + return; + } catch (Exception e) { + throw new IOException(e.getMessage()); + } + } + + @Override + public void write(char cbuf[], int off, int len) { + buf.append(cbuf, off, len); + } + + @Override + public void flush() throws IOException { + out.flush(); + } + }; + } +} diff --git a/framework/webtools/groovyScripts/entity/CheckDb.groovy b/framework/webtools/groovyScripts/entity/CheckDb.groovy index fd822de..567714f 100644 --- a/framework/webtools/groovyScripts/entity/CheckDb.groovy +++ b/framework/webtools/groovyScripts/entity/CheckDb.groovy @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import org.apache.ofbiz.entity.Delegator -import org.apache.ofbiz.security.Security + + import org.apache.ofbiz.entity.jdbc.DatabaseUtil -import org.apache.ofbiz.entity.model.ModelEntity controlPath = parameters._CONTROL_PATH_ @@ -114,7 +113,7 @@ if (security.hasPermission("ENTITY_MAINT", session)) { miter = messages.iterator() context.miters = miter } - context.encodeURLCheckDb = response.encodeURL(controlPath + "/view/checkdb") + context.checkDbURL = "view/checkdb" context.groupName = groupName ?: "org.apache.ofbiz" context.entityName = entityName ?: "" } diff --git a/framework/webtools/groovyScripts/entity/EntityRef.groovy b/framework/webtools/groovyScripts/entity/EntityRef.groovy index 17933db..279e448 100644 --- a/framework/webtools/groovyScripts/entity/EntityRef.groovy +++ b/framework/webtools/groovyScripts/entity/EntityRef.groovy @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import org.apache.ofbiz.security.CsrfUtil; + controlPath = parameters._CONTROL_PATH_ list = "$controlPath/view/entityref_list" main = "$controlPath/view/entityref_main" @@ -29,5 +31,9 @@ if (search) { list = "$list?forstatic=$forstatic" main = "$main?forstatic=$forstatic" } +tokenList = CsrfUtil.generateTokenForNonAjax(request, "view/entityref_list") +tokenMain = CsrfUtil.generateTokenForNonAjax(request, "view/entityref_main") +list = CsrfUtil.addOrUpdateTokenInUrl(list, tokenList) +main = CsrfUtil.addOrUpdateTokenInUrl(main, tokenMain) context.encodeUrlList = response.encodeURL(list) context.encodeUrlMain = response.encodeURL(main) diff --git a/framework/webtools/template/entity/CheckDb.ftl b/framework/webtools/template/entity/CheckDb.ftl index ac81459..91cf8d3 100644 --- a/framework/webtools/template/entity/CheckDb.ftl +++ b/framework/webtools/template/entity/CheckDb.ftl @@ -17,7 +17,7 @@ specific language governing permissions and limitations under the License. --> <h3>${uiLabelMap.WebtoolsCheckUpdateDatabase}</h3> -<form class="basic-form" class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -61,7 +61,7 @@ under the License. } </script> <h3>${uiLabelMap.WebtoolsRemoveAllTables}</h3> -<form class="basic-form" class="basic-form" method="post" action="${encodeURLCheckDb}" name="TablesRemoveForm"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TablesRemoveForm"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -80,7 +80,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}" name="TableRemoveForm"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TableRemoveForm"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -107,7 +107,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsCreateRemoveAllPrimaryKeys}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -125,7 +125,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -143,7 +143,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsCreateRemovePrimaryKey}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -168,7 +168,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -197,7 +197,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsCreateRemoveAllDeclaredIndices}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -214,7 +214,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -232,7 +232,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeyIndices}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -249,7 +249,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -268,7 +268,7 @@ under the License. </form> <h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeys}</h3> <p>${uiLabelMap.WebtoolsNoteForeighKeysMayAlsoBeCreated}</p> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -285,7 +285,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -303,7 +303,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsUpdateCharacterSetAndCollate}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> diff --git a/framework/webtools/template/entity/EntityRefList.ftl b/framework/webtools/template/entity/EntityRefList.ftl index 1ace17f..55e2387 100644 --- a/framework/webtools/template/entity/EntityRefList.ftl +++ b/framework/webtools/template/entity/EntityRefList.ftl @@ -1,3 +1,4 @@ + <#-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file @@ -54,9 +55,9 @@ under the License. <div class="section-header">${uiLabelMap.WebtoolsEntityPackages}</div> <#list packageNames as packageName> <#if forstatic> - <a href="<@ofbizUrl>view/entityref_main?forstatic=true#${packageName}</@ofbizUrl>" target="entityFrame">${packageName}</a><br /> + <a href="<@ofbizUrl>view/entityref_main?forstatic=true</@ofbizUrl>#${packageName}" target="entityFrame">${packageName}</a><br /> <#else> - <a href="<@ofbizUrl>view/entityref_main#${packageName}</@ofbizUrl>" target="entityFrame">${packageName}</a><br /> + <a href="<@ofbizUrl>view/entityref_main</@ofbizUrl>#${packageName}" target="entityFrame">${packageName}</a><br /> </#if> </#list> </#if> @@ -65,9 +66,9 @@ under the License. <div class="section-header">${uiLabelMap.WebtoolsEntitiesAlpha}</div> <#list entitiesList as entity> <#if forstatic> - <a href="<@ofbizUrl>view/entityref_main?forstatic=true#${entity.entityName}</@ofbizUrl>" target="entityFrame">${entity.entityName}</a> + <a href="<@ofbizUrl>view/entityref_main?forstatic=true</@ofbizUrl>#${entity.entityName}" target="entityFrame">${entity.entityName}</a> <#else> - <a href="<@ofbizUrl>view/entityref_main#${entity.entityName}${entity.url!}</@ofbizUrl>" target="entityFrame">${entity.entityName}</a> + <a href="<@ofbizUrl>view/entityref_main</@ofbizUrl>#${entity.entityName}${entity.url!}" target="entityFrame">${entity.entityName}</a> </#if> <br /> </#list> diff --git a/framework/webtools/template/entity/ViewGeneric.ftl b/framework/webtools/template/entity/ViewGeneric.ftl index f6e03f7..32b7382 100644 --- a/framework/webtools/template/entity/ViewGeneric.ftl +++ b/framework/webtools/template/entity/ViewGeneric.ftl @@ -38,6 +38,7 @@ function ShowTab(lname) { } </script> +<#assign currentFindString = currentFindString?replace("/", "/")!> <div class="screenlet"> <div class="screenlet-title-bar"> <ul> diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java index 08a92a0..697ac90 100644 --- a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java +++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java @@ -40,6 +40,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.apache.ofbiz.security.CsrfUtil; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.StringUtil; import org.apache.ofbiz.base.util.UtilCodec; @@ -1409,6 +1410,10 @@ public final class MacroFormRenderer implements FormStringRenderer { } } String focusFieldName = modelForm.getFocusFieldName(); + + // Generate CSRF name & value for form + String csrfNameValue = CsrfUtil.tokenNameNonAjax + " " +CsrfUtil.generateTokenForNonAjax(request, targ); + StringWriter sr = new StringWriter(); sr.append("<@renderFormOpen "); sr.append(" linkUrl=\""); @@ -1439,7 +1444,9 @@ public final class MacroFormRenderer implements FormStringRenderer { sr.append(Integer.toString(viewSize)); sr.append("\" useRowSubmit="); sr.append(Boolean.toString(useRowSubmit)); - sr.append(" />"); + sr.append(" csrfNameValue=\""); + sr.append(csrfNameValue); + sr.append("\" />"); executeMacro(writer, sr.toString()); } @@ -2362,6 +2369,11 @@ public final class MacroFormRenderer implements FormStringRenderer { viewSizeParam = "VIEW_SIZE" + "_" + paginatorNumber; } String str = (String) context.get("_QBESTRING_"); + + // refresh any csrf token in the query string for pagination + String tokenValue = CsrfUtil.generateTokenForNonAjax(request, targetService); + str = CsrfUtil.addOrUpdateTokenInQueryString(str, tokenValue); + // strip legacy viewIndex/viewSize params from the query string String queryString = UtilHttp.stripViewParamsFromQueryString(str, "" + paginatorNumber); // strip parameterized index/size params from the query string diff --git a/themes/bluelight/template/Header.ftl b/themes/bluelight/template/Header.ftl index 7f1038b..2c27eb7 100644 --- a/themes/bluelight/template/Header.ftl +++ b/themes/bluelight/template/Header.ftl @@ -28,6 +28,10 @@ under the License. <html lang="${docLangAttr}" dir="${langDir}" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <#assign csrfDefenseStrategy = Static["org.apache.ofbiz.entity.util.EntityUtilProperties"].getPropertyValue("security", "csrf.defense.strategy", delegator)> + <#if csrfDefenseStrategy != "org.apache.ofbiz.security.NoCsrfDefenseStrategy"> + <meta name="csrf-token" content="<@csrfTokenAjax/>"/> + </#if> <title>${layoutSettings.companyName}: <#if (titleProperty)?has_content>${uiLabelMap[titleProperty]}<#else>${title!}</#if></title> <#if layoutSettings.shortcutIcon?has_content> <#assign shortcutIcon = layoutSettings.shortcutIcon/> @@ -194,7 +198,7 @@ under the License. <#--if webSiteId?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??--> <#if parameters.componentName?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??> <#include "component://common-theme/template/includes/HelpLink.ftl" /> - <li><a class="help-link <#if pageAvail?has_content> alert</#if>" href="javascript:lookup_popup1('showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> + <li><a class="help-link <#if pageAvail?has_content> alert</#if>" href="javascript:lookup_popup1('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> </#if> <#if userLogin??> <#if "Y" == (userPreferences.COMPACT_HEADER)?default("N")> diff --git a/themes/common-theme/template/includes/ListLocales.ftl b/themes/common-theme/template/includes/ListLocales.ftl index 647090f..82c7ca7 100644 --- a/themes/common-theme/template/includes/ListLocales.ftl +++ b/themes/common-theme/template/includes/ListLocales.ftl @@ -36,7 +36,7 @@ under the License. </#if> <tr <#if altRow>class="alternate-row"</#if>> <td lang="${langAttr}" dir="${langDir}"> - <a href="<@ofbizUrl>setSessionLocale</@ofbizUrl>?newLocale=${availableLocale.toString()}"> + <a href="<@ofbizUrl>setSessionLocale?newLocale=${availableLocale.toString()}</@ofbizUrl>"> ${availableLocale.getDisplayName(availableLocale)} - [${langAttr}]</a> </td> diff --git a/themes/common-theme/template/macro/CsvFormMacroLibrary.ftl b/themes/common-theme/template/macro/CsvFormMacroLibrary.ftl index cadd70e..b371b19 100644 --- a/themes/common-theme/template/macro/CsvFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/CsvFormMacroLibrary.ftl @@ -54,7 +54,7 @@ under the License. <#macro renderEmptyFormDataMessage message></#macro> <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/template/macro/FoFormMacroLibrary.ftl b/themes/common-theme/template/macro/FoFormMacroLibrary.ftl index c99efc9..a0d8b7e 100644 --- a/themes/common-theme/template/macro/FoFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/FoFormMacroLibrary.ftl @@ -80,7 +80,7 @@ under the License. <#macro renderEmptyFormDataMessage message></#macro> <#macro renderSingleFormFieldTitle><!--title form--></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl index 0923033..77b3b67 100644 --- a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl @@ -243,8 +243,14 @@ under the License. </#macro> <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType name viewIndexField viewSizeField viewIndex viewSize targetWindow="" containerId="" containerStyle="" autocomplete="" useRowSubmit="" focusFieldName="" hasRequiredField=""> +<#macro renderFormOpen linkUrl formType name viewIndexField viewSizeField viewIndex viewSize targetWindow="" containerId="" containerStyle="" autocomplete="" useRowSubmit="" focusFieldName="" hasRequiredField="" csrfNameValue=""> <form method="post" action="${linkUrl}"<#if formType=="upload"> enctype="multipart/form-data"</#if><#if targetWindow?has_content> target="${targetWindow}"</#if><#if containerId?has_content> id="${containerId}"</#if> <#if focusFieldName?has_content> data-focus-field="${focusFieldName}"</#if> class="<#if containerStyle?has_content>${containerStyle}<#else>basic-form</#if><#if hasRequiredField?has_content> requireValidation</#if>" onsubmit="javascript:submitFormDisableSubmits(this)"<#if au [...] + <#if csrfNameValue?has_content> + <#assign result = csrfNameValue?matches(r"(\w+) (\w+)")> + <#if result> + <input type="hidden" name="${result?groups[1]}" value="${result?groups[2]}"/> + </#if> + </#if> <#if useRowSubmit?has_content && useRowSubmit> <input type="hidden" name="_useRowSubmit" value="Y"/> <#if linkUrl?index_of("VIEW_INDEX") <= 0 && linkUrl?index_of(viewIndexField) <= 0> diff --git a/themes/common-theme/template/macro/TextFormMacroLibrary.ftl b/themes/common-theme/template/macro/TextFormMacroLibrary.ftl index 228611e..0e97938 100644 --- a/themes/common-theme/template/macro/TextFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/TextFormMacroLibrary.ftl @@ -54,7 +54,7 @@ under the License. <#macro renderEmptyFormDataMessage message></#macro> <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl b/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl index 0998073..0472f2d 100644 --- a/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl @@ -59,7 +59,7 @@ under the License. <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/template/macro/XmlFormMacroLibrary.ftl b/themes/common-theme/template/macro/XmlFormMacroLibrary.ftl index b8cbc51..acc2f28 100644 --- a/themes/common-theme/template/macro/XmlFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/XmlFormMacroLibrary.ftl @@ -62,7 +62,7 @@ under the License. <#macro renderEmptyFormDataMessage message></#macro> <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/webapp/common/js/util/OfbizUtil.js b/themes/common-theme/webapp/common/js/util/OfbizUtil.js index ce99998..e42dc44 100644 --- a/themes/common-theme/webapp/common/js/util/OfbizUtil.js +++ b/themes/common-theme/webapp/common/js/util/OfbizUtil.js @@ -25,6 +25,16 @@ var AJAX_REQUEST_TIMEOUT = 5000; // Add observers on DOM ready. $(document).ready(function() { + // add CSRF token to jQuery AJAX calls to the same domain + jQuery.ajaxPrefilter(function(options, _, jqXHR) { + var token; + if (!options.crossDomain) { + token = jQuery("meta[name='csrf-token']").attr("content") + if (token) { + return jqXHR.setRequestHeader("X-CSRF-Token", token); + } + } + }); //initializing UI combobox dropdown by overriding its methods. ajaxAutoCompleteDropDown(); // bindObservers will add observer on passed html section when DOM is ready. @@ -1218,7 +1228,7 @@ function getJSONuiLabels(requiredLabels, callback) { } } /** - * Read the requiered uiLabel from the uiLabelXml Resource + * Read the required uiLabel from the uiLabelXml Resource * @param uiResource String * @param errUiLabel String * @returns String with Label diff --git a/themes/flatgrey/template/Header.ftl b/themes/flatgrey/template/Header.ftl index 8920f07..bbe4eb3 100644 --- a/themes/flatgrey/template/Header.ftl +++ b/themes/flatgrey/template/Header.ftl @@ -24,6 +24,10 @@ under the License. <html lang="${docLangAttr}" dir="${langDir}" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <#assign csrfDefenseStrategy = Static["org.apache.ofbiz.entity.util.EntityUtilProperties"].getPropertyValue("security", "csrf.defense.strategy", delegator)> + <#if csrfDefenseStrategy != "org.apache.ofbiz.security.NoCsrfDefenseStrategy"> + <meta name="csrf-token" content="<@csrfTokenAjax/>"/> + </#if> <title>${layoutSettings.companyName}: <#if (titleProperty)?has_content>${uiLabelMap[titleProperty]}<#else>${title!}</#if></title> <#if layoutSettings.shortcutIcon?has_content> <#assign shortcutIcon = layoutSettings.shortcutIcon/> @@ -156,7 +160,7 @@ under the License. <#---if webSiteId?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??--> <#if parameters.componentName?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??> <#include "component://common-theme/template/includes/HelpLink.ftl" /> - <li><a <#if pageAvail?has_content>class="alert"</#if> href="javascript:lookup_popup1('showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}','help' ,500,500);">${uiLabelMap.CommonHelp}</a></li> + <li><a <#if pageAvail?has_content>class="alert"</#if> href="javascript:lookup_popup1('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);">${uiLabelMap.CommonHelp}</a></li> </#if> </ul> </li> diff --git a/themes/rainbowstone/template/includes/Header.ftl b/themes/rainbowstone/template/includes/Header.ftl index bb1ad5e..93a8500 100644 --- a/themes/rainbowstone/template/includes/Header.ftl +++ b/themes/rainbowstone/template/includes/Header.ftl @@ -24,6 +24,10 @@ under the License. <html lang="${docLangAttr}" dir="${langDir}" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <#assign csrfDefenseStrategy = Static["org.apache.ofbiz.entity.util.EntityUtilProperties"].getPropertyValue("security", "csrf.defense.strategy", delegator)> + <#if csrfDefenseStrategy != "org.apache.ofbiz.security.NoCsrfDefenseStrategy"> + <meta name="csrf-token" content="<@csrfTokenAjax/>"/> + </#if> <title>${layoutSettings.companyName}: <#if (titleProperty)?has_content>${uiLabelMap[titleProperty]}<#else>${title!}</#if></title> <#if layoutSettings.shortcutIcon?has_content> <#assign shortcutIcon = layoutSettings.shortcutIcon/> diff --git a/themes/rainbowstone/template/includes/TopAppBar.ftl b/themes/rainbowstone/template/includes/TopAppBar.ftl index 18f610a..c70a040 100644 --- a/themes/rainbowstone/template/includes/TopAppBar.ftl +++ b/themes/rainbowstone/template/includes/TopAppBar.ftl @@ -238,7 +238,7 @@ under the License. <div id="main-nav-bar-right"> <div id="company-logo"></div> <#if parameters.componentName?exists && requestAttributes._CURRENT_VIEW_?exists && helpTopic?exists> - <a class="dark-color" title="${uiLabelMap.CommonHelp}" href="javascript:lookup_popup1('showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}','help' ,500,500);"><img class="appbar-btn-img" id="help-btn" src="/rainbowstone/images/help.svg" alt="Help"></a> + <a class="dark-color" title="${uiLabelMap.CommonHelp}" href="javascript:lookup_popup1('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);"><img class="appbar-btn-img" id="help-btn" src="/rainbowstone/images/help.svg" alt="Help"></a> </#if> <#include "component://rainbowstone/template/includes/Avatar.ftl"/> diff --git a/themes/tomahawk/template/AppBarClose.ftl b/themes/tomahawk/template/AppBarClose.ftl index af35581..811bac5 100644 --- a/themes/tomahawk/template/AppBarClose.ftl +++ b/themes/tomahawk/template/AppBarClose.ftl @@ -75,7 +75,7 @@ under the License. <#--if webSiteId?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??--> <#if parameters.componentName?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??> <#include "component://common-theme/template/includes/HelpLink.ftl" /> - <li><a class="help-link <#if pageAvail?has_content> alert</#if>" href="javascript:lookup_popup1('showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> + <li><a class="help-link <#if pageAvail?has_content> alert</#if>" href="javascript:lookup_popup1('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> </#if> <li><a href="<@ofbizUrl>logout</@ofbizUrl>">${uiLabelMap.CommonLogout}</a></li> <li><a href="<@ofbizUrl>ListVisualThemes</@ofbizUrl>">${uiLabelMap.CommonVisualThemes}</a></li> diff --git a/themes/tomahawk/template/Header.ftl b/themes/tomahawk/template/Header.ftl index 3376614..d01ae9c 100644 --- a/themes/tomahawk/template/Header.ftl +++ b/themes/tomahawk/template/Header.ftl @@ -28,6 +28,10 @@ under the License. <html lang="${docLangAttr}" dir="${langDir}" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <#assign csrfDefenseStrategy = Static["org.apache.ofbiz.entity.util.EntityUtilProperties"].getPropertyValue("security", "csrf.defense.strategy", delegator)> + <#if csrfDefenseStrategy != "org.apache.ofbiz.security.NoCsrfDefenseStrategy"> + <meta name="csrf-token" content="<@csrfTokenAjax/>"/> + </#if> <title>${layoutSettings.companyName}: <#if (titleProperty)?has_content>${uiLabelMap[titleProperty]}<#else>${title!}</#if></title> <#if layoutSettings.shortcutIcon?has_content> <#assign shortcutIcon = layoutSettings.shortcutIcon/>

