This is an automated email from the ASF dual-hosted git repository.
jleroux pushed a change to branch release17.12 in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git. from cf936d6 Fixed: The createTaskContent request does not work new 27e5752 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 84e102a Revert "Merge branch 'JacquesLeRoux-POC-for-CSRF-Token-OFBIZ-11306' into trunk" new 793628b 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 | 10 ++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) |
This is an automated email from the ASF dual-hosted git repository.
jleroux pushed a commit to branch release17.12 in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git commit 27e57522b15d71352c61919befc6eb451ed4e864 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 Much Conflicts, but that should be OK --- .../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 +- framework/security/config/security.properties | 32 +- .../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/ControlEventListener.java | 3 + .../ofbiz/webapp/control/RequestHandler.java | 633 ++++++++++++--------- .../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 | 399 ++++++++++--- .../webtools/template/entity/EntityRefList.ftl | 9 +- framework/webtools/template/entity/ViewGeneric.ftl | 1 + themes/bluelight/template/Header.ftl | 6 +- themes/common/template/includes/ListLocales.ftl | 2 +- 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 + 28 files changed, 1740 insertions(+), 392 deletions(-) diff --git a/applications/humanres/template/category/CategoryTree.ftl b/applications/humanres/template/category/CategoryTree.ftl index b7ee8ee..e43f16f 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 eb685cc..19cafae 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,6 +25,7 @@ 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.UtilMisc; import org.apache.ofbiz.base.util.UtilValidate; @@ -32,6 +33,7 @@ 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; @@ -49,7 +51,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(); @@ -127,6 +128,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 e333fef..1a58d32 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 835de82..db1341e 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 @@ -76,7 +76,8 @@ public class CommonEvents { "thisRequestUri", "org.apache.tomcat.util.net.secure_protocol_version", "userLogin", - "impersonateLogin" + "impersonateLogin", + "requestMapMap" // requestMapMap is used by CSRFUtil }; public static String setFollowerPage(HttpServletRequest request, HttpServletResponse response) { diff --git a/framework/security/config/security.properties b/framework/security/config/security.properties index 5b809ff..c1b9243 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 @@ -135,6 +135,34 @@ security.login.externalLoginKey.enabled=true # -- Security key used to encrypt and decrypt the autogenerated password in forgot password functionality. login.secret_key_string=Secret Key -# -- By default the SameSite value in SameSiteFilter is strict. This allows to change it ot lax if needed +# -- Time To Live of the token send to the external server in seconds, 10 seconds seems plenty enough OOTB. Custom projects might want set a lower value. +security.jwt.token.expireTime=1800 + +# -- Enables the internal Single Sign On feature which allows a token based login between OFBiz instances +# -- 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 +security.token.key=security.token.key + +# -- By default the SameSite value in SameSiteFilter is strict. This allows to change it to 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 1541aee..bbb9b7c 100644 --- a/framework/webapp/dtd/site-conf.xsd +++ b/framework/webapp/dtd/site-conf.xsd @@ -348,6 +348,20 @@ under the License. </xs:restriction> </xs:simpleType> </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/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 6802da1..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,25 +18,34 @@ *******************************************************************************/ 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; +import java.util.Collections; import java.util.Enumeration; 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; @@ -53,6 +62,8 @@ 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; import org.apache.ofbiz.webapp.event.EventHandlerException; @@ -70,12 +81,12 @@ import org.apache.ofbiz.widget.model.ThemeFactory; public class RequestHandler { public static final String module = RequestHandler.class.getName(); - private final String defaultStatusCodeString = UtilProperties.getPropertyValue("requestHandler", "status-code", "302"); private final ViewFactory viewFactory; private final EventFactory eventFactory; private final URL controllerConfigURL; private final boolean trackServerHit; private final boolean trackVisit; + private ControllerConfig ccfg; public static RequestHandler getRequestHandler(ServletContext servletContext) { RequestHandler rh = (RequestHandler) servletContext.getAttribute("_REQUEST_HANDLER_"); @@ -112,11 +123,87 @@ public class RequestHandler { return null; } - public void doRequest(HttpServletRequest request, HttpServletResponse response, String requestUri) throws RequestHandlerException, RequestHandlerExceptionAllowExternalRequests { - HttpSession session = request.getSession(); - Delegator delegator = (Delegator) request.getAttribute("delegator"); - GenericValue userLogin = (GenericValue) session.getAttribute("userLogin"); - doRequest(request, response, requestUri, userLogin, delegator); + /** + * 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 + * @param req The HTTP request to match + * @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; + } + } + return rmaps != null ? rmaps : Collections.emptyList(); + } + + /** + * Find the request map matching {@code method}. + * Otherwise fall back to the one matching the "all" and "" special methods + * in that respective order. + * + * @param method the HTTP method to match + * @param rmaps the collection of request map candidates + * @return a request map {@code Optional} + */ + static Optional<RequestMap> resolveMethod(String method, Collection<RequestMap> rmaps) { + for (RequestMap map : rmaps) { + if (map.method.equalsIgnoreCase(method)) { + return Optional.of(map); + } + } + if (method.isEmpty()) { + return Optional.empty(); + } else if (method.equals("all")) { + return resolveMethod("", rmaps); + } else { + return resolveMethod("all", rmaps); + } + } + + /** + * 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, @@ -127,20 +214,13 @@ public class RequestHandler { long startTime = System.currentTimeMillis(); HttpSession session = request.getSession(); - // get the controllerConfig once for this method so we don't have to get it over and over inside the method - ConfigXMLReader.ControllerConfig controllerConfig = this.getControllerConfig(); - Map<String, ConfigXMLReader.RequestMap> requestMapMap = null; - String statusCodeString = null; + // Parse controller config. try { - requestMapMap = controllerConfig.getRequestMapMap(); - statusCodeString = controllerConfig.getStatusCode(); + ccfg = ConfigXMLReader.getControllerConfig(controllerConfigURL); } catch (WebAppConfigurationException e) { Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); throw new RequestHandlerException(e); } - if (UtilValidate.isEmpty(statusCodeString)) { - statusCodeString = defaultStatusCodeString; - } // workaround if we are in the root webapp String cname = UtilHttp.getApplicationName(request); @@ -155,50 +235,38 @@ public class RequestHandler { } } - String overrideViewUri = RequestHandler.getOverrideViewUri(request.getPathInfo()); + String requestMissingErrorMessage = "Unknown request [" + + defaultRequestUri + + "]; this request does not exist or cannot be called directly."; - String requestMissingErrorMessage = "Unknown request [" + defaultRequestUri + "]; this request does not exist or cannot be called directly."; - ConfigXMLReader.RequestMap requestMap = null; - if (defaultRequestUri != null) { - requestMap = requestMapMap.get(defaultRequestUri); - } - // check for default request - if (requestMap == null) { - String defaultRequest; - try { - defaultRequest = controllerConfig.getDefaultRequest(); - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - throw new RequestHandlerException(e); - } - if (defaultRequest != null) { // required! to avoid a null pointer exception and generate a requesthandler exception if default request not found. - requestMap = requestMapMap.get(defaultRequest); - } - } + String path = request.getPathInfo(); + String requestUri = getRequestUri(path); - // check for override view - if (overrideViewUri != null) { - ConfigXMLReader.ViewMap viewMap; - try { - viewMap = getControllerConfig().getViewMapMap().get(overrideViewUri); - if (viewMap == null) { - String defaultRequest = controllerConfig.getDefaultRequest(); - if (defaultRequest != null) { // required! to avoid a null pointer exception and generate a requesthandler exception if default request not found. - requestMap = requestMapMap.get(defaultRequest); - } + 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); } - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - throw new RequestHandlerException(e); + } else { + throw new RequestHandlerExceptionAllowExternalRequests(); } } + // The "overriddenView" attribute is set by resolveURI when necessary. + String overrideViewUri = (String) request.getAttribute("overriddenView"); - // if no matching request is found in the controller, depending on throwRequestHandlerExceptionOnMissingLocalRequest - // we throw a RequestHandlerException or RequestHandlerExceptionAllowExternalRequests - if (requestMap == null) { - if (throwRequestHandlerExceptionOnMissingLocalRequest) throw new RequestHandlerException(requestMissingErrorMessage); - else throw new RequestHandlerExceptionAllowExternalRequests(); - } + String method = UtilHttp.getRequestMethod(request); + RequestMap requestMap = resolveMethod(method, rmaps).orElseThrow(() -> { + String msg = UtilProperties.getMessage("WebappUiLabels", "RequestMethodNotMatchConfig", + UtilMisc.toList(requestUri, method), UtilHttp.getLocale(request)); + return new MethodNotAllowedException(msg); + }); String eventReturn = null; if (requestMap.metrics != null && requestMap.metrics.getThreshold() != 0.0 && requestMap.metrics.getTotalEvents() > 3 && requestMap.metrics.getThreshold() < requestMap.metrics.getServiceRate()) { @@ -212,7 +280,7 @@ public class RequestHandler { // Check for chained request. if (chain != null) { String chainRequestUri = RequestHandler.getRequestUri(chain); - requestMap = requestMapMap.get(chainRequestUri); + requestMap = ccfg.getRequestMapMap().get(chainRequestUri); if (requestMap == null) { throw new RequestHandlerException("Unknown chained request [" + chainRequestUri + "]; this request does not exist"); } @@ -236,24 +304,16 @@ 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) { - String defaultRequest; - try { - defaultRequest = controllerConfig.getDefaultRequest(); - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - throw new RequestHandlerException(e); - } - if (defaultRequest == null || !requestMapMap.get(defaultRequest).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 = requestMapMap.get(defaultRequest); + requestMap = ccfg.getRequestMapMap().get(ccfg.getDefaultRequest()); } } // Check if we SHOULD be secure and are not. - String forwardedProto = request.getHeader("X-Forwarded-Proto"); - boolean isForwardedSecure = UtilValidate.isNotEmpty(forwardedProto) && "HTTPS".equals(forwardedProto.toUpperCase()); - if ((!request.isSecure() && !isForwardedSecure) && requestMap.securityHttps) { + boolean forwardedHTTPS = "HTTPS".equalsIgnoreCase(request.getHeader("X-Forwarded-Proto")); + if (!request.isSecure() && !forwardedHTTPS && requestMap.securityHttps) { // If the request method was POST then return an error to avoid problems with XSRF where the request may have come from another machine/program and had the same session ID but was not encrypted as it should have been (we used to let it pass to not lose data since it was too late to protect that data anyway) if ("POST".equalsIgnoreCase(request.getMethod())) { // we can't redirect with the body parameters, and for better security from XSRF, just return an error message @@ -290,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, statusCodeString); + callRedirect(newUrl, response, request, ccfg.getStatusCode()); return; } } @@ -298,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); } @@ -332,66 +366,56 @@ public class RequestHandler { // If its the first visit run the first visit events. if (this.trackVisit(request) && session.getAttribute("_FIRST_VISIT_EVENTS_") == null) { - if (Debug.infoOn()) + if (Debug.infoOn()) { Debug.logInfo("This is the first request in this visit." + showSessionId(request), module); + } session.setAttribute("_FIRST_VISIT_EVENTS_", "complete"); - try { - for (ConfigXMLReader.Event event: controllerConfig.getFirstVisitEventList().values()) { - try { - String returnString = this.runEvent(request, response, event, null, "firstvisit"); - if (returnString == null || "none".equalsIgnoreCase(returnString)) { - interruptRequest = true; - } else if (!"success".equalsIgnoreCase(returnString)) { - throw new EventHandlerException("First-Visit event did not return 'success'."); - } - } catch (EventHandlerException e) { - Debug.logError(e, module); + for (ConfigXMLReader.Event event: ccfg.getFirstVisitEventList().values()) { + try { + String returnString = this.runEvent(request, response, event, null, "firstvisit"); + if (returnString == null || "none".equalsIgnoreCase(returnString)) { + interruptRequest = true; + } else if (!"success".equalsIgnoreCase(returnString)) { + throw new EventHandlerException("First-Visit event did not return 'success'."); } + } catch (EventHandlerException e) { + Debug.logError(e, module); } - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - throw new RequestHandlerException(e); } } // Invoke the pre-processor (but NOT in a chain) - try { - for (ConfigXMLReader.Event event: controllerConfig.getPreprocessorEventList().values()) { - try { - String returnString = this.runEvent(request, response, event, null, "preprocessor"); - if (returnString == null || "none".equalsIgnoreCase(returnString)) { - interruptRequest = true; - } else if (!"success".equalsIgnoreCase(returnString)) { - if (!returnString.contains(":_protect_:")) { - throw new EventHandlerException("Pre-Processor event [" + event.invoke + "] did not return 'success'."); - } else { // protect the view normally rendered and redirect to error response view - returnString = returnString.replace(":_protect_:", ""); - if (returnString.length() > 0) { - request.setAttribute("_ERROR_MESSAGE_", returnString); - } - eventReturn = null; - // check to see if there is a "protect" response, if so it's ok else show the default_error_response_view - if (!requestMap.requestResponseMap.containsKey("protect")) { - String protectView = controllerConfig.getProtectView(); - if (protectView != null) { - overrideViewUri = protectView; - } else { - overrideViewUri = EntityUtilProperties.getPropertyValue("security", "default.error.response.view", delegator); - overrideViewUri = overrideViewUri.replace("view:", ""); - if ("none:".equals(overrideViewUri)) { - interruptRequest = true; - } + for (ConfigXMLReader.Event event: ccfg.getPreprocessorEventList().values()) { + try { + String returnString = this.runEvent(request, response, event, null, "preprocessor"); + if (returnString == null || "none".equalsIgnoreCase(returnString)) { + interruptRequest = true; + } else if (!"success".equalsIgnoreCase(returnString)) { + if (!returnString.contains(":_protect_:")) { + throw new EventHandlerException("Pre-Processor event [" + event.invoke + "] did not return 'success'."); + } else { // protect the view normally rendered and redirect to error response view + returnString = returnString.replace(":_protect_:", ""); + if (returnString.length() > 0) { + request.setAttribute("_ERROR_MESSAGE_", returnString); + } + eventReturn = null; + // check to see if there is a "protect" response, if so it's ok else show the default_error_response_view + if (!requestMap.requestResponseMap.containsKey("protect")) { + if (ccfg.getProtectView() != null) { + overrideViewUri = ccfg.getProtectView(); + } else { + overrideViewUri = EntityUtilProperties.getPropertyValue("security", "default.error.response.view", delegator); + overrideViewUri = overrideViewUri.replace("view:", ""); + if ("none:".equals(overrideViewUri)) { + interruptRequest = true; } } } } - } catch (EventHandlerException e) { - Debug.logError(e, module); } + } catch (EventHandlerException e) { + Debug.logError(e, module); } - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - throw new RequestHandlerException(e); } } @@ -405,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 = requestMapMap.get("checkLogin").event; + ConfigXMLReader.Event checkLoginEvent = ccfg.getRequestMapMap().get("checkLogin").event; String checkLoginReturnString = null; try { @@ -424,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 = requestMapMap.get("checkLogin"); + requestMap = ccfg.getRequestMapMap().get("checkLogin"); } else { - requestMap = requestMapMap.get("ajaxCheckLogin"); + requestMap = ccfg.getRequestMapMap().get("ajaxCheckLogin"); } } } @@ -435,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_"); @@ -532,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()); } @@ -557,8 +587,13 @@ public class RequestHandler { if (UtilValidate.isNotEmpty(queryString)) { redirectTarget += "?" + queryString; } - - callRedirect(makeLink(request, response, redirectTarget), response, request, statusCodeString); + 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; } } @@ -605,41 +640,55 @@ public class RequestHandler { // ======== handle views ======== // first invoke the post-processor events. - try { - for (ConfigXMLReader.Event event: controllerConfig.getPostprocessorEventList().values()) { - try { - String returnString = this.runEvent(request, response, event, requestMap, "postprocessor"); - if (returnString != null && !"success".equalsIgnoreCase(returnString)) { - throw new EventHandlerException("Post-Processor event did not return 'success'."); - } - } catch (EventHandlerException e) { - Debug.logError(e, module); + for (ConfigXMLReader.Event event: ccfg.getPostprocessorEventList().values()) { + try { + String returnString = this.runEvent(request, response, event, requestMap, "postprocessor"); + if (returnString != null && !"success".equalsIgnoreCase(returnString)) { + throw new EventHandlerException("Post-Processor event did not return 'success'."); } + } catch (EventHandlerException e) { + Debug.logError(e, module); } - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); - throw new RequestHandlerException(e); } - String responseStatusCode = nextRequestResponse.statusCode; - if(UtilValidate.isNotEmpty(responseStatusCode)) - statusCodeString = 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, statusCodeString); + 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, 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, statusCodeString); + 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, statusCodeString); + 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, statusCodeString); + 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); @@ -656,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; } @@ -707,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()) { @@ -740,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) { @@ -751,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. */ @@ -803,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); @@ -900,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 + "]"); } @@ -1031,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("&"); @@ -1056,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); @@ -1067,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; @@ -1105,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) { @@ -1129,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('/'); @@ -1160,95 +1199,107 @@ 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); } - public void runAfterLoginEvents(HttpServletRequest request, HttpServletResponse response) { + @FunctionalInterface + private interface EventCollectionProducer { + Collection<ConfigXMLReader.Event> get() throws WebAppConfigurationException; + } + + private void runEvents(HttpServletRequest req, HttpServletResponse res, + EventCollectionProducer prod, String trigger) { try { - for (ConfigXMLReader.Event event: getControllerConfig().getAfterLoginEventList().values()) { - try { - String returnString = this.runEvent(request, response, event, null, "after-login"); - if (returnString != null && !"success".equalsIgnoreCase(returnString)) { - throw new EventHandlerException("Pre-Processor event did not return 'success'."); - } - } catch (EventHandlerException e) { - Debug.logError(e, module); + for (ConfigXMLReader.Event event: prod.get()) { + String ret = runEvent(req, res, event, null, trigger); + if (ret != null && !"success".equalsIgnoreCase(ret)) { + throw new EventHandlerException("Pre-Processor event did not return 'success'."); } } + } catch (EventHandlerException e) { + Debug.logError(e, module); } catch (WebAppConfigurationException e) { Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); } } - public void runBeforeLogoutEvents(HttpServletRequest request, HttpServletResponse response) { - try { - for (ConfigXMLReader.Event event: getControllerConfig().getBeforeLogoutEventList().values()) { - try { - String returnString = this.runEvent(request, response, event, null, "before-logout"); - if (returnString != null && !"success".equalsIgnoreCase(returnString)) { - throw new EventHandlerException("Pre-Processor event did not return 'success'."); - } - } catch (EventHandlerException e) { - Debug.logError(e, module); - } + /** + * Run all the "after-login" Web events defined in the controller configuration. + * + * @param req the request to run the events with + * @param resp the response to run the events with + */ + public void runAfterLoginEvents(HttpServletRequest req, HttpServletResponse resp) { + EventCollectionProducer prod = () -> getControllerConfig().getAfterLoginEventList().values(); + runEvents(req, resp, prod, "after-login"); + } + + /** + * Run all the "before-logout" Web events defined in the controller configuration. + * + * @param req the request to run the events with + * @param resp the response to run the events with + */ + public void runBeforeLogoutEvents(HttpServletRequest req, HttpServletResponse resp) { + EventCollectionProducer prod = () -> getControllerConfig().getBeforeLogoutEventList().values(); + 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; } - } catch (WebAppConfigurationException e) { - Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); } + 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) { - 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 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) { @@ -1256,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 dba2450..6366fb9 100644 --- a/framework/webtools/template/entity/CheckDb.ftl +++ b/framework/webtools/template/entity/CheckDb.ftl @@ -16,100 +16,311 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> - <h3>${uiLabelMap.WebtoolsCheckUpdateDatabase}</h3> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="checkupdatetables"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <label> <input type="checkbox" name="checkPks" value="true" checked="checked"/> ${uiLabelMap.WebtoolsPks}</label> - <label> <input type="checkbox" name="checkFks" value="true"/> ${uiLabelMap.WebtoolsFks}</label> - <label> <input type="checkbox" name="checkFkIdx" value="true"/> ${uiLabelMap.WebtoolsFkIdx}</label> - <label> <input type="checkbox" name="addMissing" value="true"/> ${uiLabelMap.WebtoolsAddMissing}</label> - <label> <input type="checkbox" name="repair" value="true"/> ${uiLabelMap.WebtoolsRepairColumnSizes}</label> - <input type="submit" value="${uiLabelMap.WebtoolsCheckUpdateDatabase}"/> - </form> - <p>${uiLabelMap.WebtoolsNoteUseAtYourOwnRisk}</p> - <script language="JavaScript" type="text/javascript"> - function enableTablesRemove() { - document.forms["TablesRemoveForm"].elements["TablesRemoveButton"].disabled=false; - } - </script> - <h3>${uiLabelMap.WebtoolsRemoveAllTables}</h3> - <form method="post" action="${encodeURLCheckDb}" name="TablesRemoveForm"> - <input type="hidden" name="option" value="removetables"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}" name="TablesRemoveButton" disabled="disabled"/> - <input type="button" value="${uiLabelMap.WebtoolsEnable}" onclick="enableTablesRemove();"/> - </form> - <form method="post" action="${encodeURLCheckDb}" name="TableRemoveForm"> - <input type="hidden" name="option" value="removetable"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="20"/> - ${uiLabelMap.WebtoolsEntityName}: <input type="text" name="entityName" value="${entityName}" size="20"/> - <input type="submit" value="${uiLabelMap.CommonRemove}" name="TablesRemoveButton"/> - </form> - <h3>${uiLabelMap.WebtoolsCreateRemoveAllPrimaryKeys}</h3> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="createpks"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </form> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="removepks"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </form> - <h3>${uiLabelMap.WebtoolsCreateRemovePrimaryKey}</h3> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="createpk"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="20"/> - ${uiLabelMap.WebtoolsEntityName}: <input type="text" name="entityName" value="${entityName}" size="20"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </form> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="removepk"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="20"/> - ${uiLabelMap.WebtoolsEntityName}: <input type="text" name="entityName" value="${entityName}" size="20"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </form> - <h3>${uiLabelMap.WebtoolsCreateRemoveAllDeclaredIndices}</h3> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="createidx"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </form> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="removeidx"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </form> - <h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeyIndices}</h3> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="createfkidxs"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </form> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="removefkidxs"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </form> - <h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeys}</h3> - <p>${uiLabelMap.WebtoolsNoteForeighKeysMayAlsoBeCreated}</p> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="createfks"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </form> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="removefks"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </form> - <h3>${uiLabelMap.WebtoolsUpdateCharacterSetAndCollate}</h3> - <form method="post" action="${encodeURLCheckDb}"> - <input type="hidden" name="option" value="updateCharsetCollate"/> - ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonUpdate}"/> - </form> +<h3>${uiLabelMap.WebtoolsCheckUpdateDatabase}</h3> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <td> + <input type="hidden" name="option" value="checkupdatetables"/> + </td> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}: </label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + </td> + </tr> + <tr> + <td class="label"> + </td> + <td> + <label> <input type="checkbox" name="checkPks" value="true" checked="checked"/> ${uiLabelMap.WebtoolsPks}</label> + <label> <input type="checkbox" name="checkFks" value="true"/> ${uiLabelMap.WebtoolsFks}</label> + <label> <input type="checkbox" name="checkFkIdx" value="true"/> ${uiLabelMap.WebtoolsFkIdx}</label> + <label> <input type="checkbox" name="addMissing" value="true"/> ${uiLabelMap.WebtoolsAddMissing}</label> + <label> <input type="checkbox" name="repair" value="true"/> ${uiLabelMap.WebtoolsRepairColumnSizes}</label> + </td> + </tr> + <tr> + <td class="label"> + </td> + <td> + <input type="submit" value="${uiLabelMap.WebtoolsCheckUpdateDatabase}"/> + </td> + </tr> + </tbody> + </table> +</form> +<p>${uiLabelMap.WebtoolsNoteUseAtYourOwnRisk}</p> +<script type="application/javascript"> + function enableTablesRemove() { + document.forms["TablesRemoveForm"].elements["TablesRemoveButton"].disabled=false; + } +</script> +<h3>${uiLabelMap.WebtoolsRemoveAllTables}</h3> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TablesRemoveForm"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="removetables"/> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}" name="TablesRemoveButton" disabled="disabled"/> + <input type="button" value="${uiLabelMap.WebtoolsEnable}" onclick="enableTablesRemove();"/> + </td> + </tr> + </tbody> + </table> +</form> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TableRemoveForm"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="removetable"/> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="20"/> + </td> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsEntityName}:</label> + </td> + <td> + <input type="text" name="entityName" value="${entityName}" size="20"/> + <input type="submit" value="${uiLabelMap.CommonRemove}" name="TablesRemoveButton"/> + </td> + </tr> + </tbody> + </table> +</form> +<h3>${uiLabelMap.WebtoolsCreateRemoveAllPrimaryKeys}</h3> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="createpks"/> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </td> + </tr> + </tbody> + </table> +</form> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="removepks"/> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </tr> + </tbody> + </table> +</form> +<h3>${uiLabelMap.WebtoolsCreateRemovePrimaryKey}</h3> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="createpk"/> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + </td> + </tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsEntityName}:</label> + </td> + <td> + <input type="text" name="entityName" value="${entityName}" size="20"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </td> + </tr> + </tbody> + </table> +</form> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <td> + <input type="hidden" name="option" value="removepk"/> + </td> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="20"/> + </td> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsEntityName}:</label> + </td> + <td> + <input type="text" name="entityName" value="${entityName}" size="20"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </td> + </tr> + </tbody> + </table> +</form> +<h3>${uiLabelMap.WebtoolsCreateRemoveAllDeclaredIndices}</h3> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="createidx"/> + </tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </td> + </tr> + </tbody> + </table> +</form> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="removeidx"/> + </tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </td> + </tr> + </tbody> + </table> +</form> +<h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeyIndices}</h3> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="createfkidxs"/> + </tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </td> + </tr> + </tbody> + </table> +</form> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="removefkidxs"/> + </tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </td> + </tr> + </tbody> + </table> +</form> +<h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeys}</h3> +<p>${uiLabelMap.WebtoolsNoteForeighKeysMayAlsoBeCreated}</p> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="createfks"/> + </tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </td> + </tr> + </tbody> + </table> +</form> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="removefks"/> + </tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </td> + </tr> + </tbody> + </table> +</form> +<h3>${uiLabelMap.WebtoolsUpdateCharacterSetAndCollate}</h3> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> + <table class="basic-table" cellspacing="0"> + <tbody> + <tr> + <input type="hidden" name="option" value="updateCharsetCollate"/> + </tr> + <tr> + <td class="label"> + <label>${uiLabelMap.WebtoolsGroupName}:</label> + </td> + <td> + <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonUpdate}"/> + </td + </tr> + </tbody> + </table> +</form> <#if miters?has_content> <hr /> <ul> 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 c19a3ba..681f91c 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/themes/bluelight/template/Header.ftl b/themes/bluelight/template/Header.ftl index df2bca1..0ef8b95 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/> @@ -193,7 +197,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/template/includes/ListLocales.ftl b/themes/common/template/includes/ListLocales.ftl index 647090f..82c7ca7 100644 --- a/themes/common/template/includes/ListLocales.ftl +++ b/themes/common/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/flatgrey/template/Header.ftl b/themes/flatgrey/template/Header.ftl index effe4d2..fc9cdf0 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/> @@ -155,7 +159,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 2afc7a7..3e6d53b 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 ac0f215..4714c88 100644 --- a/themes/rainbowstone/template/includes/TopAppBar.ftl +++ b/themes/rainbowstone/template/includes/TopAppBar.ftl @@ -214,7 +214,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 7afd8c7..ebb1ed3 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 6bbd01e..de5fae2 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 release17.12 in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git commit 84e102a6a12f64687c54fe2635681b38ee8555a7 Author: Jacques Le Roux <[hidden email]> AuthorDate: Sun Apr 5 10:05:16 2020 +0200 Revert "Merge branch 'JacquesLeRoux-POC-for-CSRF-Token-OFBIZ-11306' into trunk" This reverts commit 27e57522b15d71352c61919befc6eb451ed4e864. --- .../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 +- framework/security/config/security.properties | 32 +- .../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/ControlEventListener.java | 3 - .../ofbiz/webapp/control/RequestHandler.java | 633 +++++++++------------ .../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 | 399 +++---------- .../webtools/template/entity/EntityRefList.ftl | 9 +- framework/webtools/template/entity/ViewGeneric.ftl | 1 - themes/bluelight/template/Header.ftl | 6 +- themes/common/template/includes/ListLocales.ftl | 2 +- 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 - 28 files changed, 392 insertions(+), 1740 deletions(-) diff --git a/applications/humanres/template/category/CategoryTree.ftl b/applications/humanres/template/category/CategoryTree.ftl index e43f16f..b7ee8ee 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 19cafae..eb685cc 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,7 +25,6 @@ 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.UtilMisc; import org.apache.ofbiz.base.util.UtilValidate; @@ -33,7 +32,6 @@ 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; @@ -51,6 +49,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(); @@ -128,11 +127,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 1a58d32..e333fef 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 db1341e..835de82 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 @@ -76,8 +76,7 @@ public class CommonEvents { "thisRequestUri", "org.apache.tomcat.util.net.secure_protocol_version", "userLogin", - "impersonateLogin", - "requestMapMap" // requestMapMap is used by CSRFUtil + "impersonateLogin" }; public static String setFollowerPage(HttpServletRequest request, HttpServletResponse response) { diff --git a/framework/security/config/security.properties b/framework/security/config/security.properties index c1b9243..5b809ff 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 @@ -135,34 +135,6 @@ security.login.externalLoginKey.enabled=true # -- Security key used to encrypt and decrypt the autogenerated password in forgot password functionality. login.secret_key_string=Secret Key -# -- Time To Live of the token send to the external server in seconds, 10 seconds seems plenty enough OOTB. Custom projects might want set a lower value. -security.jwt.token.expireTime=1800 - -# -- Enables the internal Single Sign On feature which allows a token based login between OFBiz instances -# -- 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 -security.token.key=security.token.key - -# -- By default the SameSite value in SameSiteFilter is strict. This allows to change it to lax if needed +# -- 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 bbb9b7c..1541aee 100644 --- a/framework/webapp/dtd/site-conf.xsd +++ b/framework/webapp/dtd/site-conf.xsd @@ -348,20 +348,6 @@ under the License. </xs:restriction> </xs:simpleType> </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/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..6802da1 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,34 +18,25 @@ *******************************************************************************/ 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; -import java.util.Collections; import java.util.Enumeration; 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; @@ -62,8 +53,6 @@ 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; import org.apache.ofbiz.webapp.event.EventHandlerException; @@ -81,12 +70,12 @@ import org.apache.ofbiz.widget.model.ThemeFactory; public class RequestHandler { public static final String module = RequestHandler.class.getName(); + private final String defaultStatusCodeString = UtilProperties.getPropertyValue("requestHandler", "status-code", "302"); private final ViewFactory viewFactory; private final EventFactory eventFactory; private final URL controllerConfigURL; private final boolean trackServerHit; private final boolean trackVisit; - private ControllerConfig ccfg; public static RequestHandler getRequestHandler(ServletContext servletContext) { RequestHandler rh = (RequestHandler) servletContext.getAttribute("_REQUEST_HANDLER_"); @@ -123,87 +112,11 @@ public class RequestHandler { return null; } - /** - * 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 - * @param req The HTTP request to match - * @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; - } - } - return rmaps != null ? rmaps : Collections.emptyList(); - } - - /** - * Find the request map matching {@code method}. - * Otherwise fall back to the one matching the "all" and "" special methods - * in that respective order. - * - * @param method the HTTP method to match - * @param rmaps the collection of request map candidates - * @return a request map {@code Optional} - */ - static Optional<RequestMap> resolveMethod(String method, Collection<RequestMap> rmaps) { - for (RequestMap map : rmaps) { - if (map.method.equalsIgnoreCase(method)) { - return Optional.of(map); - } - } - if (method.isEmpty()) { - return Optional.empty(); - } else if (method.equals("all")) { - return resolveMethod("", rmaps); - } else { - return resolveMethod("all", rmaps); - } - } - - /** - * 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 requestUri) throws RequestHandlerException, RequestHandlerExceptionAllowExternalRequests { + HttpSession session = request.getSession(); + Delegator delegator = (Delegator) request.getAttribute("delegator"); + GenericValue userLogin = (GenericValue) session.getAttribute("userLogin"); + doRequest(request, response, requestUri, userLogin, delegator); } public void doRequest(HttpServletRequest request, HttpServletResponse response, String chain, @@ -214,13 +127,20 @@ public class RequestHandler { long startTime = System.currentTimeMillis(); HttpSession session = request.getSession(); - // Parse controller config. + // get the controllerConfig once for this method so we don't have to get it over and over inside the method + ConfigXMLReader.ControllerConfig controllerConfig = this.getControllerConfig(); + Map<String, ConfigXMLReader.RequestMap> requestMapMap = null; + String statusCodeString = null; try { - ccfg = ConfigXMLReader.getControllerConfig(controllerConfigURL); + requestMapMap = controllerConfig.getRequestMapMap(); + statusCodeString = controllerConfig.getStatusCode(); } catch (WebAppConfigurationException e) { Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); throw new RequestHandlerException(e); } + if (UtilValidate.isEmpty(statusCodeString)) { + statusCodeString = defaultStatusCodeString; + } // workaround if we are in the root webapp String cname = UtilHttp.getApplicationName(request); @@ -235,38 +155,50 @@ public class RequestHandler { } } - String requestMissingErrorMessage = "Unknown request [" - + defaultRequestUri - + "]; this request does not exist or cannot be called directly."; + String overrideViewUri = RequestHandler.getOverrideViewUri(request.getPathInfo()); - String path = request.getPathInfo(); - String requestUri = getRequestUri(path); + String requestMissingErrorMessage = "Unknown request [" + defaultRequestUri + "]; this request does not exist or cannot be called directly."; + ConfigXMLReader.RequestMap requestMap = null; + if (defaultRequestUri != null) { + requestMap = requestMapMap.get(defaultRequestUri); + } + // check for default request + if (requestMap == null) { + String defaultRequest; + try { + defaultRequest = controllerConfig.getDefaultRequest(); + } catch (WebAppConfigurationException e) { + Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); + throw new RequestHandlerException(e); + } + if (defaultRequest != null) { // required! to avoid a null pointer exception and generate a requesthandler exception if default request not found. + requestMap = requestMapMap.get(defaultRequest); + } + } - 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); + // check for override view + if (overrideViewUri != null) { + ConfigXMLReader.ViewMap viewMap; + try { + viewMap = getControllerConfig().getViewMapMap().get(overrideViewUri); + if (viewMap == null) { + String defaultRequest = controllerConfig.getDefaultRequest(); + if (defaultRequest != null) { // required! to avoid a null pointer exception and generate a requesthandler exception if default request not found. + requestMap = requestMapMap.get(defaultRequest); + } } - } else { - throw new RequestHandlerExceptionAllowExternalRequests(); + } catch (WebAppConfigurationException e) { + Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); + throw new RequestHandlerException(e); } } - // The "overriddenView" attribute is set by resolveURI when necessary. - String overrideViewUri = (String) request.getAttribute("overriddenView"); - String method = UtilHttp.getRequestMethod(request); - RequestMap requestMap = resolveMethod(method, rmaps).orElseThrow(() -> { - String msg = UtilProperties.getMessage("WebappUiLabels", "RequestMethodNotMatchConfig", - UtilMisc.toList(requestUri, method), UtilHttp.getLocale(request)); - return new MethodNotAllowedException(msg); - }); + // if no matching request is found in the controller, depending on throwRequestHandlerExceptionOnMissingLocalRequest + // we throw a RequestHandlerException or RequestHandlerExceptionAllowExternalRequests + if (requestMap == null) { + if (throwRequestHandlerExceptionOnMissingLocalRequest) throw new RequestHandlerException(requestMissingErrorMessage); + else throw new RequestHandlerExceptionAllowExternalRequests(); + } String eventReturn = null; if (requestMap.metrics != null && requestMap.metrics.getThreshold() != 0.0 && requestMap.metrics.getTotalEvents() > 3 && requestMap.metrics.getThreshold() < requestMap.metrics.getServiceRate()) { @@ -280,7 +212,7 @@ public class RequestHandler { // Check for chained request. if (chain != null) { String chainRequestUri = RequestHandler.getRequestUri(chain); - requestMap = ccfg.getRequestMapMap().get(chainRequestUri); + requestMap = requestMapMap.get(chainRequestUri); if (requestMap == null) { throw new RequestHandlerException("Unknown chained request [" + chainRequestUri + "]; this request does not exist"); } @@ -304,16 +236,24 @@ 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) { + String defaultRequest; + try { + defaultRequest = controllerConfig.getDefaultRequest(); + } catch (WebAppConfigurationException e) { + Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); + throw new RequestHandlerException(e); + } + if (defaultRequest == null || !requestMapMap.get(defaultRequest).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 = requestMapMap.get(defaultRequest); } } // Check if we SHOULD be secure and are not. - boolean forwardedHTTPS = "HTTPS".equalsIgnoreCase(request.getHeader("X-Forwarded-Proto")); - if (!request.isSecure() && !forwardedHTTPS && requestMap.securityHttps) { + String forwardedProto = request.getHeader("X-Forwarded-Proto"); + boolean isForwardedSecure = UtilValidate.isNotEmpty(forwardedProto) && "HTTPS".equals(forwardedProto.toUpperCase()); + if ((!request.isSecure() && !isForwardedSecure) && requestMap.securityHttps) { // If the request method was POST then return an error to avoid problems with XSRF where the request may have come from another machine/program and had the same session ID but was not encrypted as it should have been (we used to let it pass to not lose data since it was too late to protect that data anyway) if ("POST".equalsIgnoreCase(request.getMethod())) { // we can't redirect with the body parameters, and for better security from XSRF, just return an error message @@ -350,7 +290,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, statusCodeString); return; } } @@ -358,7 +298,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); } @@ -366,56 +332,66 @@ public class RequestHandler { // If its the first visit run the first visit events. if (this.trackVisit(request) && session.getAttribute("_FIRST_VISIT_EVENTS_") == null) { - if (Debug.infoOn()) { + if (Debug.infoOn()) Debug.logInfo("This is the first request in this visit." + showSessionId(request), module); - } session.setAttribute("_FIRST_VISIT_EVENTS_", "complete"); - for (ConfigXMLReader.Event event: ccfg.getFirstVisitEventList().values()) { - try { - String returnString = this.runEvent(request, response, event, null, "firstvisit"); - if (returnString == null || "none".equalsIgnoreCase(returnString)) { - interruptRequest = true; - } else if (!"success".equalsIgnoreCase(returnString)) { - throw new EventHandlerException("First-Visit event did not return 'success'."); + try { + for (ConfigXMLReader.Event event: controllerConfig.getFirstVisitEventList().values()) { + try { + String returnString = this.runEvent(request, response, event, null, "firstvisit"); + if (returnString == null || "none".equalsIgnoreCase(returnString)) { + interruptRequest = true; + } else if (!"success".equalsIgnoreCase(returnString)) { + throw new EventHandlerException("First-Visit event did not return 'success'."); + } + } catch (EventHandlerException e) { + Debug.logError(e, module); } - } catch (EventHandlerException e) { - Debug.logError(e, module); } + } catch (WebAppConfigurationException e) { + Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); + throw new RequestHandlerException(e); } } // Invoke the pre-processor (but NOT in a chain) - for (ConfigXMLReader.Event event: ccfg.getPreprocessorEventList().values()) { - try { - String returnString = this.runEvent(request, response, event, null, "preprocessor"); - if (returnString == null || "none".equalsIgnoreCase(returnString)) { - interruptRequest = true; - } else if (!"success".equalsIgnoreCase(returnString)) { - if (!returnString.contains(":_protect_:")) { - throw new EventHandlerException("Pre-Processor event [" + event.invoke + "] did not return 'success'."); - } else { // protect the view normally rendered and redirect to error response view - returnString = returnString.replace(":_protect_:", ""); - if (returnString.length() > 0) { - request.setAttribute("_ERROR_MESSAGE_", returnString); - } - eventReturn = null; - // check to see if there is a "protect" response, if so it's ok else show the default_error_response_view - if (!requestMap.requestResponseMap.containsKey("protect")) { - if (ccfg.getProtectView() != null) { - overrideViewUri = ccfg.getProtectView(); - } else { - overrideViewUri = EntityUtilProperties.getPropertyValue("security", "default.error.response.view", delegator); - overrideViewUri = overrideViewUri.replace("view:", ""); - if ("none:".equals(overrideViewUri)) { - interruptRequest = true; + try { + for (ConfigXMLReader.Event event: controllerConfig.getPreprocessorEventList().values()) { + try { + String returnString = this.runEvent(request, response, event, null, "preprocessor"); + if (returnString == null || "none".equalsIgnoreCase(returnString)) { + interruptRequest = true; + } else if (!"success".equalsIgnoreCase(returnString)) { + if (!returnString.contains(":_protect_:")) { + throw new EventHandlerException("Pre-Processor event [" + event.invoke + "] did not return 'success'."); + } else { // protect the view normally rendered and redirect to error response view + returnString = returnString.replace(":_protect_:", ""); + if (returnString.length() > 0) { + request.setAttribute("_ERROR_MESSAGE_", returnString); + } + eventReturn = null; + // check to see if there is a "protect" response, if so it's ok else show the default_error_response_view + if (!requestMap.requestResponseMap.containsKey("protect")) { + String protectView = controllerConfig.getProtectView(); + if (protectView != null) { + overrideViewUri = protectView; + } else { + overrideViewUri = EntityUtilProperties.getPropertyValue("security", "default.error.response.view", delegator); + overrideViewUri = overrideViewUri.replace("view:", ""); + if ("none:".equals(overrideViewUri)) { + interruptRequest = true; + } } } } } + } catch (EventHandlerException e) { + Debug.logError(e, module); } - } catch (EventHandlerException e) { - Debug.logError(e, module); } + } catch (WebAppConfigurationException e) { + Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); + throw new RequestHandlerException(e); } } @@ -429,20 +405,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 = requestMapMap.get("checkLogin").event; String checkLoginReturnString = null; try { @@ -455,9 +424,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 = requestMapMap.get("checkLogin"); } else { - requestMap = ccfg.getRequestMapMap().get("ajaxCheckLogin"); + requestMap = requestMapMap.get("ajaxCheckLogin"); } } } @@ -466,7 +435,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 +532,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 +557,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, statusCodeString); return; } } @@ -640,55 +605,41 @@ public class RequestHandler { // ======== handle views ======== // first invoke the post-processor events. - for (ConfigXMLReader.Event event: ccfg.getPostprocessorEventList().values()) { - try { - String returnString = this.runEvent(request, response, event, requestMap, "postprocessor"); - if (returnString != null && !"success".equalsIgnoreCase(returnString)) { - throw new EventHandlerException("Post-Processor event did not return 'success'."); + try { + for (ConfigXMLReader.Event event: controllerConfig.getPostprocessorEventList().values()) { + try { + String returnString = this.runEvent(request, response, event, requestMap, "postprocessor"); + if (returnString != null && !"success".equalsIgnoreCase(returnString)) { + throw new EventHandlerException("Post-Processor event did not return 'success'."); + } + } catch (EventHandlerException e) { + Debug.logError(e, module); } - } catch (EventHandlerException e) { - Debug.logError(e, module); } + } catch (WebAppConfigurationException e) { + Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); + throw new RequestHandlerException(e); } - // 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)) + statusCodeString = 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); - } 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); + callRedirect(nextRequestResponse.value, response, request, statusCodeString); } 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, statusCodeString); } 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, statusCodeString); } 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, statusCodeString); } else if ("view".equals(nextRequestResponse.type)) { if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a view." + showSessionId(request), module); @@ -705,13 +656,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 +707,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 +740,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 +751,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 +803,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 +900,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 +1031,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 +1056,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 +1067,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 +1105,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 +1129,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,107 +1160,95 @@ 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); } - @FunctionalInterface - private interface EventCollectionProducer { - Collection<ConfigXMLReader.Event> get() throws WebAppConfigurationException; - } - - private void runEvents(HttpServletRequest req, HttpServletResponse res, - EventCollectionProducer prod, String trigger) { + public void runAfterLoginEvents(HttpServletRequest request, HttpServletResponse response) { try { - for (ConfigXMLReader.Event event: prod.get()) { - String ret = runEvent(req, res, event, null, trigger); - if (ret != null && !"success".equalsIgnoreCase(ret)) { - throw new EventHandlerException("Pre-Processor event did not return 'success'."); + for (ConfigXMLReader.Event event: getControllerConfig().getAfterLoginEventList().values()) { + try { + String returnString = this.runEvent(request, response, event, null, "after-login"); + if (returnString != null && !"success".equalsIgnoreCase(returnString)) { + throw new EventHandlerException("Pre-Processor event did not return 'success'."); + } + } catch (EventHandlerException e) { + Debug.logError(e, module); } } - } catch (EventHandlerException e) { - Debug.logError(e, module); } catch (WebAppConfigurationException e) { Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); } } - /** - * Run all the "after-login" Web events defined in the controller configuration. - * - * @param req the request to run the events with - * @param resp the response to run the events with - */ - public void runAfterLoginEvents(HttpServletRequest req, HttpServletResponse resp) { - EventCollectionProducer prod = () -> getControllerConfig().getAfterLoginEventList().values(); - runEvents(req, resp, prod, "after-login"); - } - - /** - * Run all the "before-logout" Web events defined in the controller configuration. - * - * @param req the request to run the events with - * @param resp the response to run the events with - */ - public void runBeforeLogoutEvents(HttpServletRequest req, HttpServletResponse resp) { - EventCollectionProducer prod = () -> getControllerConfig().getBeforeLogoutEventList().values(); - 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 void runBeforeLogoutEvents(HttpServletRequest request, HttpServletResponse response) { + try { + for (ConfigXMLReader.Event event: getControllerConfig().getBeforeLogoutEventList().values()) { + try { + String returnString = this.runEvent(request, response, event, null, "before-logout"); + if (returnString != null && !"success".equalsIgnoreCase(returnString)) { + throw new EventHandlerException("Pre-Processor event did not return 'success'."); + } + } catch (EventHandlerException e) { + Debug.logError(e, module); + } } + } catch (WebAppConfigurationException e) { + Debug.logError(e, "Exception thrown while parsing controller.xml file: ", module); } - 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); + 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; + } } - /** - * 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 +1256,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 6366fb9..dba2450 100644 --- a/framework/webtools/template/entity/CheckDb.ftl +++ b/framework/webtools/template/entity/CheckDb.ftl @@ -16,311 +16,100 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> -<h3>${uiLabelMap.WebtoolsCheckUpdateDatabase}</h3> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <td> - <input type="hidden" name="option" value="checkupdatetables"/> - </td> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}: </label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - </td> - </tr> - <tr> - <td class="label"> - </td> - <td> - <label> <input type="checkbox" name="checkPks" value="true" checked="checked"/> ${uiLabelMap.WebtoolsPks}</label> - <label> <input type="checkbox" name="checkFks" value="true"/> ${uiLabelMap.WebtoolsFks}</label> - <label> <input type="checkbox" name="checkFkIdx" value="true"/> ${uiLabelMap.WebtoolsFkIdx}</label> - <label> <input type="checkbox" name="addMissing" value="true"/> ${uiLabelMap.WebtoolsAddMissing}</label> - <label> <input type="checkbox" name="repair" value="true"/> ${uiLabelMap.WebtoolsRepairColumnSizes}</label> - </td> - </tr> - <tr> - <td class="label"> - </td> - <td> - <input type="submit" value="${uiLabelMap.WebtoolsCheckUpdateDatabase}"/> - </td> - </tr> - </tbody> - </table> -</form> -<p>${uiLabelMap.WebtoolsNoteUseAtYourOwnRisk}</p> -<script type="application/javascript"> - function enableTablesRemove() { - document.forms["TablesRemoveForm"].elements["TablesRemoveButton"].disabled=false; - } -</script> -<h3>${uiLabelMap.WebtoolsRemoveAllTables}</h3> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TablesRemoveForm"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="removetables"/> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}" name="TablesRemoveButton" disabled="disabled"/> - <input type="button" value="${uiLabelMap.WebtoolsEnable}" onclick="enableTablesRemove();"/> - </td> - </tr> - </tbody> - </table> -</form> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TableRemoveForm"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="removetable"/> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="20"/> - </td> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsEntityName}:</label> - </td> - <td> - <input type="text" name="entityName" value="${entityName}" size="20"/> - <input type="submit" value="${uiLabelMap.CommonRemove}" name="TablesRemoveButton"/> - </td> - </tr> - </tbody> - </table> -</form> -<h3>${uiLabelMap.WebtoolsCreateRemoveAllPrimaryKeys}</h3> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="createpks"/> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </td> - </tr> - </tbody> - </table> -</form> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="removepks"/> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </tr> - </tbody> - </table> -</form> -<h3>${uiLabelMap.WebtoolsCreateRemovePrimaryKey}</h3> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="createpk"/> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - </td> - </tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsEntityName}:</label> - </td> - <td> - <input type="text" name="entityName" value="${entityName}" size="20"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </td> - </tr> - </tbody> - </table> -</form> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <td> - <input type="hidden" name="option" value="removepk"/> - </td> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="20"/> - </td> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsEntityName}:</label> - </td> - <td> - <input type="text" name="entityName" value="${entityName}" size="20"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </td> - </tr> - </tbody> - </table> -</form> -<h3>${uiLabelMap.WebtoolsCreateRemoveAllDeclaredIndices}</h3> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="createidx"/> - </tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </td> - </tr> - </tbody> - </table> -</form> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="removeidx"/> - </tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </td> - </tr> - </tbody> - </table> -</form> -<h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeyIndices}</h3> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="createfkidxs"/> - </tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </td> - </tr> - </tbody> - </table> -</form> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="removefkidxs"/> - </tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </td> - </tr> - </tbody> - </table> -</form> -<h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeys}</h3> -<p>${uiLabelMap.WebtoolsNoteForeighKeysMayAlsoBeCreated}</p> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="createfks"/> - </tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonCreate}"/> - </td> - </tr> - </tbody> - </table> -</form> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="removefks"/> - </tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonRemove}"/> - </td> - </tr> - </tbody> - </table> -</form> -<h3>${uiLabelMap.WebtoolsUpdateCharacterSetAndCollate}</h3> -<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> - <table class="basic-table" cellspacing="0"> - <tbody> - <tr> - <input type="hidden" name="option" value="updateCharsetCollate"/> - </tr> - <tr> - <td class="label"> - <label>${uiLabelMap.WebtoolsGroupName}:</label> - </td> - <td> - <input type="text" name="groupName" value="${groupName}" size="40"/> - <input type="submit" value="${uiLabelMap.CommonUpdate}"/> - </td - </tr> - </tbody> - </table> -</form> + <h3>${uiLabelMap.WebtoolsCheckUpdateDatabase}</h3> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="checkupdatetables"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <label> <input type="checkbox" name="checkPks" value="true" checked="checked"/> ${uiLabelMap.WebtoolsPks}</label> + <label> <input type="checkbox" name="checkFks" value="true"/> ${uiLabelMap.WebtoolsFks}</label> + <label> <input type="checkbox" name="checkFkIdx" value="true"/> ${uiLabelMap.WebtoolsFkIdx}</label> + <label> <input type="checkbox" name="addMissing" value="true"/> ${uiLabelMap.WebtoolsAddMissing}</label> + <label> <input type="checkbox" name="repair" value="true"/> ${uiLabelMap.WebtoolsRepairColumnSizes}</label> + <input type="submit" value="${uiLabelMap.WebtoolsCheckUpdateDatabase}"/> + </form> + <p>${uiLabelMap.WebtoolsNoteUseAtYourOwnRisk}</p> + <script language="JavaScript" type="text/javascript"> + function enableTablesRemove() { + document.forms["TablesRemoveForm"].elements["TablesRemoveButton"].disabled=false; + } + </script> + <h3>${uiLabelMap.WebtoolsRemoveAllTables}</h3> + <form method="post" action="${encodeURLCheckDb}" name="TablesRemoveForm"> + <input type="hidden" name="option" value="removetables"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}" name="TablesRemoveButton" disabled="disabled"/> + <input type="button" value="${uiLabelMap.WebtoolsEnable}" onclick="enableTablesRemove();"/> + </form> + <form method="post" action="${encodeURLCheckDb}" name="TableRemoveForm"> + <input type="hidden" name="option" value="removetable"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="20"/> + ${uiLabelMap.WebtoolsEntityName}: <input type="text" name="entityName" value="${entityName}" size="20"/> + <input type="submit" value="${uiLabelMap.CommonRemove}" name="TablesRemoveButton"/> + </form> + <h3>${uiLabelMap.WebtoolsCreateRemoveAllPrimaryKeys}</h3> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="createpks"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </form> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="removepks"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </form> + <h3>${uiLabelMap.WebtoolsCreateRemovePrimaryKey}</h3> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="createpk"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="20"/> + ${uiLabelMap.WebtoolsEntityName}: <input type="text" name="entityName" value="${entityName}" size="20"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </form> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="removepk"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="20"/> + ${uiLabelMap.WebtoolsEntityName}: <input type="text" name="entityName" value="${entityName}" size="20"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </form> + <h3>${uiLabelMap.WebtoolsCreateRemoveAllDeclaredIndices}</h3> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="createidx"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </form> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="removeidx"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </form> + <h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeyIndices}</h3> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="createfkidxs"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </form> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="removefkidxs"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </form> + <h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeys}</h3> + <p>${uiLabelMap.WebtoolsNoteForeighKeysMayAlsoBeCreated}</p> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="createfks"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonCreate}"/> + </form> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="removefks"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonRemove}"/> + </form> + <h3>${uiLabelMap.WebtoolsUpdateCharacterSetAndCollate}</h3> + <form method="post" action="${encodeURLCheckDb}"> + <input type="hidden" name="option" value="updateCharsetCollate"/> + ${uiLabelMap.WebtoolsGroupName}: <input type="text" name="groupName" value="${groupName}" size="40"/> + <input type="submit" value="${uiLabelMap.CommonUpdate}"/> + </form> <#if miters?has_content> <hr /> <ul> 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 681f91c..c19a3ba 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/themes/bluelight/template/Header.ftl b/themes/bluelight/template/Header.ftl index 0ef8b95..df2bca1 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/> @@ -197,7 +193,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/template/includes/ListLocales.ftl b/themes/common/template/includes/ListLocales.ftl index 82c7ca7..647090f 100644 --- a/themes/common/template/includes/ListLocales.ftl +++ b/themes/common/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/flatgrey/template/Header.ftl b/themes/flatgrey/template/Header.ftl index fc9cdf0..effe4d2 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/> @@ -159,7 +155,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 3e6d53b..2afc7a7 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 4714c88..ac0f215 100644 --- a/themes/rainbowstone/template/includes/TopAppBar.ftl +++ b/themes/rainbowstone/template/includes/TopAppBar.ftl @@ -214,7 +214,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 ebb1ed3..7afd8c7 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 de5fae2..6bbd01e 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 release17.12 in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git commit 793628bb15521257a8ccdfec181daa4cdb501eea 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) Conflicts handled by hand framework/security/config/security.properties framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java --- .../src/main/java/org/apache/ofbiz/base/util/UtilMisc.java | 13 +++++++++++++ framework/security/config/security.properties | 6 +++++- .../org/apache/ofbiz/webapp/control/RequestHandler.java | 10 ++++++++++ 3 files changed, 28 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 2be803e..0f0a4f3 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 @@ -598,6 +598,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 5b809ff..2a044d6 100644 --- a/framework/security/config/security.properties +++ b/framework/security/config/security.properties @@ -135,6 +135,10 @@ security.login.externalLoginKey.enabled=true # -- Security key used to encrypt and decrypt the autogenerated password in forgot password functionality. login.secret_key_string=Secret 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 6802da1..ea0655d 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 @@ -76,6 +76,8 @@ public class RequestHandler { private final URL controllerConfigURL; private final boolean trackServerHit; private final boolean trackVisit; + private final List hostHeadersAllowed; + private ControllerConfig ccfg; public static RequestHandler getRequestHandler(ServletContext servletContext) { RequestHandler rh = (RequestHandler) servletContext.getAttribute("_REQUEST_HANDLER_"); @@ -100,6 +102,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() { @@ -122,6 +127,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 |