CVE-2021-25930: Cross-Site Request Forgery (CSRF) (CWE-352)
value: In OpenNMS Horizon, versions opennms-1-0-stable through opennms-27.1.0-1; OpenNMS Meridian, versions meridian-foundation-2015.1.0-1 through meridian-foundation-2019.1.18-1; meridian-foundation-2020.1.0-1 through meridian-foundation-2020.1.6-1 are vulnerable to CSRF, due to no CSRF protection, and since there is no validation of an existing user name while renaming a user. As a result, privileges of the renamed user are being overwritten by the old user and the old user is being deleted from the user list.
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: 4.3
Published: 2021-05-20T14:15Z
Repository: https://github.com/OpenNMS/opennms
Language: Other
Files changed: opennms-config/src/main/java/org/opennms/netmgt/config/UserManager.java, opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml, opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/list.jsp, opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/modifyGroup.jsp, opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/newGroup.jsp, opennms-webapp/src/main/webapp/admin/userGroupView/roles/editDetails.jsp, opennms-webapp/src/main/webapp/admin/userGroupView/roles/editSpecific.jsp, opennms-webapp/src/main/webapp/admin/userGroupView/roles/list.jsp, opennms-webapp/src/main/webapp/admin/userGroupView/roles/view.jsp, opennms-webapp/src/main/webapp/admin/userGroupView/users/list.jsp, opennms-webapp/src/main/webapp/admin/userGroupView/users/modifyUser.jsp, opennms-webapp/src/main/webapp/admin/userGroupView/users/newPassword.jsp, opennms-webapp/src/main/webapp/admin/userGroupView/users/newUser.jsp, smoke-test/src/test/java/org/opennms/smoketest/UserIT.java
Commit date: 2021-03-12 17:49:14 +0100
Commit message: NMS-13124: Fixed user deletion by renaming bug and CSRF privilege escalation issue


Diff: diff --git a/opennms-config/src/main/java/org/opennms/netmgt/config/UserManager.java b/opennms-config/src/main/java/org/opennms/netmgt/config/UserManager.java
index f10649f716b..05fadc9ded7 100644
--- a/opennms-config/src/main/java/org/opennms/netmgt/config/UserManager.java
+++ b/opennms-config/src/main/java/org/opennms/netmgt/config/UserManager.java
@@ -993,10 +993,14 @@ public abstract class UserManager implements UserConfig {
                 final User data = m_users.get(oldName);
                 if (data == null) {
                     m_users.remove(oldName);
                     throw new Exception("UserFactory:rename the data contained for old user " + oldName + " is null");
                 } else {
+                    if (m_users.containsKey(newName)) {
+                        throw new Exception("UserFactory: cannot rename user " + oldName + ". An user with the given name " + newName + " already exists");
+                    }
+
                     // Rename the user in the user map.
                     m_users.remove(oldName);
                     data.setUserId(newName);
                     m_users.put(newName, data);
         
diff --git a/opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml b/opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml
index fb52db0a799..af3f1cce4b8 100644
--- a/opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml
+++ b/opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml
@@ -120,10 +120,22 @@
   </http>
   
   <!-- Allow anonymous access to the webstart portion of the app -->
   <http pattern="/webstart/**" security="none"/>
 
+  <http pattern="/admin/userGroupView/**" use-expressions="true" access-denied-page="/accessDenied.jsp" realm="OpenNMS Realm" auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
+    <!-- see NMS-13124 -->
+    <csrf />
+    <intercept-url pattern="/admin/userGroupView/**" access="hasAnyRole('ROLE_ADMIN')" />
+    <http-basic entry-point-ref="xRequestedWithAwareBasicAuthEntryPoint" />
+    <logout logout-success-url="/" />
+    <custom-filter position="FORM_LOGIN_FILTER" ref="onmsUsernamePasswordAuthenticationFilter" />
+    <custom-filter position="PRE_AUTH_FILTER" ref="attributePreAuthFilter"/>
+    <custom-filter after="PRE_AUTH_FILTER" ref="headerPreAuthFilter"/>
+    <custom-filter position="LAST" ref="authFilterEnabler"/>
+  </http>
+
   <!-- Only one <http> section can match the implicit '/**' pattern -->
   <http pattern="/**" use-expressions="true" access-denied-page="/accessDenied.jsp" realm="OpenNMS Realm" auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
     <intercept-url pattern="/" access="hasAnyRole('ROLE_ANONYMOUS','ROLE_USER','ROLE_DASHBOARD')" />
     <intercept-url pattern="/favicon.ico" access="hasAnyRole('ROLE_ANONYMOUS','ROLE_USER','ROLE_DASHBOARD')" />
     <intercept-url pattern="/apple-touch-icon.png" access="hasAnyRole('ROLE_ANONYMOUS','ROLE_USER','ROLE_DASHBOARD')" />
@@ -156,11 +168,11 @@
     <intercept-url pattern="/gwt.js" access="hasAnyRole('ROLE_USER','ROLE_DASHBOARD')" />
     <intercept-url pattern="/surveillanceservice.gwt" access="hasAnyRole('ROLE_USER','ROLE_DASHBOARD')" />
     <intercept-url pattern="/graph/graph.png" access="hasAnyRole('ROLE_USER','ROLE_DASHBOARD')" />
     <intercept-url pattern="/dashboard/**" access="hasAnyRole('ROLE_USER','ROLE_DASHBOARD')" />
     <intercept-url pattern="/coreweb/**" access="hasAnyRole('ROLE_USER','ROLE_DASHBOARD')" />
-    
+
     <!-- DO NOT ALLOW ACCESS TO /osgi -->
     <intercept-url pattern="/osgi/**" access="denyAll" />
 
     <!-- Exclude jsp includes other than bootstrap, they shouldn't be accessible by themself -->
     <intercept-url pattern="/includes/bootstrap.jsp" access="permitAll" />
diff --git a/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/list.jsp b/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/list.jsp
index f82e15920e3..05082802a81 100644
--- a/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/list.jsp
+++ b/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/list.jsp
@@ -100,10 +100,11 @@
 
 <form method="post" name="allGroups">
   <input type="hidden" name="operation"/>
   <input type="hidden" name="groupName"/>
   <input type="hidden" name="newName"/>
+  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 
   <div class="card">
     <table class="table table-sm table-bordered">
          <tr>
           <th>Delete</th>
diff --git a/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/modifyGroup.jsp b/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/modifyGroup.jsp
index c70e61e3487..f7d6eadda0b 100644
--- a/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/modifyGroup.jsp
+++ b/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/modifyGroup.jsp
@@ -290,10 +290,11 @@
 </script>
 
 <form role="form" class="form" method="post" id="modifyGroup" name="modifyGroup">
   <input type="hidden" name="groupName" value="<%=group.getName()%>"/>
   <input type="hidden" name="operation"/>
+  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 
     <div class="card">
         <div class="card-header">
             <span>Assignments</span>
         </div>
diff --git a/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/newGroup.jsp b/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/newGroup.jsp
index 7f6d06db02c..90e078543d0 100644
--- a/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/newGroup.jsp
+++ b/opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/newGroup.jsp
@@ -74,10 +74,11 @@
     <%}%>
   </div>
   <div class="card-body">
     <form role="form" class="form" id="newGroupForm" method="post" name="newGroupForm" onsubmit="return validateFormInput();">
       <input type="hidden" name="operation" />
+      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 
       <div class="form-group">
         <label for="groupName" class="">Group Name</label>
       <input class="form-control" id="groupName" type="text" name="groupName"/>
       </div>
diff --git a/opennms-webapp/src/main/webapp/admin/userGroupView/roles/editDetails.jsp b/opennms-webapp/src/main/webapp/admin/userGroupView/roles/editDetails.jsp
index 7aa44abc87b..ce6047acb54 100644
--- a/opennms-webapp/src/main/webapp/admin/userGroupView/roles/editDetails.jsp
+++ b/opennms-webapp/src/main/webapp/admin/userGroupView/roles/editDetails.jsp
@@ -68,10 +68,11 @@
   <div class="card-header">
     <span>Edit On-Call Role</span>
   </div>
   <div class="card-body">
     <form role="form" class="form" action="<c:url value='${reqUrl}'/>" method="post" name="editForm">
+      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
       <input type="hidden" name="operation" value="saveDetails"/>
       <input type="hidden" name="role" value="${fn:escapeXml(role.name)}"/>
 
       <div class="form-group form-row">
         <label for="input_roleName" class="col-sm-2 col-form-label">Name</label>
diff --git a/opennms-webapp/src/main/webapp/admin/userGroupView/roles/editSpecific.jsp b/opennms-webapp/src/main/webapp/admin/userGroupView/roles/editSpecific.jsp
index c086a2d1858..185b17a8ac4 100644
--- a/opennms-webapp/src/main/webapp/admin/userGroupView/roles/editSpecific.jsp
+++ b/opennms-webapp/src/main/webapp/admin/userGroupView/roles/editSpecific.jsp
@@ -57,11 +57,12 @@
     <p class="alert alert-danger">${error}</p>
     <form role="form" class="form" action="<c:url value='${reqUrl}'/>" method="post" name="saveEntryForm">
       <input type="hidden" name="operation" value="saveEntry"/>
       <input type="hidden" name="role" value="${fn:escapeXml(role.name)}"/>
       <input type="hidden" name="schedIndex" value="${schedIndex}"/>
-      <input type="hidden" name="timeIndex" value="${timeIndex}" /> 
+      <input type="hidden" name="timeIndex" value="${timeIndex}" />
+      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 
       <div class="form-group form-row">
         <label class="col-sm-2">On-Call Role</label>
         <div class="col-sm-4">
           <p class="form-control-static"><c:out value="${role.name}"/></p>
diff --git a/opennms-webapp/src/main/webapp/admin/userGroupView/roles/list.jsp b/opennms-webapp/src/main/webapp/admin/userGroupView/roles/list.jsp
index ef1112ffaae..9420a7aa8c3 100644
--- a/opennms-webapp/src/main/webapp/admin/userGroupView/roles/list.jsp
+++ b/opennms-webapp/src/main/webapp/admin/userGroupView/roles/list.jsp
@@ -91,10 +91,11 @@
 </script>
 
 <form action="<c:url value='${reqUrl}'/>" method="post" name="roleForm">
 	<input type="hidden" name="operation" />
 	<input type="hidden" name="role" />
+	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 </form>
 
 <div class="card">
   <div class="card-header">
     <span>On-Call Role Configuration</span>
@@ -146,10 +147,11 @@
 	 	</c:choose>
   </table>
 </div> <!-- panel -->
 
 <form action="<c:url value='${reqUrl}'/>" method="post" name="newForm">
+  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
   <input name="operation" type="hidden" value="new"/>
   <button type="submit" class="btn btn-secondary">Add New On-Call Role</button>
 </form>
 
 <jsp:include page="/includes/bootstrap-footer.jsp" flush="false" />
diff --git a/opennms-webapp/src/main/webapp/admin/userGroupView/roles/view.jsp b/opennms-webapp/src/main/webapp/admin/userGroupView/roles/view.jsp
index e68648cac2e..d1554b4c649 100644
--- a/opennms-webapp/src/main/webapp/admin/userGroupView/roles/view.jsp
+++ b/opennms-webapp/src/main/webapp/admin/userGroupView/roles/view.jsp
@@ -155,14 +155,16 @@
 </div> <!-- panel -->
 
 <form action="<c:url value='${reqUrl}'/>" method="post" name="editForm">
   <input type="hidden" name="operation" value="editDetails"/>
   <input type="hidden" name="role" value="${fn:escapeXml(role.name)}"/>
+  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
   <button type="submit" class="btn btn-secondary">Value Details</button>
 </form>
 
 <form action="<c:url value='${reqUrl}'/>" method="post" name="doneForm" class="my-4">
+  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
   <button type="submit" class="btn btn-secondary">Done</button>
 </form>
 
 <div class="card top-buffer">
   <div class="card-header">
@@ -218,9 +220,10 @@
 				</c:forEach>
 			</table>
 </div>
 
 <form action="<c:url value='${reqUrl}'/>" method="post" name="doneForm" class="mb-4">
+  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
   <button type="submit" class="btn btn-secondary">Done</button>
 </form>
 
 <jsp:include page="/includes/bootstrap-footer.jsp" flush="false" />
diff --git a/opennms-webapp/src/main/webapp/admin/userGroupView/users/list.jsp b/opennms-webapp/src/main/webapp/admin/userGroupView/users/list.jsp
index 1250fa5c183..682adb4b46b 100644
--- a/opennms-webapp/src/main/webapp/admin/userGroupView/users/list.jsp
+++ b/opennms-webapp/src/main/webapp/admin/userGroupView/users/list.jsp
@@ -93,13 +93,23 @@
     
     function renameUser(userID)
     {
         document.allUsers.userID.value=userID;
         var newID = prompt("Enter new name for user.", userID);
-        
-        if (newID != null && newID != "")
-        {
+
+        if (newID != null && newID != "") {
+          if (/.*[&<>"`']+.*/.test(newID)) {
+            alert("The user ID must not contain any HTML markup.");
+            return;
+          }
+
+          var element =  document.getElementById('users(' + _.escape(newID) + ').doModify');
+          if (typeof(element) != 'undefined' && element != null) {
+            alert("A user with this ID already exist.");
+            return;
+          }
+
           document.allUsers.newID.value = newID;
           document.allUsers.action="admin/userGroupView/users/renameUser";
           document.allUsers.submit();
         }
     }
@@ -110,10 +120,11 @@
 <form method="post" name="allUsers">
 <input type="hidden" name="redirect"/>
 <input type="hidden" name="userID"/>
 <input type="hidden" name="newID"/>
 <input type="hidden" name="password"/>
+<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 
 <p>
   Click on the <i>User ID</i> link to view detailed information about a
   user.
 </p>
@@ -207,6 +218,10 @@
        </tbody>
      </table>
   </div> <!-- panel -->
 </form>
 
+<jsp:include page="/assets/load-assets.jsp" flush="false">
+    <jsp:param name="asset" value="underscore-js" />
+</jsp:include>
+
 <jsp:include page="/includes/bootstrap-footer.jsp" flush="false" />
diff --git a/opennms-webapp/src/main/webapp/admin/userGroupView/users/modifyUser.jsp b/opennms-webapp/src/main/webapp/admin/userGroupView/users/modifyUser.jsp
index 2c02c352a6a..b6f97ef6cf0 100644
--- a/opennms-webapp/src/main/webapp/admin/userGroupView/users/modifyUser.jsp
+++ b/opennms-webapp/src/main/webapp/admin/userGroupView/users/modifyUser.jsp
@@ -254,10 +254,11 @@
 
 <form role="form" class="form-horizontal" id="modifyUser" method="post" name="modifyUser">
 <input id="userID" type="hidden" name="userID" value="<%=user.getUserId()%>"/>
 <input id="password" type="hidden" name="password"/>
 <input id="redirect" type="hidden" name="redirect"/>
+<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
 
 <div class="row">
   <div class="col-md-6">
     <div class="card">
       <div class="card-header">
diff --git a/opennms-webapp/src/main/webapp/admin/userGroupView/users/newPassword.jsp b/opennms-webapp/src/main/webapp/admin/userGroupView/users/newPassword.jsp
index 877489f2176..e12fabf20df 100644
--- a/opennms-webapp/src/main/webapp/admin/userGroupView/users/newPassword.jsp
+++ b/opennms-webapp/src/main/webapp/admin/userGroupView/users/newPassword.jsp
@@ -64,10 +64,12 @@
   <div class="card-header">
     <span>Please enter a new password and confirm</span>
   </div>
   <div class="card-body">
     <form role="form" class="form" method="post" name="goForm">
+      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
+
       <div class="form-group">
         <label for="pass1" class="">Password</label>
         <input type="password" class="form-control" id="pass1" name="pass1">
       </div>
   
diff --git a/opennms-webapp/src/main/webapp/admin/userGroupView/users/newUser.jsp b/opennms-webapp/src/main/webapp/admin/userGroupView/users/newUser.jsp
index b07ef034cc7..7b862f85bef 100644
--- a/opennms-webapp/src/main/webapp/admin/userGroupView/users/newUser.jsp
+++ b/opennms-webapp/src/main/webapp/admin/userGroupView/users/newUser.jsp
@@ -80,10 +80,11 @@
       <span>Please enter a user ID and password below</span>
     <%}%>
   </div>
   <div class="card-body">
     <form class="form" role="form" id="newUserForm" method="post" name="newUserForm" onsubmit="return validateFormInput();">
+      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
       <div class="form-group">
         <label for="userID" class="">User ID</label>
         <input id="userID" type="text" name="userID" class="form-control">
       </div>
       <div class="form-group">
diff --git a/smoke-test/src/test/java/org/opennms/smoketest/UserIT.java b/smoke-test/src/test/java/org/opennms/smoketest/UserIT.java
index 688e015883f..8c9beaee494 100644
--- a/smoke-test/src/test/java/org/opennms/smoketest/UserIT.java
+++ b/smoke-test/src/test/java/org/opennms/smoketest/UserIT.java
@@ -29,10 +29,12 @@
 package org.opennms.smoketest;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
 import java.util.List;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.openqa.selenium.Alert;
@@ -156,6 +158,170 @@ public class UserIT extends OpenNMSSeleniumIT {
         enterText(By.cssSelector("input[type=password][name=pass2]"), "admin");
         driver.findElement(By.cssSelector("button[type=submit]")).click();
         assertTrue(wait.until(pageContainsText("Password successfully changed")));
     }
 
+    @Test
+    public void testInvalidUserIds() {
+        testInvalidUserId("John<b>Doe</b>",true);
+        testInvalidUserId("Jane'Doe'",true);
+        testInvalidUserId("John&Doe",true);
+        testInvalidUserId("Jane\"\"Doe",true);
+    }
+
+    @Test
+    public void testValidUserIds() {
+        testInvalidUserId("John-Doe",false);
+        testInvalidUserId("Jane/Doe",false);
+        testInvalidUserId("John.Doe",false);
+        testInvalidUserId("Jane#Doe", false);
+        testInvalidUserId("John@Döe.com", false);
+        testInvalidUserId("JohnDoé", false);
+    }
+
+    @Test
+    public void testInvalidGroupIds() {
+        testInvalidGroupId("John<b>Doe</b>",true);
+        testInvalidGroupId("Jane'Doe'",true);
+        testInvalidGroupId("John&Doe",true);
+        testInvalidGroupId("Jane\"\"Doe",true);
+    }
+
+    @Test
+    public void testValidGroupIds() {
+        testInvalidGroupId("John-Doe",false);
+        testInvalidGroupId("Jane/Doe",false);
+        testInvalidGroupId("John.Doe",false);
+        testInvalidGroupId("Jane#Doe", false);
+        testInvalidGroupId("John@Döe.com", false);
+        testInvalidGroupId("JohnDoé", false);
+    }
+
+    public void testInvalidUserId(final String userId, final boolean mustFail) {
+        adminPage();
+        findElementByLink("Configure Users, Groups and On-Call Roles").click();
+        findElementByLink("Configure Users").click();
+        findElementByLink("Add new user").click();
+
+        enterText(By.id("userID"), userId);
+        enterText(By.id("pass1"), "SmokeTestPassword");
+        enterText(By.id("pass2"), "SmokeTestPassword");
+        findElementByXpath("//button[@type='submit' and text()='OK']").click();
+
+        if (mustFail) {
+            try {
+                final Alert alert = wait.withTimeout(Duration.of(5, ChronoUnit.SECONDS)).until(ExpectedConditions.alertIsPresent());
+                alert.dismiss();
+            } catch (final Exception e) {
+                LOG.debug("Got an exception waiting for a 'invalid user ID' alert.", e);
+                throw e;
+            }
+        } else {
+            wait.until(ExpectedConditions.elementToBeClickable(By.name("finish")));
+        }
+    }
+
+    public void testInvalidGroupId(final String groupId, final boolean mustFail) {
+        adminPage();
+        findElementByLink("Configure Users, Groups and On-Call Roles").click();
+        findElementByLink("Configure Groups").click();
+        findElementByLink("Add new group").click();
+
+        enterText(By.id("groupName"), groupId);
+        enterText(By.id("groupComment"), "SmokeTestComment");
+        findElementByXpath("//button[@type='submit' and text()='OK']").click();
+
+        if (mustFail) {
+            try {
+                final Alert alert = wait.withTimeout(Duration.of(5, ChronoUnit.SECONDS)).until(ExpectedConditions.alertIsPresent());
+                alert.dismiss();
+            } catch (final Exception e) {
+                LOG.debug("Got an exception waiting for a 'invalid group ID' alert.", e);
+                throw e;
+            }
+        } else {
+            wait.until(ExpectedConditions.elementToBeClickable(By.name("finish")));
+        }
+    }
+
+    /**
+     * see NMS-13124
+     */
+    @Test
+    public void testCsrfPrivilegeEscalation() {
+        // visit the admin's user page
+        adminPage();
+        findElementByLink("Configure Users, Groups and On-Call Roles").click();
+        findElementByLink("Configure Users").click();
+        findElementByLink("Add new user").click();
+
+        // add a new user 'user'
+        enterText(By.id("userID"), "user");
+        enterText(By.id("pass1"), "pass");
+        enterText(By.id("pass2"), "pass");
+        findElementByXpath("//button[@type='submit' and text()='OK']").click();
+
+        // assign just the ROLE_USER
+        final Select select = new Select(driver.findElement(By.name("availableRoles")));
+        select.selectByValue("ROLE_USER");
+        findElementById("roles.doAdd").click();
+        findElementById("saveUserButton").click();
+
+        // assert that this is correctly set
+        adminPage();
+        findElementByLink("Configure Users, Groups and On-Call Roles").click();
+        findElementByLink("Configure Users").click();
+        findElementById("users(user).doDetails").click();
+
+        assertTrue(wait.until(pageContainsText("ROLE_USER")));
+
+        // now construct an exploit to set ROLE_ADMIN for user 'user'
+        final String html = "<form action='" + stack.opennms().getBaseUrlInternal() + "opennms/admin/userGroupView/users/updateUser' method='POST' enctype='application/x-www-form-urlencoded'>" +
+                "<input type='hidden' name='userID' value='user' />" +
+                "<input type='hidden' name='password' value=' ' />" +
+                "<input type='hidden' name='redirect' value='/admin/userGroupView/users/saveUser' /> <input type='hidden' name='fullName' value=' ' />" +
+                "<input type='hidden' name='userComments' value=' ' />" +
+                "<input type='hidden' name='configuredRoles' value='ROLE_ADMIN' />" +
+                "<input type='hidden' name='email' value=' ' />" +
+                "<input type='hidden' name='pemail' value=' ' />" +
+                "<input type='hidden' name='xmppAddress' value=' ' />" +
+                "<input type='hidden' name='microblog' value=' ' />" +
+                "<input type='hidden' name='numericalService' value=' ' />" +
+                "<input type='hidden' name='numericalPin' value=' ' />" +
+                "<input type='hidden' name='textService' value=' ' />" +
+                "<input type='hidden' name='textPin' value=' ' />" +
+                "<input type='hidden' name='workPhone' value=' ' />" +
+                "<input type='hidden' name='mobilePhone' value=' ' />" +
+                "<input type='hidden' name='homePhone' value=' ' />" +
+                "<input type='hidden' name='tuiPin' value=' ' />" +
+                "<input type='hidden' name='timeZoneId' value=' ' />" +
+                "<input type='hidden' name='dutySchedules' value='0' />" +
+                "<input type='hidden' name='numSchedules' value='1' />" +
+                "<input type='submit' id='submitIt' />" +
+                "</form>";
+        String script = "var foo = document.createElement('div'); " +
+                "foo.innerHTML=\"" + html + "\"; " +
+                "document.body.appendChild(foo)";
+
+        // ...and execute it
+        driver.executeScript(script);
+        findElementById("submitIt").click();
+
+        // this should be denied due to CSRF protection
+        assertTrue(wait.until(pageContainsText("Access denied")));
+
+        // assure that the user's role is still ROLE_USER
+        adminPage();
+        findElementByLink("Configure Users, Groups and On-Call Roles").click();
+        findElementByLink("Configure Users").click();
+        findElementById("users(user).doDetails").click();
+        assertTrue(wait.until(pageContainsText("ROLE_USER")));
+
+        // delete the user
+        adminPage();
+        findElementByLink("Configure Users, Groups and On-Call Roles").click();
+        findElementByLink("Configure Users").click();
+
+        findElementById("users(user).doDelete").click();
+        handleAlert("Are you sure you want to delete the user user?");
+    }
 }

Vulnerable code:     
      <input type="hidden" name="timeIndex" value="${timeIndex}" /> 
        
        if (newID != null && newID != "")
        {
Fixed code:                     if (m_users.containsKey(newName)) {
                        throw new Exception("UserFactory: cannot rename user " + oldName + ". An user with the given name " + newName + " already exists");
                    }

  <http pattern="/admin/userGroupView/**" use-expressions="true" access-denied-page="/accessDenied.jsp" realm="OpenNMS Realm" auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
    <!-- see NMS-13124 -->
    <csrf />
    <intercept-url pattern="/admin/userGroupView/**" access="hasAnyRole('ROLE_ADMIN')" />
    <http-basic entry-point-ref="xRequestedWithAwareBasicAuthEntryPoint" />
    <logout logout-success-url="/" />
    <custom-filter position="FORM_LOGIN_FILTER" ref="onmsUsernamePasswordAuthenticationFilter" />
    <custom-filter position="PRE_AUTH_FILTER" ref="attributePreAuthFilter"/>
    <custom-filter after="PRE_AUTH_FILTER" ref="headerPreAuthFilter"/>
    <custom-filter position="LAST" ref="authFilterEnabler"/>
  </http>


  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
      <input type="hidden" name="timeIndex" value="${timeIndex}" />
      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
	<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

        if (newID != null && newID != "") {
          if (/.*[&<>"`']+.*/.test(newID)) {
            alert("The user ID must not contain any HTML markup.");
            return;
          }

          var element =  document.getElementById('users(' + _.escape(newID) + ').doModify');
          if (typeof(element) != 'undefined' && element != null) {
            alert("A user with this ID already exist.");
            return;
          }

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<jsp:include page="/assets/load-assets.jsp" flush="false">
    <jsp:param name="asset" value="underscore-js" />
</jsp:include>

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

      <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
import java.time.Duration;
import java.time.temporal.ChronoUnit;
    @Test
    public void testInvalidUserIds() {
        testInvalidUserId("John<b>Doe</b>",true);
        testInvalidUserId("Jane'Doe'",true);
        testInvalidUserId("John&Doe",true);
        testInvalidUserId("Jane\"\"Doe",true);
    }

    @Test
    public void testValidUserIds() {
        testInvalidUserId("John-Doe",false);
        testInvalidUserId("Jane/Doe",false);
        testInvalidUserId("John.Doe",false);
        testInvalidUserId("Jane#Doe", false);
        testInvalidUserId("John@Döe.com", false);
        testInvalidUserId("JohnDoé", false);
    }

    @Test
    public void testInvalidGroupIds() {
        testInvalidGroupId("John<b>Doe</b>",true);
        testInvalidGroupId("Jane'Doe'",true);
        testInvalidGroupId("John&Doe",true);
        testInvalidGroupId("Jane\"\"Doe",true);
    }

    @Test
    public void testValidGroupIds() {
        testInvalidGroupId("John-Doe",false);
        testInvalidGroupId("Jane/Doe",false);
        testInvalidGroupId("John.Doe",false);
        testInvalidGroupId("Jane#Doe", false);
        testInvalidGroupId("John@Döe.com", false);
        testInvalidGroupId("JohnDoé", false);
    }

    public void testInvalidUserId(final String userId, final boolean mustFail) {
        adminPage();
        findElementByLink("Configure Users, Groups and On-Call Roles").click();
        findElementByLink("Configure Users").click();
        findElementByLink("Add new user").click();

        enterText(By.id("userID"), userId);
        enterText(By.id("pass1"), "SmokeTestPassword");
        enterText(By.id("pass2"), "SmokeTestPassword");
        findElementByXpath("//button[@type='submit' and text()='OK']").click();

        if (mustFail) {
            try {
                final Alert alert = wait.withTimeout(Duration.of(5, ChronoUnit.SECONDS)).until(ExpectedConditions.alertIsPresent());
                alert.dismiss();
            } catch (final Exception e) {
                LOG.debug("Got an exception waiting for a 'invalid user ID' alert.", e);
                throw e;
            }
        } else {
            wait.until(ExpectedConditions.elementToBeClickable(By.name("finish")));
        }
    }

    public void testInvalidGroupId(final String groupId, final boolean mustFail) {
        adminPage();
        findElementByLink("Configure Users, Groups and On-Call Roles").click();
        findElementByLink("Configure Groups").click();
        findElementByLink("Add new group").click();

        enterText(By.id("groupName"), groupId);
        enterText(By.id("groupComment"), "SmokeTestComment");
        findElementByXpath("//button[@type='submit' and text()='OK']").click();

        if (mustFail) {
            try {
                final Alert alert = wait.withTimeout(Duration.of(5, ChronoUnit.SECONDS)).until(ExpectedConditions.alertIsPresent());
                alert.dismiss();
            } catch (final Exception e) {
                LOG.debug("Got an exception waiting for a 'invalid group ID' alert.", e);
                throw e;
            }
        } else {
            wait.until(ExpectedConditions.elementToBeClickable(By.name("finish")));
        }
    }

    /**
     * see NMS-13124
     */
    @Test
    public void testCsrfPrivilegeEscalation() {
        // visit the admin's user page
        adminPage();
        findElementByLink("Configure Users, Groups and On-Call Roles").click();
        findElementByLink("Configure Users").click();
        findElementByLink("Add new user").click();

        // add a new user 'user'
        enterText(By.id("userID"), "user");
        enterText(By.id("pass1"), "pass");
        enterText(By.id("pass2"), "pass");
        findElementByXpath("//button[@type='submit' and text()='OK']").click();

        // assign just the ROLE_USER
        final Select select = new Select(driver.findElement(By.name("availableRoles")));
        select.selectByValue("ROLE_USER");
        findElementById("roles.doAdd").click();
        findElementById("saveUserButton").click();

        // assert that this is correctly set
        adminPage();
        findElementByLink("Configure Users, Groups and On-Call Roles").click();
        findElementByLink("Configure Users").click();
        findElementById("users(user).doDetails").click();

        assertTrue(wait.until(pageContainsText("ROLE_USER")));

        // now construct an exploit to set ROLE_ADMIN for user 'user'
        final String html = "<form action='" + stack.opennms().getBaseUrlInternal() + "opennms/admin/userGroupView/users/updateUser' method='POST' enctype='application/x-www-form-urlencoded'>" +
                "<input type='hidden' name='userID' value='user' />" +
                "<input type='hidden' name='password' value=' ' />" +
                "<input type='hidden' name='redirect' value='/admin/userGroupView/users/saveUser' /> <input type='hidden' name='fullName' value=' ' />" +
                "<input type='hidden' name='userComments' value=' ' />" +
                "<input type='hidden' name='configuredRoles' value='ROLE_ADMIN' />" +
                "<input type='hidden' name='email' value=' ' />" +
                "<input type='hidden' name='pemail' value=' ' />" +
                "<input type='hidden' name='xmppAddress' value=' ' />" +
                "<input type='hidden' name='microblog' value=' ' />" +
                "<input type='hidden' name='numericalService' value=' ' />" +
                "<input type='hidden' name='numericalPin' value=' ' />" +
                "<input type='hidden' name='textService' value=' ' />" +
                "<input type='hidden' name='textPin' value=' ' />" +
                "<input type='hidden' name='workPhone' value=' ' />" +
                "<input type='hidden' name='mobilePhone' value=' ' />" +
                "<input type='hidden' name='homePhone' value=' ' />" +
                "<input type='hidden' name='tuiPin' value=' ' />" +
                "<input type='hidden' name='timeZoneId' value=' ' />" +
                "<input type='hidden' name='dutySchedules' value='0' />" +
                "<input type='hidden' name='numSchedules' value='1' />" +
                "<input type='submit' id='submitIt' />" +
                "</form>";
        String script = "var foo = document.createElement('div'); " +
                "foo.innerHTML=\"" + html + "\"; " +
                "document.body.appendChild(foo)";

        // ...and execute it
        driver.executeScript(script);
        findElementById("submitIt").click();

        // this should be denied due to CSRF protection
        assertTrue(wait.until(pageContainsText("Access denied")));

        // assure that the user's role is still ROLE_USER
        adminPage();
        findElementByLink("Configure Users, Groups and On-Call Roles").click();
        findElementByLink("Configure Users").click();
        findElementById("users(user).doDetails").click();
        assertTrue(wait.until(pageContainsText("ROLE_USER")));

        // delete the user
        adminPage();
        findElementByLink("Configure Users, Groups and On-Call Roles").click();
        findElementByLink("Configure Users").click();

        findElementById("users(user).doDelete").click();
        handleAlert("Are you sure you want to delete the user user?");
    }