CVE-2021-25930 (Other, published 2021-05-20T14:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 4.3)
Weakness: CWE-352 \u2014 Cross-Site Request Forgery (CSRF)
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.
Repository: https://github.com/OpenNMS/opennms
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 message: NMS-13124: Fixed user deletion by renaming bug and CSRF privilege escalation issue



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?");
    }