This is an automated email from the ASF dual-hosted git repository.
jleroux pushed a change to branch release18.12 in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git. from 8f51785 Fixed: The createTaskContent request does not work new 0add8be 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 new 9fa3dbe Revert "Merge branch 'JacquesLeRoux-POC-for-CSRF-Token-OFBIZ-11306' into trunk" new 5530e23 Fixed: Prevent Host Header Injection (CVE-2019-12425) The 3 revisions listed above as "new" are entirely new to this repository and will be described in separate emails. The revisions listed as "add" were already present in the repository and have only been added to this reference. Summary of changes: .../src/main/java/org/apache/ofbiz/base/util/UtilMisc.java | 13 +++++++++++++ framework/security/config/security.properties | 6 +++++- .../org/apache/ofbiz/webapp/control/RequestHandler.java | 9 +++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) |
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 <[hidden email]> 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/> |
In reply to this post by jleroux@apache.org
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 9fa3dbe1eaaff766cf92c8f6f8e9913b0e8f5091 Author: Jacques Le Roux <[hidden email]> AuthorDate: Sun Apr 5 10:04:15 2020 +0200 Revert "Merge branch 'JacquesLeRoux-POC-for-CSRF-Token-OFBIZ-11306' into trunk" This reverts commit 0add8bedbca231ffd839eb733f1041ce5487e9d6. --- .../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, 263 insertions(+), 1344 deletions(-) diff --git a/applications/humanres/template/category/CategoryTree.ftl b/applications/humanres/template/category/CategoryTree.ftl index f14bbfc..10a08ac 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 + "&<@csrfTokenPair>viewprofile</@csrfTokenPair>"; + window.location = "viewprofile?partyId=" + id; } 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 8b56c55..c986f1e 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,14 +25,12 @@ 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; @@ -50,6 +48,7 @@ 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(); @@ -127,11 +126,6 @@ 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 dd4ca21..dce62c7 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" : "getChild", + "ajax" : { "url" : "<@ofbizUrl>getChild</@ofbizUrl>", "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 34fbb3b..d6b104c 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,8 +77,7 @@ public class CommonEvents { "thisRequestUri", "org.apache.tomcat.util.net.secure_protocol_version", "userLogin", - "impersonateLogin", - "requestMapMap" // requestMapMap is used by CSRFUtil + "impersonateLogin" }; /** 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 b2cd339..80407c6 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" csrf-token="false"/> + <security https="true" auth="true"/> <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" method="post"> + <request-map uri="SetTimeZoneFromBrowser"> <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 2a639c5..b9e0b2e 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,27 +149,9 @@ 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 deleted file mode 100644 index 5b72990..0000000 --- a/framework/security/src/main/java/org/apache/ofbiz/security/CsrfDefenseStrategy.java +++ /dev/null @@ -1,93 +0,0 @@ -/******************************************************************************* - * 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 deleted file mode 100644 index 9d400b8..0000000 --- a/framework/security/src/main/java/org/apache/ofbiz/security/CsrfUtil.java +++ /dev/null @@ -1,358 +0,0 @@ -/******************************************************************************* - * 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 deleted file mode 100644 index 322afb5..0000000 --- a/framework/security/src/main/java/org/apache/ofbiz/security/ICsrfDefenseStrategy.java +++ /dev/null @@ -1,55 +0,0 @@ -/******************************************************************************* - * 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 deleted file mode 100644 index 279310c..0000000 --- a/framework/security/src/main/java/org/apache/ofbiz/security/NoCsrfDefenseStrategy.java +++ /dev/null @@ -1,50 +0,0 @@ -/******************************************************************************* - * 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 deleted file mode 100644 index 53d0096..0000000 --- a/framework/security/src/test/java/org/apache/ofbiz/security/CsrfUtilTests.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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 f6a2f6b..535e48d 100644 --- a/framework/webapp/config/freemarkerTransforms.properties +++ b/framework/webapp/config/freemarkerTransforms.properties @@ -28,5 +28,3 @@ 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 01d0046..fc9a966 100644 --- a/framework/webapp/dtd/site-conf.xsd +++ b/framework/webapp/dtd/site-conf.xsd @@ -305,20 +305,6 @@ 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 e350b95..8181eb8 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,7 +51,6 @@ 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; @@ -466,7 +465,6 @@ 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; @@ -498,7 +496,6 @@ 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 2bb0aab..353b56b 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,7 +26,6 @@ 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; @@ -70,8 +69,6 @@ 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 b18fa8d..e1d1745 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,11 +18,12 @@ *******************************************************************************/ 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; @@ -32,20 +33,13 @@ 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; @@ -56,13 +50,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; @@ -81,6 +75,8 @@ 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; @@ -88,6 +84,66 @@ 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) { @@ -124,7 +180,7 @@ public class RequestHandler { } /** - * Finds a collection of request maps in {@code ccfg} matching {@code req}. + * Find 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 @@ -132,25 +188,22 @@ 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.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; - } + 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; } return rmaps != null ? rmaps : Collections.emptyList(); } @@ -179,33 +232,6 @@ 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 { @@ -216,7 +242,7 @@ public class RequestHandler { // Parse controller config. try { - ccfg = ConfigXMLReader.getControllerConfig(controllerConfigURL); + ccfg = new ControllerConfig(getControllerConfig()); } catch (WebAppConfigurationException e) { Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); throw new RequestHandlerException(e); @@ -241,27 +267,18 @@ 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) { - 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); - } + 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 = UtilHttp.getRequestMethod(request); + String method = request.getMethod(); RequestMap requestMap = resolveMethod(method, rmaps).orElseThrow(() -> { String msg = UtilProperties.getMessage("WebappUiLabels", "RequestMethodNotMatchConfig", UtilMisc.toList(requestUri, method), UtilHttp.getLocale(request)); @@ -280,7 +297,7 @@ public class RequestHandler { // Check for chained request. if (chain != null) { String chainRequestUri = RequestHandler.getRequestUri(chain); - requestMap = ccfg.getRequestMapMap().get(chainRequestUri); + requestMap = ccfg.getRequestMapMap().getFirst(chainRequestUri); if (requestMap == null) { throw new RequestHandlerException("Unknown chained request [" + chainRequestUri + "]; this request does not exist"); } @@ -304,11 +321,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().get(ccfg.getDefaultRequest()).securityDirectRequest) { + if (ccfg.getDefaultRequest() == null || !ccfg.getRequestMapMap().getFirst(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().get(ccfg.getDefaultRequest()); + requestMap = ccfg.getRequestMapMap().getFirst(ccfg.getDefaultRequest()); } } // Check if we SHOULD be secure and are not. @@ -350,7 +367,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.getStatusCode()); + callRedirect(newUrl, response, request, ccfg.getStatusCodeString()); return; } } @@ -358,7 +375,33 @@ public class RequestHandler { // Check for HTTPS client (x.509) security if (request.isSecure() && requestMap.securityCert) { - if (!checkCertificates(request, certs -> SSLUtil.isClientTrusted(certs, null))) { + 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) { Debug.logWarning(requestMissingErrorMessage, module); throw new RequestHandlerException(requestMissingErrorMessage); } @@ -429,20 +472,13 @@ 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().get("checkLogin").event; + ConfigXMLReader.Event checkLoginEvent = ccfg.getRequestMapMap().getFirst("checkLogin").event; String checkLoginReturnString = null; try { @@ -455,9 +491,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().get("checkLogin"); + requestMap = ccfg.getRequestMapMap().getFirst("checkLogin"); } else { - requestMap = ccfg.getRequestMapMap().get("ajaxCheckLogin"); + requestMap = ccfg.getRequestMapMap().getFirst("ajaxCheckLogin"); } } } @@ -466,7 +502,9 @@ 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); - previousParamMap.forEach(request::setAttribute); + for (Map.Entry<String, Object> previousParamEntry: previousParamMap.entrySet()) { + request.setAttribute(previousParamEntry.getKey(), previousParamEntry.getValue()); + } // to avoid this data being included again, now remove the _PREVIOUS_PARAM_MAP_ attribute request.getSession().removeAttribute("_PREVIOUS_PARAM_MAP_FORM_"); @@ -561,7 +599,6 @@ 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()); } @@ -587,13 +624,8 @@ public class RequestHandler { if (UtilValidate.isNotEmpty(queryString)) { redirectTarget += "?" + queryString; } - 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()); + + callRedirect(makeLink(request, response, redirectTarget), response, request, ccfg.getStatusCodeString()); return; } } @@ -651,44 +683,31 @@ public class RequestHandler { } } - // The status code used to redirect the HTTP client. - String redirectSC = UtilValidate.isNotEmpty(nextRequestResponse.statusCode) - ? nextRequestResponse.statusCode - : ccfg.getStatusCode(); - + String responseStatusCode = nextRequestResponse.statusCode; + if(UtilValidate.isNotEmpty(responseStatusCode)) + ccfg.setStatusCodeString(responseStatusCode); + 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, redirectSC); + callRedirect(nextRequestResponse.value, response, request, ccfg.getStatusCodeString()); } 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, redirectSC); + request, ccfg.getStatusCodeString()); } 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, redirectSC); + callRedirect(url + this.makeQueryString(request, nextRequestResponse), response, request, ccfg.getStatusCodeString()); } else if ("request-redirect".equals(nextRequestResponse.type)) { if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a Request redirect." + showSessionId(request), module); - 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); + callRedirect(makeLinkWithQueryString(request, response, "/" + nextRequestResponse.value, nextRequestResponse), response, request, ccfg.getStatusCodeString()); } 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); - 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); + callRedirect(makeLink(request, response, nextRequestResponse.value), response, request, ccfg.getStatusCodeString()); } else if ("view".equals(nextRequestResponse.type)) { if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a view." + showSessionId(request), module); @@ -705,13 +724,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.cast(session.getAttribute("_SAVED_VIEW_PARAMS_")); + urlParams = UtilGenerics.<String, Object>checkMap(session.getAttribute("_SAVED_VIEW_PARAMS_")); } else if (session.getAttribute("_HOME_VIEW_NAME_") != null) { viewName = (String) session.getAttribute("_HOME_VIEW_NAME_"); - urlParams = UtilGenerics.cast(session.getAttribute("_HOME_VIEW_PARAMS_")); + urlParams = UtilGenerics.<String, Object>checkMap(session.getAttribute("_HOME_VIEW_PARAMS_")); } else if (session.getAttribute("_LAST_VIEW_NAME_") != null) { viewName = (String) session.getAttribute("_LAST_VIEW_NAME_"); - urlParams = UtilGenerics.cast(session.getAttribute("_LAST_VIEW_PARAMS_")); + urlParams = UtilGenerics.<String, Object>checkMap(session.getAttribute("_LAST_VIEW_PARAMS_")); } else if (UtilValidate.isNotEmpty(nextRequestResponse.value)) { viewName = nextRequestResponse.value; } @@ -756,7 +775,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.cast(session.getAttribute("_HOME_VIEW_PARAMS_")); + urlParams = UtilGenerics.<String, Object>checkMap(session.getAttribute("_HOME_VIEW_PARAMS_")); } if (urlParams != null) { for (Map.Entry<String, Object> urlParamEntry: urlParams.entrySet()) { @@ -789,7 +808,7 @@ public class RequestHandler { try { String errorPageLocation = getControllerConfig().getErrorpage(); errorPage = FlexibleLocation.resolveLocation(errorPageLocation); - } catch (MalformedURLException e) { + } catch (WebAppConfigurationException | MalformedURLException e) { Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); } if (errorPage == null) { @@ -800,8 +819,14 @@ public class RequestHandler { /** Returns the default status-code for this request. */ public String getStatusCode(HttpServletRequest request) { - String statusCode = getControllerConfig().getStatusCode(); - return UtilValidate.isNotEmpty(statusCode) ? statusCode : null; + 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; } /** Returns the ViewFactory Object. */ @@ -846,11 +871,11 @@ public class RequestHandler { return nextPage; } - private static void callRedirect(String url, HttpServletResponse resp, HttpServletRequest req, String statusCodeString) throws RequestHandlerException { + private 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<>(); + Map<String, Object> reqAttrMap = new HashMap<String, Object>(); Integer statusCode; try { statusCode = Integer.valueOf(statusCodeString); @@ -943,7 +968,13 @@ public class RequestHandler { req.getSession().removeAttribute("_SAVED_VIEW_PARAMS_"); } - ConfigXMLReader.ViewMap viewMap = (view == null) ? null : getControllerConfig().getViewMapMap().get(view); + 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); + } if (viewMap == null) { throw new RequestHandlerException("No definition found for view with name [" + view + "]"); } @@ -1068,7 +1099,7 @@ public class RequestHandler { } } - private static void addNameValuePairToQueryString(StringBuilder queryString, String name, String value) { + private void addNameValuePairToQueryString(StringBuilder queryString, String name, String value) { if (UtilValidate.isNotEmpty(value)) { if (queryString.length() > 1) { queryString.append("&"); @@ -1093,10 +1124,6 @@ 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); @@ -1108,7 +1135,13 @@ public class RequestHandler { String requestUri = RequestHandler.getRequestUri(url); ConfigXMLReader.RequestMap requestMap = null; if (requestUri != null) { - requestMap = getControllerConfig().getRequestMapMap().get(requestUri); + 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; + } } boolean didFullSecure = false; boolean didFullStandard = false; @@ -1140,12 +1173,8 @@ public class RequestHandler { return null; } } - - String controlPath = targetControlPath; - if (UtilValidate.isEmpty(controlPath)){ - // create the path to the control servlet - controlPath = (String) request.getAttribute("_CONTROL_PATH_"); - } + // create the path to the control servlet + String controlPath = (String) request.getAttribute("_CONTROL_PATH_"); //If required by webSite parameter, surcharge control path if (webSiteProps.getWebappPath() != null) { @@ -1168,7 +1197,7 @@ public class RequestHandler { try { GenericValue webSiteValue = EntityQuery.use(delegator).from("WebSite").where("webSiteId", webSiteId).cache().queryOne(); if (webSiteValue != null) { - ServletContext application = (request.getServletContext()); + ServletContext application = ((ServletContext) request.getAttribute("servletContext")); String domainName = request.getLocalName(); if (application.getAttribute("MULTI_SITE_ENABLED") != null && UtilValidate.isNotEmpty(webSiteValue.getString("hostedPathAlias")) && !domainName.equals(webSiteValue.getString("httpHost"))) { newURL.append('/'); @@ -1199,9 +1228,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) { - RequestHandler rh = from(request); + 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_"); return rh.makeLink(request, response, url, fullPath, secure, encode); } @@ -1248,58 +1277,55 @@ public class RequestHandler { runEvents(req, resp, prod, "before-logout"); } - /** - * 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; + 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 { + 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) { - return track(request, trackVisit, rmap -> rmap.trackVisit); + 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; + } } - private static String showSessionId(HttpServletRequest request) { + private String showSessionId(HttpServletRequest request) { Delegator delegator = (Delegator) request.getAttribute("delegator"); boolean showSessionIdInLog = EntityUtilProperties.propertyValueEqualsIgnoreCase("requestHandler", "show-sessionId-in-log", "Y", delegator); if (showSessionIdInLog) { @@ -1307,42 +1333,4 @@ 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 deleted file mode 100644 index 6a2d89e..0000000 --- a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/CsrfTokenAjaxTransform.java +++ /dev/null @@ -1,75 +0,0 @@ -/******************************************************************************* - * 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 deleted file mode 100644 index d51bd61..0000000 --- a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/ftl/CsrfTokenPairNonAjaxTransform.java +++ /dev/null @@ -1,76 +0,0 @@ -/******************************************************************************* - * 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 567714f..fd822de 100644 --- a/framework/webtools/groovyScripts/entity/CheckDb.groovy +++ b/framework/webtools/groovyScripts/entity/CheckDb.groovy @@ -16,9 +16,10 @@ * 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_ @@ -113,7 +114,7 @@ if (security.hasPermission("ENTITY_MAINT", session)) { miter = messages.iterator() context.miters = miter } - context.checkDbURL = "view/checkdb" + context.encodeURLCheckDb = response.encodeURL(controlPath + "/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 279e448..17933db 100644 --- a/framework/webtools/groovyScripts/entity/EntityRef.groovy +++ b/framework/webtools/groovyScripts/entity/EntityRef.groovy @@ -16,8 +16,6 @@ * 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" @@ -31,9 +29,5 @@ 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 91cf8d3..ac81459 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" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" class="basic-form" method="post" action="${encodeURLCheckDb}"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -61,7 +61,7 @@ under the License. } </script> <h3>${uiLabelMap.WebtoolsRemoveAllTables}</h3> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TablesRemoveForm"> +<form class="basic-form" class="basic-form" method="post" action="${encodeURLCheckDb}" 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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TableRemoveForm"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}" 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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> +<form class="basic-form" method="post" action="${encodeURLCheckDb}"> <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 55e2387..1ace17f 100644 --- a/framework/webtools/template/entity/EntityRefList.ftl +++ b/framework/webtools/template/entity/EntityRefList.ftl @@ -1,4 +1,3 @@ - <#-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file @@ -55,9 +54,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</@ofbizUrl>#${packageName}" target="entityFrame">${packageName}</a><br /> + <a href="<@ofbizUrl>view/entityref_main?forstatic=true#${packageName}</@ofbizUrl>" target="entityFrame">${packageName}</a><br /> <#else> - <a href="<@ofbizUrl>view/entityref_main</@ofbizUrl>#${packageName}" target="entityFrame">${packageName}</a><br /> + <a href="<@ofbizUrl>view/entityref_main#${packageName}</@ofbizUrl>" target="entityFrame">${packageName}</a><br /> </#if> </#list> </#if> @@ -66,9 +65,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</@ofbizUrl>#${entity.entityName}" target="entityFrame">${entity.entityName}</a> + <a href="<@ofbizUrl>view/entityref_main?forstatic=true#${entity.entityName}</@ofbizUrl>" target="entityFrame">${entity.entityName}</a> <#else> - <a href="<@ofbizUrl>view/entityref_main</@ofbizUrl>#${entity.entityName}${entity.url!}" target="entityFrame">${entity.entityName}</a> + <a href="<@ofbizUrl>view/entityref_main#${entity.entityName}${entity.url!}</@ofbizUrl>" 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 32b7382..f6e03f7 100644 --- a/framework/webtools/template/entity/ViewGeneric.ftl +++ b/framework/webtools/template/entity/ViewGeneric.ftl @@ -38,7 +38,6 @@ 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 697ac90..08a92a0 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,7 +40,6 @@ 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; @@ -1410,10 +1409,6 @@ 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=\""); @@ -1444,9 +1439,7 @@ public final class MacroFormRenderer implements FormStringRenderer { sr.append(Integer.toString(viewSize)); sr.append("\" useRowSubmit="); sr.append(Boolean.toString(useRowSubmit)); - sr.append(" csrfNameValue=\""); - sr.append(csrfNameValue); - sr.append("\" />"); + sr.append(" />"); executeMacro(writer, sr.toString()); } @@ -2369,11 +2362,6 @@ 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 2c27eb7..7f1038b 100644 --- a/themes/bluelight/template/Header.ftl +++ b/themes/bluelight/template/Header.ftl @@ -28,10 +28,6 @@ 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/> @@ -198,7 +194,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('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> + <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> </#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 82c7ca7..647090f 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?newLocale=${availableLocale.toString()}</@ofbizUrl>"> + <a href="<@ofbizUrl>setSessionLocale</@ofbizUrl>?newLocale=${availableLocale.toString()}"> ${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 b371b19..cadd70e 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 csrfNameValue></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#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 a0d8b7e..c99efc9 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 csrfNameValue></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#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 77b3b67..0923033 100644 --- a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl @@ -243,14 +243,8 @@ under the License. </#macro> <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType name viewIndexField viewSizeField viewIndex viewSize targetWindow="" containerId="" containerStyle="" autocomplete="" useRowSubmit="" focusFieldName="" hasRequiredField="" csrfNameValue=""> +<#macro renderFormOpen linkUrl formType name viewIndexField viewSizeField viewIndex viewSize targetWindow="" containerId="" containerStyle="" autocomplete="" useRowSubmit="" focusFieldName="" hasRequiredField=""> <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 0e97938..228611e 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 csrfNameValue></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#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 0472f2d..0998073 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 csrfNameValue></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#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 acc2f28..b8cbc51 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 csrfNameValue></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#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 e42dc44..ce99998 100644 --- a/themes/common-theme/webapp/common/js/util/OfbizUtil.js +++ b/themes/common-theme/webapp/common/js/util/OfbizUtil.js @@ -25,16 +25,6 @@ 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. @@ -1228,7 +1218,7 @@ function getJSONuiLabels(requiredLabels, callback) { } } /** - * Read the required uiLabel from the uiLabelXml Resource + * Read the requiered 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 bbe4eb3..8920f07 100644 --- a/themes/flatgrey/template/Header.ftl +++ b/themes/flatgrey/template/Header.ftl @@ -24,10 +24,6 @@ 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/> @@ -160,7 +156,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('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);">${uiLabelMap.CommonHelp}</a></li> + <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> </#if> </ul> </li> diff --git a/themes/rainbowstone/template/includes/Header.ftl b/themes/rainbowstone/template/includes/Header.ftl index 93a8500..bb1ad5e 100644 --- a/themes/rainbowstone/template/includes/Header.ftl +++ b/themes/rainbowstone/template/includes/Header.ftl @@ -24,10 +24,6 @@ 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 c70a040..18f610a 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('<@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> + <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> </#if> <#include "component://rainbowstone/template/includes/Avatar.ftl"/> diff --git a/themes/tomahawk/template/AppBarClose.ftl b/themes/tomahawk/template/AppBarClose.ftl index 811bac5..af35581 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('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> + <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> </#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 d01ae9c..3376614 100644 --- a/themes/tomahawk/template/Header.ftl +++ b/themes/tomahawk/template/Header.ftl @@ -28,10 +28,6 @@ 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/> |
In reply to this post by jleroux@apache.org
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 5530e23738e52d9db35f19bd4d97530bc0a0d762 Author: Jacques Le Roux <[hidden email]> AuthorDate: Sat Apr 4 19:32:02 2020 +0200 Fixed: Prevent Host Header Injection (CVE-2019-12425) (OFBIZ-11583) --- .../src/main/java/org/apache/ofbiz/base/util/UtilMisc.java | 13 +++++++++++++ framework/security/config/security.properties | 6 +++++- .../org/apache/ofbiz/webapp/control/RequestHandler.java | 9 +++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilMisc.java b/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilMisc.java index 7f76944..b6985a6 100644 --- a/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilMisc.java +++ b/framework/base/src/main/java/org/apache/ofbiz/base/util/UtilMisc.java @@ -615,6 +615,19 @@ public final class UtilMisc { return LocaleHolder.availableLocaleList; } + /** List of domains or IP addresses to be checked to prevent Host Header Injection, + * no spaces after commas,no wildcard, can be extended of course... + * @return List of domains or IP addresses to be checked to prevent Host Header Injection, + */ + public static List<String> getHostHeadersAllowed() { + String hostHeadersAllowedString = UtilProperties.getPropertyValue("security", "host-headers-allowed", "localhost"); + List<String> hostHeadersAllowed = null; + if (UtilValidate.isNotEmpty(hostHeadersAllowedString)) { + hostHeadersAllowed = StringUtil.split(hostHeadersAllowedString, ","); + } + return Collections.unmodifiableList(hostHeadersAllowed); + } + /** @deprecated use Thread.sleep() */ @Deprecated public static void staticWait(long timeout) throws InterruptedException { diff --git a/framework/security/config/security.properties b/framework/security/config/security.properties index b9e0b2e..f5d3120 100644 --- a/framework/security/config/security.properties +++ b/framework/security/config/security.properties @@ -152,6 +152,10 @@ 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 security.token.key=security.token.key -# -- By default the SameSite value in SameSiteFilter is strict. This allows to change it ot lax if needed +# -- List of domains or IP addresses to be checked to prevent Host Header Injection, +# -- no spaces after commas,no wildcard, can be extended of course... +host-headers-allowed=localhost,127.0.0.1,demo-trunk.ofbiz.apache.org,demo-stable.ofbiz.apache.org,demo-old.ofbiz.apache.org + +# -- By default the SameSite value in SameSiteFilter is strict. This allows to change it to lax if needed SameSiteCookieAttribute= 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..a310e52 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 @@ -82,6 +82,7 @@ public class RequestHandler { private final URL controllerConfigURL; private final boolean trackServerHit; private final boolean trackVisit; + private final List hostHeadersAllowed; private ControllerConfig ccfg; static class ControllerConfig { @@ -167,6 +168,9 @@ public class RequestHandler { this.trackServerHit = !"false".equalsIgnoreCase(context.getInitParameter("track-serverhit")); this.trackVisit = !"false".equalsIgnoreCase(context.getInitParameter("track-visit")); + + hostHeadersAllowed = UtilMisc.getHostHeadersAllowed(); + } public ConfigXMLReader.ControllerConfig getControllerConfig() { @@ -235,6 +239,11 @@ public class RequestHandler { public void doRequest(HttpServletRequest request, HttpServletResponse response, String chain, GenericValue userLogin, Delegator delegator) throws RequestHandlerException, RequestHandlerExceptionAllowExternalRequests { + if (!hostHeadersAllowed.contains(request.getServerName())) { + Debug.logError("Domain " + request.getServerName() + " not accepted to prevent host header injection ", module); + throw new RequestHandlerException("Domain " + request.getServerName() + " not accepted to prevent host header injection "); + } + final boolean throwRequestHandlerExceptionOnMissingLocalRequest = EntityUtilProperties.propertyValueEqualsIgnoreCase( "requestHandler", "throwRequestHandlerExceptionOnMissingLocalRequest", "Y", delegator); long startTime = System.currentTimeMillis(); |
Free forum by Nabble | Edit this page |