CVE-2020-28191: Cross-Site Request Forgery (CSRF) (CWE-352)
value: The console in Togglz before 2.9.4 allows CSRF.
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 8.8
Published: 2022-12-26T22:15Z
Repository: https://github.com/togglz/togglz
Language: Java
Files changed: console/pom.xml, console/src/main/java/org/togglz/console/handlers/edit/EditPageHandler.java, console/src/main/java/org/togglz/console/security/TogglzCSRFTokenCache.java, console/src/main/java/org/togglz/console/security/TogglzCSRFTokenProvider.java, console/src/main/java/org/togglz/console/security/TogglzCSRFTokenValidator.java, console/src/main/resources/META-INF/services/org.togglz.servlet.spi.CSRFTokenProvider, console/src/main/resources/META-INF/services/org.togglz.servlet.spi.CSRFTokenValidator, console/src/main/resources/org/togglz/console/error.html, console/src/main/resources/org/togglz/console/index.html, servlet/src/main/java/org/togglz/servlet/spi/CSRFTokenValidator.java
Commit date: 2021-01-12 08:19:14 +0100
Commit message: Added CSRF protection to the togglz console via a CSRF token passed between the server and the clinet. This remediates the vulnerabilty CVE-2020-28191 by blocking CSRF attacks as the attcker will not be able to guess the CSRF token value. (#495)

This has been implemented with either the session timeout of the application the togglz console is embedded in. Or if no user session is available it defaults to a 10 minute timeout for the CSRF token.
This CSRF token does not interfere with either OWASP's CSRFGuard or Spring-Security's CSRF protection if they are used within the application.

Co-authored-by: Joseph Beeton <joseph.p.beeton1@aexp.com>
Diff: diff --git a/console/pom.xml b/console/pom.xml
index 2b5b4745..3646d371 100644
--- a/console/pom.xml
+++ b/console/pom.xml
@@ -75,7 +75,13 @@
       <groupId>com.floreysoft</groupId>
       <artifactId>jmte</artifactId>
       <version>6.0.0</version>
     </dependency>
 
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-collections4</artifactId>
+      <version>4.4</version>
+    </dependency>
+
   </dependencies>
 </project>
diff --git a/console/src/main/java/org/togglz/console/handlers/edit/EditPageHandler.java b/console/src/main/java/org/togglz/console/handlers/edit/EditPageHandler.java
index bc479770..59edd721 100644
--- a/console/src/main/java/org/togglz/console/handlers/edit/EditPageHandler.java
+++ b/console/src/main/java/org/togglz/console/handlers/edit/EditPageHandler.java
@@ -17,10 +17,11 @@ import org.togglz.core.metadata.FeatureMetaData;
 import org.togglz.core.repository.FeatureState;
 import org.togglz.core.spi.ActivationStrategy;
 
 import com.floreysoft.jmte.Engine;
 import org.togglz.core.util.Services;
+import org.togglz.servlet.spi.CSRFTokenValidator;
 
 public class EditPageHandler extends RequestHandlerBase {
 
     @Override
     public boolean handles(String path) {
@@ -35,11 +36,14 @@ public class EditPageHandler extends RequestHandlerBase {
     @Override
     public void process(RequestEvent event) throws IOException {
         FeatureManager featureManager = event.getFeatureManager();
         HttpServletRequest request = event.getRequest();
         HttpServletResponse response = event.getResponse();
-
+		if(!validateCSRFToken(event)) {
+			renderErrorPage(event);
+			return;
+		}
         // identify the feature
         Feature feature = null;
         String featureAsString = request.getParameter("f");
         for (Feature f : featureManager.getFeatures()) {
             if (f.name().equals(featureAsString)) {
@@ -76,29 +80,45 @@ public class EditPageHandler extends RequestHandlerBase {
                 FeatureState state = featureModel.toFeatureState();
                 featureManager.setFeatureState(state);
                 response.sendRedirect("index");
 
             }
-
             // got validation errors
             else {
                 renderEditPage(event, featureModel);
             }
 
         }
 
     }
 
+    private boolean validateCSRFToken(RequestEvent event) {
+    	boolean isValid = true;
+		for (CSRFTokenValidator validator : Services.get(CSRFTokenValidator.class)) {
+			if(!validator.isTokenValid(event.getRequest())){
+				isValid = false;
+				break;
+			}
+		}
+		return isValid;
+	}
+
+    private void renderErrorPage(RequestEvent event) throws IOException {
+		String template = getResourceAsString("error.html");
+		String content = new Engine().transform(template, new HashMap<>());
+		event.getResponse().setStatus(401);
+		writeResponse(event, content);
+	}
+
     private void renderEditPage(RequestEvent event, FeatureModel featureModel) throws IOException {
         List<CSRFToken> tokens = new ArrayList<>();
         for (CSRFTokenProvider provider : Services.get(CSRFTokenProvider.class)) {
             CSRFToken token = provider.getToken(event.getRequest());
             if (token != null) {
                 tokens.add(token);
             }
         }
-
         Map<String, Object> model = new HashMap<>();
         model.put("model", featureModel);
         model.put("tokens", tokens);
 
         String template = getResourceAsString("edit.html");
diff --git a/console/src/main/java/org/togglz/console/security/TogglzCSRFTokenCache.java b/console/src/main/java/org/togglz/console/security/TogglzCSRFTokenCache.java
new file mode 100644
index 00000000..6d5dc78f
--- /dev/null
+++ b/console/src/main/java/org/togglz/console/security/TogglzCSRFTokenCache.java
@@ -0,0 +1,32 @@
+package org.togglz.console.security;
+
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.collections4.map.PassiveExpiringMap;
+import org.togglz.servlet.spi.CSRFToken;
+
+public class TogglzCSRFTokenCache {
+
+	private static final PassiveExpiringMap<String, CSRFToken> expiringMap;
+	private static final Object lock = new Object();
+	static {
+		PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<String, CSRFToken>
+				expirationPolicy = new PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<>(
+				10, TimeUnit.MINUTES);
+		expiringMap = new PassiveExpiringMap<>(expirationPolicy, new HashMap<>());
+	}
+
+	public static void cacheToken(CSRFToken token) {
+		synchronized (lock) {
+			expiringMap.put(token.getValue(), token);
+		}
+	}
+
+	public static boolean isTokenInCache(CSRFToken token) {
+		synchronized (lock) {
+			return expiringMap.containsKey(token.getValue());
+		}
+	}
+	
+}
\ No newline at end of file
diff --git a/console/src/main/java/org/togglz/console/security/TogglzCSRFTokenProvider.java b/console/src/main/java/org/togglz/console/security/TogglzCSRFTokenProvider.java
new file mode 100644
index 00000000..958a1fc2
--- /dev/null
+++ b/console/src/main/java/org/togglz/console/security/TogglzCSRFTokenProvider.java
@@ -0,0 +1,25 @@
+package org.togglz.console.security;
+
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.togglz.servlet.spi.CSRFToken;
+import org.togglz.servlet.spi.CSRFTokenProvider;
+
+import static org.togglz.console.security.TogglzCSRFTokenValidator.CSRF_TOKEN_NAME;
+
+public class TogglzCSRFTokenProvider implements CSRFTokenProvider {
+
+	@Override
+	public CSRFToken getToken(HttpServletRequest request) {
+		CSRFToken token;
+		if (request.getAttribute(CSRF_TOKEN_NAME) == null) {
+			token = new CSRFToken(CSRF_TOKEN_NAME, UUID.randomUUID().toString());
+			TogglzCSRFTokenCache.cacheToken(token);
+		} else {
+			token = new CSRFToken(CSRF_TOKEN_NAME, request.getAttribute(CSRF_TOKEN_NAME).toString());
+		}
+		return token;
+	}
+}
\ No newline at end of file
diff --git a/console/src/main/java/org/togglz/console/security/TogglzCSRFTokenValidator.java b/console/src/main/java/org/togglz/console/security/TogglzCSRFTokenValidator.java
new file mode 100644
index 00000000..5b964d09
--- /dev/null
+++ b/console/src/main/java/org/togglz/console/security/TogglzCSRFTokenValidator.java
@@ -0,0 +1,22 @@
+package org.togglz.console.security;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.togglz.servlet.spi.CSRFToken;
+import org.togglz.servlet.spi.CSRFTokenValidator;
+
+public class TogglzCSRFTokenValidator implements CSRFTokenValidator {
+
+
+	public static final String CSRF_TOKEN_NAME = "togglz_csrf";
+
+	@Override
+	public boolean isTokenValid(HttpServletRequest request) {
+		String token = request.getParameter(CSRF_TOKEN_NAME);
+		if(token==null) {
+			return false;
+		} else {
+			return TogglzCSRFTokenCache.isTokenInCache(new CSRFToken(CSRF_TOKEN_NAME,token));
+		}
+	}
+}
diff --git a/console/src/main/resources/META-INF/services/org.togglz.servlet.spi.CSRFTokenProvider b/console/src/main/resources/META-INF/services/org.togglz.servlet.spi.CSRFTokenProvider
new file mode 100644
index 00000000..782a4c5d
--- /dev/null
+++ b/console/src/main/resources/META-INF/services/org.togglz.servlet.spi.CSRFTokenProvider
@@ -0,0 +1 @@
+org.togglz.console.security.TogglzCSRFTokenProvider
\ No newline at end of file
diff --git a/console/src/main/resources/META-INF/services/org.togglz.servlet.spi.CSRFTokenValidator b/console/src/main/resources/META-INF/services/org.togglz.servlet.spi.CSRFTokenValidator
new file mode 100644
index 00000000..d984ff63
--- /dev/null
+++ b/console/src/main/resources/META-INF/services/org.togglz.servlet.spi.CSRFTokenValidator
@@ -0,0 +1 @@
+org.togglz.console.security.TogglzCSRFTokenValidator
\ No newline at end of file
diff --git a/console/src/main/resources/org/togglz/console/error.html b/console/src/main/resources/org/togglz/console/error.html
new file mode 100644
index 00000000..83006afe
--- /dev/null
+++ b/console/src/main/resources/org/togglz/console/error.html
@@ -0,0 +1,4 @@
+<div style="text-align: center;">
+    <h1>ERROR</h1>
+    <h1><small>Invalid CSRF Token, please refresh browser from the main Togglz page.</small></h1>
+</div>
\ No newline at end of file
diff --git a/console/src/main/resources/org/togglz/console/index.html b/console/src/main/resources/org/togglz/console/index.html
index b76ea351..4d8fea62 100644
--- a/console/src/main/resources/org/togglz/console/index.html
+++ b/console/src/main/resources/org/togglz/console/index.html
@@ -65,12 +65,11 @@
                   ${end}
                   </ul>
                 ${end}
               </td>
               <td class="feature-actions">
-                <a class="btn btn-sm btn-default" href="edit?f=${feature.name}" title="Edit ${feature.label} feature">
-                  <span class="glyphicon glyphicon-cog text-muted"></span>
+                  <a class="btn btn-sm btn-default" href="edit?f=${feature.name}${foreach tokens token}${if token.name = "togglz_csrf"}&${token.name}=${token.value}${end}${end}" title="Edit ${feature.label} feature">                  <span class="glyphicon glyphicon-cog text-muted"></span>
                 </a>
               </td>
             </tr>
           ${end}
 
diff --git a/servlet/src/main/java/org/togglz/servlet/spi/CSRFTokenValidator.java b/servlet/src/main/java/org/togglz/servlet/spi/CSRFTokenValidator.java
new file mode 100644
index 00000000..99ff4422
--- /dev/null
+++ b/servlet/src/main/java/org/togglz/servlet/spi/CSRFTokenValidator.java
@@ -0,0 +1,9 @@
+package org.togglz.servlet.spi;
+
+import javax.servlet.http.HttpServletRequest;
+
+public interface CSRFTokenValidator {
+
+
+	boolean isTokenValid(HttpServletRequest request);
+}

Vulnerable code: 


                <a class="btn btn-sm btn-default" href="edit?f=${feature.name}" title="Edit ${feature.label} feature">
                  <span class="glyphicon glyphicon-cog text-muted"></span>
Fixed code:     <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-collections4</artifactId>
      <version>4.4</version>
    </dependency>

import org.togglz.servlet.spi.CSRFTokenValidator;
		if(!validateCSRFToken(event)) {
			renderErrorPage(event);
			return;
		}
    private boolean validateCSRFToken(RequestEvent event) {
    	boolean isValid = true;
		for (CSRFTokenValidator validator : Services.get(CSRFTokenValidator.class)) {
			if(!validator.isTokenValid(event.getRequest())){
				isValid = false;
				break;
			}
		}
		return isValid;
	}

    private void renderErrorPage(RequestEvent event) throws IOException {
		String template = getResourceAsString("error.html");
		String content = new Engine().transform(template, new HashMap<>());
		event.getResponse().setStatus(401);
		writeResponse(event, content);
	}

package org.togglz.console.security;

import java.util.HashMap;
import java.util.concurrent.TimeUnit;

import org.apache.commons.collections4.map.PassiveExpiringMap;
import org.togglz.servlet.spi.CSRFToken;

public class TogglzCSRFTokenCache {

	private static final PassiveExpiringMap<String, CSRFToken> expiringMap;
	private static final Object lock = new Object();
	static {
		PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<String, CSRFToken>
				expirationPolicy = new PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<>(
				10, TimeUnit.MINUTES);
		expiringMap = new PassiveExpiringMap<>(expirationPolicy, new HashMap<>());
	}

	public static void cacheToken(CSRFToken token) {
		synchronized (lock) {
			expiringMap.put(token.getValue(), token);
		}
	}

	public static boolean isTokenInCache(CSRFToken token) {
		synchronized (lock) {
			return expiringMap.containsKey(token.getValue());
		}
	}
	
}
package org.togglz.console.security;

import java.util.UUID;

import javax.servlet.http.HttpServletRequest;

import org.togglz.servlet.spi.CSRFToken;
import org.togglz.servlet.spi.CSRFTokenProvider;

import static org.togglz.console.security.TogglzCSRFTokenValidator.CSRF_TOKEN_NAME;

public class TogglzCSRFTokenProvider implements CSRFTokenProvider {

	@Override
	public CSRFToken getToken(HttpServletRequest request) {
		CSRFToken token;
		if (request.getAttribute(CSRF_TOKEN_NAME) == null) {
			token = new CSRFToken(CSRF_TOKEN_NAME, UUID.randomUUID().toString());
			TogglzCSRFTokenCache.cacheToken(token);
		} else {
			token = new CSRFToken(CSRF_TOKEN_NAME, request.getAttribute(CSRF_TOKEN_NAME).toString());
		}
		return token;
	}
}
package org.togglz.console.security;

import javax.servlet.http.HttpServletRequest;

import org.togglz.servlet.spi.CSRFToken;
import org.togglz.servlet.spi.CSRFTokenValidator;

public class TogglzCSRFTokenValidator implements CSRFTokenValidator {


	public static final String CSRF_TOKEN_NAME = "togglz_csrf";

	@Override
	public boolean isTokenValid(HttpServletRequest request) {
		String token = request.getParameter(CSRF_TOKEN_NAME);
		if(token==null) {
			return false;
		} else {
			return TogglzCSRFTokenCache.isTokenInCache(new CSRFToken(CSRF_TOKEN_NAME,token));
		}
	}
}
org.togglz.console.security.TogglzCSRFTokenProvider
org.togglz.console.security.TogglzCSRFTokenValidator
<div style="text-align: center;">
    <h1>ERROR</h1>
    <h1><small>Invalid CSRF Token, please refresh browser from the main Togglz page.</small></h1>
</div>
                  <a class="btn btn-sm btn-default" href="edit?f=${feature.name}${foreach tokens token}${if token.name = "togglz_csrf"}&${token.name}=${token.value}${end}${end}" title="Edit ${feature.label} feature">                  <span class="glyphicon glyphicon-cog text-muted"></span>
package org.togglz.servlet.spi;

import javax.servlet.http.HttpServletRequest;

public interface CSRFTokenValidator {


	boolean isTokenValid(HttpServletRequest request);
}