This is an automated email from the ASF dual-hosted git repository. jleroux pushed a commit to branch POC-for-CSRF-Token-OFBIZ-11306 in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
commit 69b551038801e53c1cd3b8c03ddf1fab0198c3fc Author: Jacques Le Roux <jacques.le.r...@les7arts.com> AuthorDate: Wed Feb 26 15:41:23 2020 +0100 Creates new POC-for-CSRF-Token-OFBIZ-11306 branch To share with James and others and later when OK to create a PR --- .../humanres/template/category/CategoryTree.ftl | 16 +- .../category/ftl/CatalogAltUrlSeoTransform.java | 8 +- .../product/category/ftl/UrlRegexpTransform.java | 13 +- .../product/template/category/CategoryTree.ftl | 2 +- .../java/org/apache/ofbiz/common/CommonEvents.java | 3 +- .../common/webcommon/WEB-INF/common-controller.xml | 7 +- framework/security/config/security.properties | 23 +- .../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 +++++++++++++++ framework/webapp/dtd/site-conf.xsd | 14 + .../ofbiz/webapp/control/ConfigXMLReader.java | 3 + .../ofbiz/webapp/control/ControlEventListener.java | 3 + .../ofbiz/webapp/control/RequestHandler.java | 33 +- .../ofbiz/webapp/ftl/CsrfTokenAjaxTransform.java | 75 +++++ .../webapp/ftl/CsrfTokenPairNonAjaxTransform.java | 76 +++++ .../ofbiz/webapp/freemarkerTransforms.properties | 2 + .../webtools/groovyScripts/entity/CheckDb.groovy | 7 +- .../webtools/groovyScripts/entity/EntityRef.groovy | 6 + framework/webtools/template/entity/CheckDb.ftl | 28 +- .../webtools/template/entity/EntityRefList.ftl | 9 +- framework/webtools/template/entity/ViewGeneric.ftl | 5 +- .../webapp/webtools/WEB-INF/controller.xml | 2 +- .../java/org/apache/ofbiz/widget/WidgetWorker.java | 14 + .../widget/renderer/macro/MacroFormRenderer.java | 14 +- themes/bluelight/template/Header.ftl | 6 +- .../common-theme/template/includes/ListLocales.ftl | 2 +- .../template/macro/CsvFormMacroLibrary.ftl | 2 +- .../template/macro/FoFormMacroLibrary.ftl | 2 +- .../template/macro/HtmlFormMacroLibrary.ftl | 8 +- .../template/macro/TextFormMacroLibrary.ftl | 2 +- .../template/macro/XlsFormMacroLibrary.ftl | 2 +- .../template/macro/XmlFormMacroLibrary.ftl | 2 +- .../webapp/common/js/util/OfbizUtil.js | 12 +- themes/flatgrey/template/Header.ftl | 6 +- themes/rainbowstone/template/includes/Header.ftl | 4 + .../rainbowstone/template/includes/TopAppBar.ftl | 2 +- themes/tomahawk/template/AppBarClose.ftl | 2 +- themes/tomahawk/template/Header.ftl | 4 + 41 files changed, 1179 insertions(+), 60 deletions(-) diff --git a/applications/humanres/template/category/CategoryTree.ftl b/applications/humanres/template/category/CategoryTree.ftl index 10a08ac..f14bbfc 100644 --- a/applications/humanres/template/category/CategoryTree.ftl +++ b/applications/humanres/template/category/CategoryTree.ftl @@ -61,18 +61,18 @@ var rawdata = [ "plugins" : [ "themes", "json_data","ui" ,"cookies", "types", "crrm", "contextmenu"], "json_data" : { "data" : rawdata, - "ajax" : { "url" : "<@ofbizUrl>getHRChild</@ofbizUrl>", "type" : "POST", - "data" : function (n) { - return { + "ajax" : { "url" : "<@ofbizUrl>getHRChild</@ofbizUrl>", "type" : "POST", + "data" : function (n) { + return { "partyId" : n.attr ? n.attr("id").replace("node_","") : 1 , "additionParam" : "','category" , "hrefString" : "viewprofile?partyId=" , "onclickFunction" : "callDocument" - }; + }; }, - success : function(data) { - return data.hrTree; - } + success : function(data) { + return data.hrTree; + } } }, "types" : { @@ -92,7 +92,7 @@ var rawdata = [ } function callDocument(id,type) { - window.location = "viewprofile?partyId=" + id; + window.location = "viewprofile?partyId=" + id + "&<@csrfTokenPair>viewprofile</@csrfTokenPair>"; } function callEmplDocument(id,type) { diff --git a/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java b/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java index b421681..653067b 100644 --- a/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java +++ b/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/CatalogAltUrlSeoTransform.java @@ -25,12 +25,14 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.apache.ofbiz.security.CsrfUtil; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.UtilValidate; import org.apache.ofbiz.base.util.template.FreeMarkerWorker; import org.apache.ofbiz.entity.Delegator; import org.apache.ofbiz.entity.GenericEntityException; import org.apache.ofbiz.entity.GenericValue; +import org.apache.ofbiz.entity.util.EntityQuery; import org.apache.ofbiz.entity.util.EntityUtilProperties; import org.apache.ofbiz.product.category.CatalogUrlFilter; import org.apache.ofbiz.product.category.CategoryContentWrapper; @@ -48,7 +50,6 @@ import freemarker.template.SimpleNumber; import freemarker.template.SimpleScalar; import freemarker.template.TemplateModelException; import freemarker.template.TemplateTransformModel; -import org.apache.ofbiz.entity.util.EntityQuery; public class CatalogAltUrlSeoTransform implements TemplateTransformModel { public final static String module = CatalogUrlSeoTransform.class.getName(); @@ -129,6 +130,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/src/main/java/org/apache/ofbiz/product/category/ftl/UrlRegexpTransform.java b/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/UrlRegexpTransform.java index 48ac9b3..642d25f 100644 --- a/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/UrlRegexpTransform.java +++ b/applications/product/src/main/java/org/apache/ofbiz/product/category/ftl/UrlRegexpTransform.java @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.ofbiz.base.component.ComponentConfig; +import org.apache.ofbiz.security.CsrfUtil; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.template.FreeMarkerWorker; import org.apache.ofbiz.entity.Delegator; @@ -134,8 +135,16 @@ public class UrlRegexpTransform implements TemplateTransformModel { } RequestHandler rh = RequestHandler.from(request); - String link = rh.makeLink(request, response, buf.toString(), fullPath, secure || request.isSecure() , encode, controlPath); - out.write(seoUrl(link, userLogin == null)); + String seoUrl = seoUrl(rh.makeLink(request, response, buf.toString(), fullPath, + secure || request.isSecure(), encode, controlPath), userLogin == null); + String requestURI = buf.toString(); + + // add / update csrf token to link when required + String tokenValue = CsrfUtil.generateTokenForNonAjax(request, + controlPath + (requestURI.startsWith("/") ? requestURI : "/"+requestURI)); + seoUrl = CsrfUtil.addOrUpdateTokenInUrl(seoUrl, tokenValue); + + out.write(seoUrl); } else if (!webSiteId.isEmpty()) { Delegator delegator = FreeMarkerWorker.unwrap(env.getVariable("delegator")); if (delegator == null) { diff --git a/applications/product/template/category/CategoryTree.ftl b/applications/product/template/category/CategoryTree.ftl index dce62c7..dd4ca21 100644 --- a/applications/product/template/category/CategoryTree.ftl +++ b/applications/product/template/category/CategoryTree.ftl @@ -65,7 +65,7 @@ var rawdata = [ "plugins" : [ "themes", "json_data","ui" ,"cookies", "types"], "json_data" : { "data" : rawdata, - "ajax" : { "url" : "<@ofbizUrl>getChild</@ofbizUrl>", + "ajax" : { "url" : "getChild", "type" : "POST", "data" : function (n) { return { diff --git a/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java b/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java index 2135444..aa42d61 100644 --- a/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java +++ b/framework/common/src/main/java/org/apache/ofbiz/common/CommonEvents.java @@ -77,7 +77,8 @@ public class CommonEvents { "thisRequestUri", "org.apache.tomcat.util.net.secure_protocol_version", "userLogin", - "impersonateLogin" + "impersonateLogin", + "requestMapMap" // requestMapMap is used by CSRFUtil }; /** Simple event to set the users per-session locale setting. The user's locale diff --git a/framework/common/webcommon/WEB-INF/common-controller.xml b/framework/common/webcommon/WEB-INF/common-controller.xml index 80407c6..6526111 100644 --- a/framework/common/webcommon/WEB-INF/common-controller.xml +++ b/framework/common/webcommon/WEB-INF/common-controller.xml @@ -75,7 +75,7 @@ under the License. <response name="error" type="view" value="login"/> </request-map> <request-map uri="logout"> - <security https="true" auth="true"/> + <security https="true" auth="true" csrf-token="false"/> <event type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="logout"/> <response name="success" type="request-redirect" value="main"/> <response name="error" type="view" value="main"/> @@ -237,7 +237,8 @@ under the License. <!--========================== AJAX events =====================--> <!-- Get states related to a country --> <request-map uri="getAssociatedStateList"> - <security https="true" auth="false"/> + <!-- depended on by ecommerce during checkout, so set the csrf-token to false --> + <security https="true" auth="false" csrf-token="false"/> <event type="service" invoke="getAssociatedStateList"/> <response name="success" type="request" value="json"/> <response name="error" type="request" value="json"/> @@ -317,7 +318,7 @@ under the License. <!-- Set TimeZone from user's browser --> <!-- XXX The auth setting is inconsistent with the one in the service for a good reason, see OFBIZ-10471 for an explanation --> - <request-map uri="SetTimeZoneFromBrowser"> + <request-map uri="SetTimeZoneFromBrowser" method="post"> <security https="false" auth="false"/> <event type="service" invoke="SetTimeZoneFromBrowser"/> <response name="success" type="request" value="json"/> diff --git a/framework/security/config/security.properties b/framework/security/config/security.properties index 5a44fe2..55c2b6a 100644 --- a/framework/security/config/security.properties +++ b/framework/security/config/security.properties @@ -1,4 +1,4 @@ -############################################################################### +############################################################################## # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -149,5 +149,24 @@ security.jwt.token.expireTime=1800 # -- To make this work you also have to configure a secret key with security.token.key security.internal.sso.enabled=false -# -- The secret key for the JWT token signature. Read Passwords and JWT (JSON Web Tokens) usage documentation to choose the way you want to store this key +# -- The secret key for the JWT token signature. Read Passwords and JWT (JSON Web Tokens) usage documentation to choose the way you want to store this key security.token.key=security.token.key + +# -- 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= 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..eaf5635 --- /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/"; + 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/dtd/site-conf.xsd b/framework/webapp/dtd/site-conf.xsd index fc9a966..01d0046 100644 --- a/framework/webapp/dtd/site-conf.xsd +++ b/framework/webapp/dtd/site-conf.xsd @@ -305,6 +305,20 @@ under the License. </xs:documentation> </xs:annotation> </xs:attribute> + <xs:attribute name="csrf-token" use="optional" default=""> + <xs:annotation> + <xs:documentation> + If true csrf token is expected. If false no csrf token check. Default to "". + </xs:documentation> + </xs:annotation> + <xs:simpleType> + <xs:restriction base="xs:token"> + <xs:enumeration value=""/> + <xs:enumeration value="true"/> + <xs:enumeration value="false"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> </xs:attributeGroup> <xs:element name="metric"> <xs:annotation> diff --git a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java index 0802582..b542036 100644 --- a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java +++ b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ConfigXMLReader.java @@ -54,6 +54,7 @@ import org.apache.ofbiz.base.util.cache.UtilCache; import org.apache.ofbiz.base.util.collections.MapContext; import org.apache.ofbiz.base.util.collections.MultivaluedMapContext; import org.apache.ofbiz.base.util.collections.MultivaluedMapContextAdapter; +import org.apache.ofbiz.security.CsrfUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -450,6 +451,7 @@ public class ConfigXMLReader { public Event event; public boolean securityHttps = true; public boolean securityAuth = false; + public boolean securityCsrfToken = true; public boolean securityCert = false; public boolean securityExternalView = true; public boolean securityDirectRequest = true; @@ -481,6 +483,7 @@ public class ConfigXMLReader { this.securityCert = "true".equals(securityElement.getAttribute("cert")); this.securityExternalView = !"false".equals(securityElement.getAttribute("external-view")); this.securityDirectRequest = !"false".equals(securityElement.getAttribute("direct-request")); + this.securityCsrfToken = CsrfUtil.strategy.modifySecurityCsrfToken(this.uri, this.method, securityElement.getAttribute("csrf-token")); } // Check for event Element eventElement = UtilXml.firstChildElement(requestMapElement, "event"); diff --git a/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlEventListener.java b/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ControlEventListener.java index 22fd632..54cb206 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; @@ -71,6 +72,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 0e91bb1..1fe488a 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 @@ -45,6 +45,7 @@ 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; @@ -428,6 +429,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) { @@ -578,8 +586,13 @@ public class RequestHandler { if (UtilValidate.isNotEmpty(queryString)) { redirectTarget += "?" + queryString; } - - callRedirect(makeLink(request, response, redirectTarget), response, request, ccfg.getStatusCode()); + 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; } } @@ -659,10 +672,22 @@ public class RequestHandler { 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, redirectSC); + 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, redirectSC); + 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); 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/webapp/src/main/resources/org/apache/ofbiz/webapp/freemarkerTransforms.properties b/framework/webapp/src/main/resources/org/apache/ofbiz/webapp/freemarkerTransforms.properties index 46f7979..65cf04e 100644 --- a/framework/webapp/src/main/resources/org/apache/ofbiz/webapp/freemarkerTransforms.properties +++ b/framework/webapp/src/main/resources/org/apache/ofbiz/webapp/freemarkerTransforms.properties @@ -29,3 +29,5 @@ ofbizNumber=org.apache.ofbiz.webapp.ftl.OfbizNumberTransform 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/webtools/groovyScripts/entity/CheckDb.groovy b/framework/webtools/groovyScripts/entity/CheckDb.groovy index fd822de..567714f 100644 --- a/framework/webtools/groovyScripts/entity/CheckDb.groovy +++ b/framework/webtools/groovyScripts/entity/CheckDb.groovy @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import org.apache.ofbiz.entity.Delegator -import org.apache.ofbiz.security.Security + + import org.apache.ofbiz.entity.jdbc.DatabaseUtil -import org.apache.ofbiz.entity.model.ModelEntity controlPath = parameters._CONTROL_PATH_ @@ -114,7 +113,7 @@ if (security.hasPermission("ENTITY_MAINT", session)) { miter = messages.iterator() context.miters = miter } - context.encodeURLCheckDb = response.encodeURL(controlPath + "/view/checkdb") + context.checkDbURL = "view/checkdb" context.groupName = groupName ?: "org.apache.ofbiz" context.entityName = entityName ?: "" } diff --git a/framework/webtools/groovyScripts/entity/EntityRef.groovy b/framework/webtools/groovyScripts/entity/EntityRef.groovy index 17933db..279e448 100644 --- a/framework/webtools/groovyScripts/entity/EntityRef.groovy +++ b/framework/webtools/groovyScripts/entity/EntityRef.groovy @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import org.apache.ofbiz.security.CsrfUtil; + controlPath = parameters._CONTROL_PATH_ list = "$controlPath/view/entityref_list" main = "$controlPath/view/entityref_main" @@ -29,5 +31,9 @@ if (search) { list = "$list?forstatic=$forstatic" main = "$main?forstatic=$forstatic" } +tokenList = CsrfUtil.generateTokenForNonAjax(request, "view/entityref_list") +tokenMain = CsrfUtil.generateTokenForNonAjax(request, "view/entityref_main") +list = CsrfUtil.addOrUpdateTokenInUrl(list, tokenList) +main = CsrfUtil.addOrUpdateTokenInUrl(main, tokenMain) context.encodeUrlList = response.encodeURL(list) context.encodeUrlMain = response.encodeURL(main) diff --git a/framework/webtools/template/entity/CheckDb.ftl b/framework/webtools/template/entity/CheckDb.ftl index ac81459..91cf8d3 100644 --- a/framework/webtools/template/entity/CheckDb.ftl +++ b/framework/webtools/template/entity/CheckDb.ftl @@ -17,7 +17,7 @@ specific language governing permissions and limitations under the License. --> <h3>${uiLabelMap.WebtoolsCheckUpdateDatabase}</h3> -<form class="basic-form" class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -61,7 +61,7 @@ under the License. } </script> <h3>${uiLabelMap.WebtoolsRemoveAllTables}</h3> -<form class="basic-form" class="basic-form" method="post" action="${encodeURLCheckDb}" name="TablesRemoveForm"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TablesRemoveForm"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -80,7 +80,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}" name="TableRemoveForm"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>" name="TableRemoveForm"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -107,7 +107,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsCreateRemoveAllPrimaryKeys}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -125,7 +125,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -143,7 +143,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsCreateRemovePrimaryKey}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -168,7 +168,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -197,7 +197,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsCreateRemoveAllDeclaredIndices}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -214,7 +214,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -232,7 +232,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeyIndices}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -249,7 +249,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -268,7 +268,7 @@ under the License. </form> <h3>${uiLabelMap.WebtoolsCreateRemoveAllForeignKeys}</h3> <p>${uiLabelMap.WebtoolsNoteForeighKeysMayAlsoBeCreated}</p> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -285,7 +285,7 @@ under the License. </tbody> </table> </form> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> @@ -303,7 +303,7 @@ under the License. </table> </form> <h3>${uiLabelMap.WebtoolsUpdateCharacterSetAndCollate}</h3> -<form class="basic-form" method="post" action="${encodeURLCheckDb}"> +<form class="basic-form" method="post" action="<@ofbizUrl>${checkDbURL}</@ofbizUrl>"> <table class="basic-table" cellspacing="0"> <tbody> <tr> diff --git a/framework/webtools/template/entity/EntityRefList.ftl b/framework/webtools/template/entity/EntityRefList.ftl index 1ace17f..55e2387 100644 --- a/framework/webtools/template/entity/EntityRefList.ftl +++ b/framework/webtools/template/entity/EntityRefList.ftl @@ -1,3 +1,4 @@ + <#-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file @@ -54,9 +55,9 @@ under the License. <div class="section-header">${uiLabelMap.WebtoolsEntityPackages}</div> <#list packageNames as packageName> <#if forstatic> - <a href="<@ofbizUrl>view/entityref_main?forstatic=true#${packageName}</@ofbizUrl>" target="entityFrame">${packageName}</a><br /> + <a href="<@ofbizUrl>view/entityref_main?forstatic=true</@ofbizUrl>#${packageName}" target="entityFrame">${packageName}</a><br /> <#else> - <a href="<@ofbizUrl>view/entityref_main#${packageName}</@ofbizUrl>" target="entityFrame">${packageName}</a><br /> + <a href="<@ofbizUrl>view/entityref_main</@ofbizUrl>#${packageName}" target="entityFrame">${packageName}</a><br /> </#if> </#list> </#if> @@ -65,9 +66,9 @@ under the License. <div class="section-header">${uiLabelMap.WebtoolsEntitiesAlpha}</div> <#list entitiesList as entity> <#if forstatic> - <a href="<@ofbizUrl>view/entityref_main?forstatic=true#${entity.entityName}</@ofbizUrl>" target="entityFrame">${entity.entityName}</a> + <a href="<@ofbizUrl>view/entityref_main?forstatic=true</@ofbizUrl>#${entity.entityName}" target="entityFrame">${entity.entityName}</a> <#else> - <a href="<@ofbizUrl>view/entityref_main#${entity.entityName}${entity.url!}</@ofbizUrl>" target="entityFrame">${entity.entityName}</a> + <a href="<@ofbizUrl>view/entityref_main</@ofbizUrl>#${entity.entityName}${entity.url!}" target="entityFrame">${entity.entityName}</a> </#if> <br /> </#list> diff --git a/framework/webtools/template/entity/ViewGeneric.ftl b/framework/webtools/template/entity/ViewGeneric.ftl index 7c163db..e907835 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> @@ -53,13 +54,13 @@ function ShowTab(lname) { <a href='<@ofbizUrl>entity/find/${entityName}</@ofbizUrl>' class="buttontext">${uiLabelMap.WebtoolsBackToFindScreen}</a> <#if enableEdit = "false"> <#if hasCreatePermission> - <form action="<@ofbizUrl>entity/edit/${currentFindString}</@ofbizUrl>" method="get"> + <form action="<@ofbizUrl>entity/edit/${currentFindString}</@ofbizUrl>" method="post"> <input type="submit" value="${uiLabelMap.CommonEdit}" /> </form> </#if> <#if value?has_content> <#if hasDeletePermission> - <form action='<@ofbizUrl>entity/change/${currentFindString}</@ofbizUrl>' method="delete" name="updateForm"> + <form action='<@ofbizUrl>entity/change/${currentFindString}</@ofbizUrl>' method="post" name="updateForm"> <input type="hidden" value="DELETE" name="_method"/> <#list pkNamesValuesMap.keySet() as pkName> <input type="hidden" value="${pkNamesValuesMap.get(pkName)}" name="${pkName}"/> diff --git a/framework/webtools/webapp/webtools/WEB-INF/controller.xml b/framework/webtools/webapp/webtools/WEB-INF/controller.xml index 6e4976c..72eec1e 100644 --- a/framework/webtools/webapp/webtools/WEB-INF/controller.xml +++ b/framework/webtools/webapp/webtools/WEB-INF/controller.xml @@ -51,7 +51,7 @@ under the License. <request-map uri="entity/create/{entityName}" method="get"><security https="true" auth="true"/><response name="success" type="view" value="EditGeneric"/></request-map> <!-- form for modifying a record --> - <request-map uri="entity/edit/{entityName}/{pkValues: .*}" method="get"><security https="true" auth="true"/><response name="success" type="view" value="EditGeneric" /></request-map> + <request-map uri="entity/edit/{entityName}/{pkValues: .*}" method="post"><security https="true" auth="true"/><response name="success" type="view" value="EditGeneric" /></request-map> <!--view relations for a given entity --> <request-map uri="entity/relations/{entityName}" method="get"><security https="true" auth="true"/><response name="success" type="view" value="ViewRelations" /></request-map> diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java index 2f043bb..e359770 100644 --- a/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java +++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/WidgetWorker.java @@ -27,6 +27,7 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.ofbiz.security.CsrfUtil; import org.apache.ofbiz.base.util.UtilCodec; import org.apache.ofbiz.base.util.UtilGenerics; import org.apache.ofbiz.base.util.UtilHttp; @@ -115,6 +116,19 @@ public final class WidgetWorker { } else { externalWriter.append(localWriter.toString()); } + + String tokenValue = CsrfUtil.generateTokenForNonAjax(request, target); + if (UtilValidate.isNotEmpty(tokenValue)){ + String currentString = externalWriter.toString(); + if(currentString.startsWith("<form")) { + currentString = currentString.substring(currentString.lastIndexOf("\"")+1); + } + if (currentString.indexOf('?') == -1) { + externalWriter.append("?" + CsrfUtil.tokenNameNonAjax + "=" + tokenValue); + } else { + externalWriter.append("&" + CsrfUtil.tokenNameNonAjax + "=" + tokenValue); + } + } } public static void appendContentUrl(Appendable writer, String location, HttpServletRequest request) throws IOException { diff --git a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java index 2a7ee14..02797ad 100644 --- a/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java +++ b/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/macro/MacroFormRenderer.java @@ -40,6 +40,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.apache.ofbiz.security.CsrfUtil; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.StringUtil; import org.apache.ofbiz.base.util.UtilCodec; @@ -1425,6 +1426,10 @@ public final class MacroFormRenderer implements FormStringRenderer { } } String focusFieldName = modelForm.getFocusFieldName(); + + // Generate CSRF name & value for form + String csrfNameValue = CsrfUtil.tokenNameNonAjax + " " +CsrfUtil.generateTokenForNonAjax(request, targ); + StringWriter sr = new StringWriter(); sr.append("<@renderFormOpen "); sr.append(" linkUrl=\""); @@ -1455,7 +1460,9 @@ public final class MacroFormRenderer implements FormStringRenderer { sr.append(Integer.toString(viewSize)); sr.append("\" useRowSubmit="); sr.append(Boolean.toString(useRowSubmit)); - sr.append(" />"); + sr.append(" csrfNameValue=\""); + sr.append(csrfNameValue); + sr.append("\" />"); executeMacro(writer, sr.toString()); } @@ -2413,6 +2420,11 @@ public final class MacroFormRenderer implements FormStringRenderer { viewSizeParam = "VIEW_SIZE" + "_" + paginatorNumber; } String str = (String) context.get("_QBESTRING_"); + + // refresh any csrf token in the query string for pagination + String tokenValue = CsrfUtil.generateTokenForNonAjax(request, targetService); + str = CsrfUtil.addOrUpdateTokenInQueryString(str, tokenValue); + // strip legacy viewIndex/viewSize params from the query string String queryString = UtilHttp.stripViewParamsFromQueryString(str, "" + paginatorNumber); // strip parameterized index/size params from the query string diff --git a/themes/bluelight/template/Header.ftl b/themes/bluelight/template/Header.ftl index 16fcea2..f4bbff9 100644 --- a/themes/bluelight/template/Header.ftl +++ b/themes/bluelight/template/Header.ftl @@ -28,6 +28,10 @@ under the License. <html lang="${docLangAttr}" dir="${langDir}" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <#assign csrfDefenseStrategy = Static["org.apache.ofbiz.entity.util.EntityUtilProperties"].getPropertyValue("security", "csrf.defense.strategy", delegator)> + <#if csrfDefenseStrategy != "org.apache.ofbiz.security.NoCsrfDefenseStrategy"> + <meta name="csrf-token" content="<@csrfTokenAjax/>"/> + </#if> <title>${layoutSettings.companyName}: <#if (titleProperty)?has_content>${uiLabelMap[titleProperty]}<#else>${title!}</#if></title> <#if layoutSettings.shortcutIcon?has_content> <#assign shortcutIcon = layoutSettings.shortcutIcon/> @@ -194,7 +198,7 @@ under the License. <#--if webSiteId?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??--> <#if parameters.componentName?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??> <#include "component://common-theme/template/includes/HelpLink.ftl" /> - <li><a class="help-link <#if pageAvail?has_content> alert</#if>" href="javascript:lookup_popup1('showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> + <li><a class="help-link <#if pageAvail?has_content> alert</#if>" href="javascript:lookup_popup1('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> </#if> <#if userLogin??> <#if "Y" == (userPreferences.COMPACT_HEADER)?default("N")> diff --git a/themes/common-theme/template/includes/ListLocales.ftl b/themes/common-theme/template/includes/ListLocales.ftl index 647090f..82c7ca7 100644 --- a/themes/common-theme/template/includes/ListLocales.ftl +++ b/themes/common-theme/template/includes/ListLocales.ftl @@ -36,7 +36,7 @@ under the License. </#if> <tr <#if altRow>class="alternate-row"</#if>> <td lang="${langAttr}" dir="${langDir}"> - <a href="<@ofbizUrl>setSessionLocale</@ofbizUrl>?newLocale=${availableLocale.toString()}"> + <a href="<@ofbizUrl>setSessionLocale?newLocale=${availableLocale.toString()}</@ofbizUrl>"> ${availableLocale.getDisplayName(availableLocale)} - [${langAttr}]</a> </td> diff --git a/themes/common-theme/template/macro/CsvFormMacroLibrary.ftl b/themes/common-theme/template/macro/CsvFormMacroLibrary.ftl index cadd70e..b371b19 100644 --- a/themes/common-theme/template/macro/CsvFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/CsvFormMacroLibrary.ftl @@ -54,7 +54,7 @@ under the License. <#macro renderEmptyFormDataMessage message></#macro> <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/template/macro/FoFormMacroLibrary.ftl b/themes/common-theme/template/macro/FoFormMacroLibrary.ftl index 948203f..29189db 100644 --- a/themes/common-theme/template/macro/FoFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/FoFormMacroLibrary.ftl @@ -80,7 +80,7 @@ under the License. <#macro renderEmptyFormDataMessage message></#macro> <#macro renderSingleFormFieldTitle><!--title form--></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl index 060fd20..9e6c62c 100644 --- a/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/HtmlFormMacroLibrary.ftl @@ -243,8 +243,14 @@ under the License. </#macro> <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType name viewIndexField viewSizeField viewIndex viewSize targetWindow="" containerId="" containerStyle="" autocomplete="" useRowSubmit="" focusFieldName="" hasRequiredField=""> +<#macro renderFormOpen linkUrl formType name viewIndexField viewSizeField viewIndex viewSize targetWindow="" containerId="" containerStyle="" autocomplete="" useRowSubmit="" focusFieldName="" hasRequiredField="" csrfNameValue=""> <form method="post" action="${linkUrl}"<#if formType=="upload"> enctype="multipart/form-data"</#if><#if targetWindow?has_content> target="${targetWindow}"</#if><#if containerId?has_content> id="${containerId}"</#if> <#if focusFieldName?has_content> data-focus-field="${focusFieldName}"</#if> class="<#if containerStyle?has_content>${containerStyle}<#else>basic-form</#if><#if hasRequiredField?has_content> requireValidation</#if>" onsubmit="javascript:submitFormDisableSubmits(this)"<#if au [...] + <#if csrfNameValue?has_content> + <#assign result = csrfNameValue?matches(r"(\w+) (\w+)")> + <#if result> + <input type="hidden" name="${result?groups[1]}" value="${result?groups[2]}"/> + </#if> + </#if> <#if useRowSubmit?has_content && useRowSubmit> <input type="hidden" name="_useRowSubmit" value="Y"/> <#if linkUrl?index_of("VIEW_INDEX") <= 0 && linkUrl?index_of(viewIndexField) <= 0> diff --git a/themes/common-theme/template/macro/TextFormMacroLibrary.ftl b/themes/common-theme/template/macro/TextFormMacroLibrary.ftl index 228611e..0e97938 100644 --- a/themes/common-theme/template/macro/TextFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/TextFormMacroLibrary.ftl @@ -54,7 +54,7 @@ under the License. <#macro renderEmptyFormDataMessage message></#macro> <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl b/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl index 0998073..0472f2d 100644 --- a/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/XlsFormMacroLibrary.ftl @@ -59,7 +59,7 @@ under the License. <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/template/macro/XmlFormMacroLibrary.ftl b/themes/common-theme/template/macro/XmlFormMacroLibrary.ftl index b8cbc51..acc2f28 100644 --- a/themes/common-theme/template/macro/XmlFormMacroLibrary.ftl +++ b/themes/common-theme/template/macro/XmlFormMacroLibrary.ftl @@ -62,7 +62,7 @@ under the License. <#macro renderEmptyFormDataMessage message></#macro> <#macro renderSingleFormFieldTitle></#macro> -<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField></#macro> +<#macro renderFormOpen linkUrl formType targetWindow containerId containerStyle autocomplete name viewIndexField viewSizeField viewIndex viewSize useRowSubmit focusFieldName hasRequiredField csrfNameValue></#macro> <#macro renderFormClose></#macro> <#macro renderMultiFormClose></#macro> diff --git a/themes/common-theme/webapp/common/js/util/OfbizUtil.js b/themes/common-theme/webapp/common/js/util/OfbizUtil.js index f268519..7636760 100644 --- a/themes/common-theme/webapp/common/js/util/OfbizUtil.js +++ b/themes/common-theme/webapp/common/js/util/OfbizUtil.js @@ -25,6 +25,16 @@ var AJAX_REQUEST_TIMEOUT = 5000; // Add observers on DOM ready. $(document).ready(function() { + // add CSRF token to jQuery AJAX calls to the same domain + jQuery.ajaxPrefilter(function(options, _, jqXHR) { + var token; + if (!options.crossDomain) { + token = jQuery("meta[name='csrf-token']").attr("content") + if (token) { + return jqXHR.setRequestHeader("X-CSRF-Token", token); + } + } + }); //initializing UI combobox dropdown by overriding its methods. ajaxAutoCompleteDropDown(); // bindObservers will add observer on passed html section when DOM is ready. @@ -1218,7 +1228,7 @@ function getJSONuiLabels(requiredLabels, callback) { } } /** - * Read the requiered uiLabel from the uiLabelXml Resource + * Read the required uiLabel from the uiLabelXml Resource * @param uiResource String * @param errUiLabel String * @returns String with Label diff --git a/themes/flatgrey/template/Header.ftl b/themes/flatgrey/template/Header.ftl index 8920f07..bbe4eb3 100644 --- a/themes/flatgrey/template/Header.ftl +++ b/themes/flatgrey/template/Header.ftl @@ -24,6 +24,10 @@ under the License. <html lang="${docLangAttr}" dir="${langDir}" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <#assign csrfDefenseStrategy = Static["org.apache.ofbiz.entity.util.EntityUtilProperties"].getPropertyValue("security", "csrf.defense.strategy", delegator)> + <#if csrfDefenseStrategy != "org.apache.ofbiz.security.NoCsrfDefenseStrategy"> + <meta name="csrf-token" content="<@csrfTokenAjax/>"/> + </#if> <title>${layoutSettings.companyName}: <#if (titleProperty)?has_content>${uiLabelMap[titleProperty]}<#else>${title!}</#if></title> <#if layoutSettings.shortcutIcon?has_content> <#assign shortcutIcon = layoutSettings.shortcutIcon/> @@ -156,7 +160,7 @@ under the License. <#---if webSiteId?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??--> <#if parameters.componentName?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??> <#include "component://common-theme/template/includes/HelpLink.ftl" /> - <li><a <#if pageAvail?has_content>class="alert"</#if> href="javascript:lookup_popup1('showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}','help' ,500,500);">${uiLabelMap.CommonHelp}</a></li> + <li><a <#if pageAvail?has_content>class="alert"</#if> href="javascript:lookup_popup1('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);">${uiLabelMap.CommonHelp}</a></li> </#if> </ul> </li> diff --git a/themes/rainbowstone/template/includes/Header.ftl b/themes/rainbowstone/template/includes/Header.ftl index bb1ad5e..93a8500 100644 --- a/themes/rainbowstone/template/includes/Header.ftl +++ b/themes/rainbowstone/template/includes/Header.ftl @@ -24,6 +24,10 @@ under the License. <html lang="${docLangAttr}" dir="${langDir}" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <#assign csrfDefenseStrategy = Static["org.apache.ofbiz.entity.util.EntityUtilProperties"].getPropertyValue("security", "csrf.defense.strategy", delegator)> + <#if csrfDefenseStrategy != "org.apache.ofbiz.security.NoCsrfDefenseStrategy"> + <meta name="csrf-token" content="<@csrfTokenAjax/>"/> + </#if> <title>${layoutSettings.companyName}: <#if (titleProperty)?has_content>${uiLabelMap[titleProperty]}<#else>${title!}</#if></title> <#if layoutSettings.shortcutIcon?has_content> <#assign shortcutIcon = layoutSettings.shortcutIcon/> diff --git a/themes/rainbowstone/template/includes/TopAppBar.ftl b/themes/rainbowstone/template/includes/TopAppBar.ftl index e82c73b..90cc979 100644 --- a/themes/rainbowstone/template/includes/TopAppBar.ftl +++ b/themes/rainbowstone/template/includes/TopAppBar.ftl @@ -238,7 +238,7 @@ under the License. <div id="main-nav-bar-right"> <div id="company-logo"></div> <#if parameters.componentName?exists && requestAttributes._CURRENT_VIEW_?exists && helpTopic?exists> - <a class="dark-color" title="${uiLabelMap.CommonHelp}" href="javascript:lookup_popup1('showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}','help' ,500,500);"><img class="appbar-btn-img" id="help-btn" src="/rainbowstone/images/help.svg" alt="Help"></a> + <a class="dark-color" title="${uiLabelMap.CommonHelp}" href="javascript:lookup_popup1('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);"><img class="appbar-btn-img" id="help-btn" src="/rainbowstone/images/help.svg" alt="Help"></a> </#if> <#include "component://rainbowstone/template/includes/Avatar.ftl"/> diff --git a/themes/tomahawk/template/AppBarClose.ftl b/themes/tomahawk/template/AppBarClose.ftl index 7475102..c79b4fb 100644 --- a/themes/tomahawk/template/AppBarClose.ftl +++ b/themes/tomahawk/template/AppBarClose.ftl @@ -75,7 +75,7 @@ under the License. <#--if webSiteId?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??--> <#if parameters.componentName?? && requestAttributes._CURRENT_VIEW_?? && helpTopic??> <#include "component://common-theme/template/includes/HelpLink.ftl" /> - <li><a class="help-link <#if pageAvail?has_content> alert</#if>" href="javascript:lookup_popup1('showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> + <li><a class="help-link <#if pageAvail?has_content> alert</#if>" href="javascript:lookup_popup1('<@ofbizUrl>showHelp?helpTopic=${helpTopic}&portalPageId=${(parameters.portalPageId!)?html}</@ofbizUrl>','help' ,500,500);" title="${uiLabelMap.CommonHelp}"></a></li> </#if> <li><a href="<@ofbizUrl>logout</@ofbizUrl>">${uiLabelMap.CommonLogout}</a></li> <li><a href="<@ofbizUrl>ListVisualThemes</@ofbizUrl>">${uiLabelMap.CommonVisualThemes}</a></li> diff --git a/themes/tomahawk/template/Header.ftl b/themes/tomahawk/template/Header.ftl index 3376614..d01ae9c 100644 --- a/themes/tomahawk/template/Header.ftl +++ b/themes/tomahawk/template/Header.ftl @@ -28,6 +28,10 @@ under the License. <html lang="${docLangAttr}" dir="${langDir}" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <#assign csrfDefenseStrategy = Static["org.apache.ofbiz.entity.util.EntityUtilProperties"].getPropertyValue("security", "csrf.defense.strategy", delegator)> + <#if csrfDefenseStrategy != "org.apache.ofbiz.security.NoCsrfDefenseStrategy"> + <meta name="csrf-token" content="<@csrfTokenAjax/>"/> + </#if> <title>${layoutSettings.companyName}: <#if (titleProperty)?has_content>${uiLabelMap[titleProperty]}<#else>${title!}</#if></title> <#if layoutSettings.shortcutIcon?has_content> <#assign shortcutIcon = layoutSettings.shortcutIcon/>