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:
core/web-assets/src/main/assets/js/apps/onms-requisitions/lib/scripts/controllers/ForeignSource.js
core/web-assets/src/main/assets/js/apps/onms-requisitions/lib/scripts/controllers/Node.js
core/web-assets/src/main/assets/js/apps/onms-requisitions/lib/scripts/controllers/QuickAddNode.js
core/web-assets/src/main/assets/js/apps/onms-requisitions/lib/scripts/controllers/Requisition.js
core/web-assets/src/main/assets/js/apps/onms-requisitions/lib/scripts/controllers/Requisitions.js
core/web-assets/src/main/assets/js/apps/onms-requisitions/lib/scripts/services/Synchronize.js
core/web-assets/src/test/javascript/ng-requisitions/controllers/ForeignSource.test.js
core/web-assets/src/test/javascript/ng-requisitions/controllers/Node.test.js
core/web-assets/src/test/javascript/ng-requisitions/controllers/Requisition.test.js
core/web-assets/src/test/javascript/ng-requisitions/controllers/Requisitions.test.js
opennms-webapp/src/main/java/org/opennms/web/admin/users/AddNewUserServlet.java
opennms-webapp/src/main/java/org/opennms/web/admin/users/RenameUserServlet.java
opennms-webapp/src/main/java/org/opennms/web/controller/admin/group/GroupController.java
opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/notification/noticeWizard/eventNotices.jsp
opennms-webapp/src/main/webapp/WEB-INF/jsp/admin/userGroupView/groups/groupDetail.jsp
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/WEB-INF/jsp/alarm/detail.jsp
opennms-webapp/src/main/webapp/WEB-INF/jsp/event/detail.jsp
opennms-webapp/src/main/webapp/WEB-INF/jsp/outage/detail.jsp
opennms-webapp/src/main/webapp/WEB-INF/tags/element/nodelist.tag
opennms-webapp/src/main/webapp/admin/notification/noticeWizard/choosePath.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/newUser.jsp
opennms-webapp/src/main/webapp/admin/userGroupView/users/userDetail.jsp
opennms-webapp/src/main/webapp/element/node.jsp
opennms-webapp/src/main/webapp/notification/detail.jsp
smoke-test/src/test/java/org/opennms/smoketest/NotificationsPageIT.java
smoke-test/src/test/java/org/opennms/smoketest/UserIT.java
Commit message: NMS-13231: Backport Security Issues from Last Month



Vulnerable code:
          growl.success('The definition for the requisition ' + $scope.foreignSource + ' has been saved.');
              growl.success('The foreign source definition for ' + $scope.foreignSource + 'has been reseted.');
      growl.success('Retrieving definition for requisition ' + $scope.foreignSource + '...');
          growl.success('The node ' + $scope.node.nodeLabel + ' has been saved.');
      growl.success('Retrieving node ' + $scope.foreignId + ' from requisition ' + $scope.foreignSource + '...');
      growl.info($sanitize('The node ' + $scope.node.nodeLabel + ' is being added to requisition ' + $scope.node.foreignSource + '. Please wait...'));
      var successMessage = $sanitize('The node ' + $scope.node.nodeLabel + ' has been added to requisition ' + $scope.node.foreignSource);
                  growl.success('The requisition ' + foreignSource + ' has been created and synchronized.');
      bootbox.confirm('Are you sure you want to remove the node ' + node.nodeLabel + '?', function(ok) {
              growl.success('The node ' + node.nodeLabel + ' has been deleted.');
      growl.success('Retrieving requisition ' + $scope.foreignSource + '...');
        bootbox.confirm('This action will override the existing foreign source definition for the requisition named ' + targetForeignSource + ', using ' + foreignSource + ' as a template. Are you sure you want to continue ? This cannot be undone.', function(ok) {
              growl.success('The foreign source definition for ' + foreignSource + ' has been cloned to ' + targetForeignSource);
            bootbox.alert('Cannot add the requisition ' + foreignSource + ' because the following characters are invalid:<br/>:, /, \\, ?, &, *, \', "');
            bootbox.alert('Cannot add the requisition ' + foreignSource+ ' because there is already a requisition with that name');
              growl.success('The requisition ' + r.foreignSource + ' has been created.');
          growl.success('The deployed statistics for ' + requisition.foreignSource + ' has been updated.');
      bootbox.confirm('Are you sure you want to remove all the nodes from ' + foreignSource + '?', function(ok) {
              growl.success('All the nodes from ' + foreignSource + ' have been removed, and the requisition has been synchronized.');
      bootbox.confirm('Are you sure you want to remove the requisition ' + foreignSource + '?', function(ok) {
              growl.success('The requisition ' + foreignSource + ' has been deleted.');
              growl.success('The import operation has been started for ' + requisition.foreignSource + ' (rescanExisting? ' + rescanExisting + ')<br/>Use <b>refresh</b> to update the deployed statistics');
          title: 'Synchronize Requisition ' + requisition.foreignSource,
        
              <input type="button" class="btn btn-secondary" value="Edit" onclick="javascript:editNotice('${notification.escapedName}')"/>
              <input type="button" class="btn btn-secondary" value="Delete"  onclick="javascript:deleteNotice('${notification.escapedName}')"/>
                <input type="radio" value="Off" onclick="javascript:setStatus('${notification.escapedName}','off')"/>Off
                <input type="radio" value="On" CHECKED onclick="javascript:setStatus('${notification.escapedName}','on')"/>On
                <input type="radio" value="Off" CHECKED onclick="javascript:setStatus('${notification.escapedName}','off')"/>Off
                <input type="radio" value="On" onclick="javascript:setStatus('${notification.escapedName}','on')"/>On
              ${notification.name}
              ${notification.eventLabel}
              ${notification.displayUei}
        <span>Details for Group: <%=group.getName()%></span>
            <%=group.getComments()%>
               <li> <%=user%> </li>
        if (newName != null && newName != "")
        {
         <tr class="divider ${groupStatus.index % 2 == 0 ?  'even' : 'odd'}" id="group-${group.name}">
              <c:when test='${group.name != "Admin"}'>
                <a id="${group.name}.doDelete" href="javascript:deleteGroup('${group.name}')" onclick="return confirm('Are you sure you want to delete the group ${group.name}?')"><i class="fa fa-trash-o fa-2x"></i></a>              
                <i class="fa fa-trash-o fa-2x" onclick="alert('Sorry, the ${group.name} group cannot be deleted.')"></i>
            <a id="${group.name}.doModify" href="javascript:modifyGroup('${group.name}')"><i class="fa fa-edit fa-2x"></i></a>
                <button id="${group.name}.doRename" type="button" class="btn btn-secondary" name="rename" onclick="renameGroup('${group.name}')">Rename</button>
                <button id="${group.name}.doRename" type="button" class="btn btn-secondary" name="rename" onclick="alert('Sorry, the Admin group cannot be renamed.')">Rename</button>
            <a href="javascript:detailGroup('${group.name}')">${group.name}</a>
                  ${group.comments.get()}
            buffer.append("<option>" + category + "</option>");
  function validateFormInput() 
    
        <td><%=ack.getAckUser()%></td>
              <a href="element/node.jsp?node=<%=event.getNodeId()%>"><%=event.getNodeLabel()%></a>
              <a href="element/node.jsp?node=<%=outage.getNodeId()%>"><%=outage.getNodeLabel()%></a>
          <div class="NLnode"><a href="${nodeLink}">${nodeModel.node.label}</a>&nbsp;&nbsp;<span class="NLdbid label label-default" title="Database ID: ${nodeModel.node.id}"><i class="fa fa-database"></i>&nbsp;${nodeModel.node.id}</span>&nbsp;<span class="NLfs label label-default" title="Requisition: ${nodeModel.node.foreignSource}"><i class="fa fa-list-alt"></i>&nbsp;${nodeModel.node.foreignSource}</span>&nbsp;<span class="NLfid label label-default" title="Foreign ID: ${nodeModel.node.foreignId}"><i class="fa fa-qrcode"></i>&nbsp;${nodeModel.node.foreignId}</span>&nbsp;<span class="NLloc label label-default" title="Location: ${nodeModel.node.location.locationName}"><i class="fa fa-map-marker"></i>&nbsp;${nodeModel.node.location.locationName}</span> <c:if test="${nodeModel.node.hasFlows}"><span class="NLflows label label-default" title="Flows: flow data available"><i class="fa fa-exchange"></i>&nbsp;flow data</span></c:if></div>
<h2><%=(newNotice.getName()!=null ? "Editing notice: " + newNotice.getName() + "<br/>" : "")%></h2>
            <input type="text" class="form-control" name="name" value='<%=(newNotice.getName()!=null ? newNotice.getName() : "")%>'/>
            <input type="text" class="form-control" name="description" value='<%=newNotice.getDescription().orElse("")%>'/>
                <label>Name:</label> <input type="text" class="form-control" size="30" name="varbindName" value='<%=varbindName%>'/>
                <label>Value:</label> <input class="form-control" type="text" size="30" name="varbindValue" value='<%=varbindValue%>'/>
            <textarea rows="3" class="form-control" name="textMsg"><%=(newNotice.getTextMessage()!=null ? newNotice.getTextMessage() : "")%></textarea>
            <textarea rows="1" class="form-control" name="numMsg"><%=newNotice.getNumericMessage().orElse("")%></textarea>
            <input type="text" class="form-control" name="subject" value='<%=newNotice.getSubject().orElse("")%>'/>
        
        if (newID != null && newID != "")
        {
            <a id="<%= "users("+curUser.getUserId()+").doDelete" %>" href="javascript:deleteUser('<%=curUser.getUserId()%>')" onclick="return confirm('Are you sure you want to delete the user <%=curUser.getUserId()%>?')"><i class="fa fa-trash-o fa-2x"></i></a> 
            <a id="<%= "users("+curUser.getUserId()+").doModify" %>" href="javascript:modifyUser('<%=curUser.getUserId()%>')"><i class="fa fa-edit fa-2x"></i></a>
                <button id="<%= "users("+curUser.getUserId()+").doRename" %>" class="btn btn-secondary"  name="rename" onclick="renameUser('<%=curUser.getUserId()%>')">Rename</button>
                <button id="<%= "users("+curUser.getUserId()+").doRename" %>" class="btn btn-secondary"  name="rename" onclick="alert('Sorry, the admin user cannot be renamed.')">Rename</button>
            <a id="<%= "users("+curUser.getUserId()+").doDetails" %>" href="javascript:detailUser('<%=curUser.getUserId()%>')"><%=curUser.getUserId()%></a>
           <div id="<%= "users("+curUser.getUserId()+").fullName" %>">
            <div id="<%= "users("+curUser.getUserId()+").email" %>">
           <div id="<%= "users("+curUser.getUserId()+").pagerEmail" %>">
           <div id="<%= "users("+curUser.getUserId()+").xmppAddress" %>">
             <div id="<%= "users("+curUser.getUserId()+").userComments" %>">
<input id="userID" type="hidden" name="userID" value="<%=user.getUserId()%>"/>
        <span>Modify User: <%=userid%></span>
    
        <span>Details for User: <%=user.getUserId()%></span>
    <div class="NPnode">Node: <strong>${model.label}</strong>&nbsp;&nbsp;&nbsp;<span class="NPdbid badge badge-secondary " title="Database ID: ${model.id}"><i class="fa fa-database"></i>&nbsp;${model.id}</span>&nbsp;<span class="NPfs badge badge-secondary " title="Requisition: ${model.foreignSource}"><i class="fa fa-list-alt"></i>&nbsp;${model.foreignSource}</span>&nbsp;<span class="NPfid badge badge-secondary " title="Foreign ID: ${model.foreignId}"><i class="fa fa-qrcode"></i>&nbsp;${model.foreignId}</span>&nbsp;<span class="NPloc badge badge-secondary " title="Location: ${model.location}"><i class="fa fa-map-marker"></i>&nbsp;${model.location}</span> <c:if test="${model.node.hasFlows}"><span class="NPflows badge badge-secondary " title="Flows: flow data available"><i class="fa fa-exchange"></i> flow data</span></c:if></div>
    <td class="col-md-2"><%=notice.getResponder()!=null ? notice.getResponder() : "&nbsp;"%></td>
      <td><%=sentTo.getUserId()%></td>


Fixed code:
          growl.success('The definition for the requisition ' + _.escape($scope.foreignSource) + ' has been saved.');
              growl.success('The foreign source definition for ' + _.escape($scope.foreignSource) + 'has been reseted.');
      growl.success('Retrieving definition for requisition ' + _.escape($scope.foreignSource) + '...');
          growl.success('The node ' + _.escape($scope.node.nodeLabel) + ' has been saved.');
      growl.success('Retrieving node ' + _.escape($scope.foreignId) + ' from requisition ' + _.escape($scope.foreignSource) + '...');
      growl.info('The node ' + _.escape($scope.node.nodeLabel) + ' is being added to requisition ' + _.escape($scope.node.foreignSource) + '. Please wait...');
      var successMessage = 'The node ' + _.escape($scope.node.nodeLabel) + ' has been added to requisition ' + _.escape($scope.node.foreignSource);
                  growl.success('The requisition ' + _.escape(foreignSource) + ' has been created and synchronized.');
      bootbox.confirm('Are you sure you want to remove the node ' + _.escape(node.nodeLabel) + '?', function(ok) {
              growl.success('The node ' + _.escape(node.nodeLabel) + ' has been deleted.');
      growl.success('Retrieving requisition ' + _.escape($scope.foreignSource) + '...');
        bootbox.confirm('This action will override the existing foreign source definition for the requisition named ' + _.escape(targetForeignSource) + ', using ' + _.escape(foreignSource) + ' as a template. Are you sure you want to continue ? This cannot be undone.', function(ok) {
              growl.success('The foreign source definition for ' + _.escape(foreignSource) + ' has been cloned to ' + _.escape(targetForeignSource));
            bootbox.alert('Cannot add the requisition ' + _.escape(foreignSource) + ' because the following characters are invalid:<br/>:, /, \\, ?, &, *, \', "');
            bootbox.alert('Cannot add the requisition ' + _.escape(foreignSource) + ' because there is already a requisition with that name');
              growl.success('The requisition ' + _.escape(r.foreignSource) + ' has been created.');
          growl.success('The deployed statistics for ' + _.escape(requisition.foreignSource) + ' has been updated.');
      bootbox.confirm('Are you sure you want to remove all the nodes from ' + _.escape(foreignSource) + '?', function(ok) {
              growl.success('All the nodes from ' + _.escape(foreignSource) + ' have been removed, and the requisition has been synchronized.');
      bootbox.confirm('Are you sure you want to remove the requisition ' + _.escape(foreignSource) + '?', function(ok) {
              growl.success('The requisition ' + _.escape(foreignSource) + ' has been deleted.');
              growl.success('The import operation has been started for ' + _.escape(requisition.foreignSource) + ' (rescanExisting? ' + rescanExisting + ')<br/>Use <b>refresh</b> to update the deployed statistics');
          title: 'Synchronize Requisition ' + _.escape(requisition.foreignSource),
const _ = require('underscore-js');
const _ = require('underscore-js');
const _ = require('underscore-js');
const _ = require('underscore-js');

        if (userID != null && userID.matches(".*[&<>\"`']+.*")) {
            throw new ServletException("User ID must not contain any HTML markup.");
        }

        if (newID != null && newID.matches(".*[&<>\"`']+.*")) {
            throw new ServletException("User ID must not contain any HTML markup.");
        }


        if (newName != null && newName.matches(".*[&<>\"`']+.*")) {
            throw new ServletException("Group ID must not contain any HTML markup.");
        }

        if (groupName != null && groupName.matches(".*[&<>\"`']+.*")) {
            throw new ServletException("Group ID must not contain any HTML markup.");
        }

        if (groupComment != null && groupComment.matches(".*[&<>\"`']+.*")) {
            throw new ServletException("Group comment must not contain any HTML markup.");
        }

<%@ taglib uri="https://www.owasp.org/index.php/OWASP_Java_Encoder_Project" prefix="e"%>
              <input type="button" class="btn btn-secondary" value="Edit" onclick="javascript:editNotice('${e:forJavaScript(notification.escapedName)}')"/>
              <input type="button" class="btn btn-secondary" value="Delete"  onclick="javascript:deleteNotice('${e:forJavaScript(notification.escapedName)}')"/>
                <input type="radio" value="Off" onclick="javascript:setStatus('${e:forJavaScript(notification.escapedName)}','off')"/>Off
                <input type="radio" value="On" CHECKED onclick="javascript:setStatus('${e:forJavaScript(notification.escapedName)}','on')"/>On
                <input type="radio" value="Off" CHECKED onclick="javascript:setStatus('${e:forJavaScript(notification.escapedName)}','off')"/>Off
                <input type="radio" value="On" onclick="javascript:setStatus('${e:forJavaScript(notification.escapedName)}','on')"/>On
            ${fn:escapeXml(notification.name)}
              ${fn:escapeXml(notification.eventLabel)}
              ${fn:escapeXml(notification.displayUei)}
<%@ page import="org.opennms.core.utils.WebSecurityUtils" %>
        <span>Details for Group: <%=WebSecurityUtils.sanitizeString(group.getName())%></span>
            <%=WebSecurityUtils.sanitizeString(group.getComments())%>
               <li> <%=WebSecurityUtils.sanitizeString(user)%> </li>
<%@taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
        if (newName != null && newName != "") {
          if (/.*[&<>"`']+.*/.test(newName)) {
            alert("The group ID must not contain any HTML markup.");
            return;
          }
         <tr class="divider ${groupStatus.index % 2 == 0 ?  'even' : 'odd'}" id="group-${fn:escapeXml(group.name)}">
              <c:when test='${fn:escapeXml(group.name) != "Admin"}'>
                <a id="${group.name}.doDelete" href="javascript:deleteGroup('${fn:escapeXml(group.name)}')" onclick="return confirm('Are you sure you want to delete the group ${fn:escapeXml(group.name)}?')"><i class="fa fa-trash-o fa-2x"></i></a>
                <i class="fa fa-trash-o fa-2x" onclick="alert('Sorry, the ${fn:escapeXml(group.name)} group cannot be deleted.')"></i>
            <a id="${fn:escapeXml(group.name)}.doModify" href="javascript:modifyGroup('${fn:escapeXml(group.name)}')"><i class="fa fa-edit fa-2x"></i></a>
                <button id="${fn:escapeXml(group.name)}.doRename" type="button" class="btn btn-secondary" name="rename" onclick="renameGroup('${fn:escapeXml(group.name)}')">Rename</button>
                <button id="${fn:escapeXml(group.name)}.doRename" type="button" class="btn btn-secondary" name="rename" onclick="alert('Sorry, the Admin group cannot be renamed.')">Rename</button>
            <a href="javascript:detailGroup('${fn:escapeXml(group.name)}')">${fn:escapeXml(group.name)}</a>
                  ${fn:escapeXml(group.comments.get())}
<%@ page import="org.opennms.core.utils.WebSecurityUtils" %>
            buffer.append("<option>" + WebSecurityUtils.sanitizeString(category) + "</option>");
  function validateFormInput()

    if (/.*[&<>"`']+.*/.test(id)) {
        alert("The group ID must not contain any HTML markup.");
        return false;
    }

    var comment = new String(document.newGroupForm.groupComment.value);
    if (/.*[&<>"`']+.*/.test(comment)) {
        alert("The group comment must not contain any HTML markup.");
        return false;
    }

        <td><%=WebSecurityUtils.sanitizeString(ack.getAckUser())%></td>
              <a href="element/node.jsp?node=<%=event.getNodeId()%>"><%=WebSecurityUtils.sanitizeString(event.getNodeLabel())%></a>
              <a href="element/node.jsp?node=<%=outage.getNodeId()%>"><%=WebSecurityUtils.sanitizeString(outage.getNodeLabel())%></a>
<%@taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
          <div class="NLnode"><a href="${nodeLink}">${nodeModel.node.label}</a>&nbsp;&nbsp;<span class="NLdbid label label-default" title="Database ID: ${nodeModel.node.id}"><i class="fa fa-database"></i>&nbsp;${nodeModel.node.id}</span>&nbsp;<span class="NLfs label label-default" title="Requisition: ${fn:escapeXml(nodeModel.node.foreignSource)}"><i class="fa fa-list-alt"></i>&nbsp;${fn:escapeXml(nodeModel.node.foreignSource)}</span>&nbsp;<span class="NLfid label label-default" title="Foreign ID: ${nodeModel.node.foreignId}"><i class="fa fa-qrcode"></i>&nbsp;${nodeModel.node.foreignId}</span>&nbsp;<span class="NLloc label label-default" title="Location: ${nodeModel.node.location.locationName}"><i class="fa fa-map-marker"></i>&nbsp;${nodeModel.node.location.locationName}</span> <c:if test="${nodeModel.node.hasFlows}"><span class="NLflows label label-default" title="Flows: flow data available"><i class="fa fa-exchange"></i>&nbsp;flow data</span></c:if></div>
<%@ page import="org.opennms.core.utils.WebSecurityUtils" %>

<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>

<h2><%=(newNotice.getName()!=null ? "Editing notice: " + WebSecurityUtils.sanitizeString(newNotice.getName()) + "<br/>" : "")%></h2>
            <input type="text" class="form-control" name="name" value='<%=(newNotice.getName()!=null ? WebSecurityUtils.sanitizeString(newNotice.getName()) : "")%>'/>
            <input type="text" class="form-control" name="description" value='<%=WebSecurityUtils.sanitizeString(newNotice.getDescription().orElse(""))%>'/>
                <label>Name:</label> <input type="text" class="form-control" size="30" name="varbindName" value='<%=WebSecurityUtils.sanitizeString(varbindName)%>'/>
                <label>Value:</label> <input class="form-control" type="text" size="30" name="varbindValue" value='<%=WebSecurityUtils.sanitizeString(varbindValue)%>'/>
            <textarea rows="3" class="form-control" name="textMsg"><%=(newNotice.getTextMessage()!=null ? WebSecurityUtils.sanitizeString(newNotice.getTextMessage()) : "")%></textarea>
            <textarea rows="1" class="form-control" name="numMsg"><%=WebSecurityUtils.sanitizeString(newNotice.getNumericMessage().orElse(""))%></textarea>
            <input type="text" class="form-control" name="subject" value='<%=WebSecurityUtils.sanitizeString(newNotice.getSubject().orElse(""))%>'/>
<%@ page import="org.opennms.core.utils.WebSecurityUtils" %>

<%@taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>


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

	      String sanitizedUserId = WebSecurityUtils.sanitizeString(curUser.getUserId());
            <a id="<%= "users("+sanitizedUserId+").doDelete" %>" href="javascript:deleteUser('<%=sanitizedUserId%>')" onclick="return confirm('Are you sure you want to delete the user <%=sanitizedUserId%>?')"><i class="fa fa-trash-o fa-2x"></i></a>
            <a id="<%= "users("+sanitizedUserId+").doModify" %>" href="javascript:modifyUser('<%=sanitizedUserId%>')"><i class="fa fa-edit fa-2x"></i></a>
                <button id="<%= "users("+sanitizedUserId+").doRename" %>" class="btn btn-secondary"  name="rename" onclick="renameUser('<%=sanitizedUserId%>')">Rename</button>
                <button id="<%= "users("+sanitizedUserId+").doRename" %>" class="btn btn-secondary"  name="rename" onclick="alert('Sorry, the admin user cannot be renamed.')">Rename</button>
            <a id="<%= "users("+sanitizedUserId+").doDetails" %>" href="javascript:detailUser('<%=sanitizedUserId%>')"><%=sanitizedUserId%></a>
           <div id="<%= "users("+sanitizedUserId+").fullName" %>">
            <div id="<%= "users("+sanitizedUserId+").email" %>">
           <div id="<%= "users("+sanitizedUserId+").pagerEmail" %>">
           <div id="<%= "users("+sanitizedUserId+").xmppAddress" %>">
             <div id="<%= "users("+sanitizedUserId+").userComments" %>">
<%@ page import="org.opennms.core.utils.WebSecurityUtils" %>
<input id="userID" type="hidden" name="userID" value="<%=WebSecurityUtils.sanitizeString(user.getUserId())%>"/>
        <span>Modify User: <%=WebSecurityUtils.sanitizeString(userid)%></span>

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

<%@ page import="org.opennms.core.utils.WebSecurityUtils" %>
<%@ page import="java.util.stream.Collectors" %>
        <span>Details for User: <%=WebSecurityUtils.sanitizeString(user.getUserId())%></span>
    <div class="NPnode">Node: <strong>${model.label}</strong>&nbsp;&nbsp;&nbsp;<span class="NPdbid badge badge-secondary " title="Database ID: ${model.id}"><i class="fa fa-database"></i>&nbsp;${model.id}</span>&nbsp;<span class="NPfs badge badge-secondary " title="Requisition: ${fn:escapeXml(model.foreignSource)}"><i class="fa fa-list-alt"></i>&nbsp;${fn:escapeXml(model.foreignSource)}</span>&nbsp;<span class="NPfid badge badge-secondary " title="Foreign ID: ${model.foreignId}"><i class="fa fa-qrcode"></i>&nbsp;${model.foreignId}</span>&nbsp;<span class="NPloc badge badge-secondary " title="Location: ${model.location}"><i class="fa fa-map-marker"></i>&nbsp;${model.location}</span> <c:if test="${model.node.hasFlows}"><span class="NPflows badge badge-secondary " title="Flows: flow data available"><i class="fa fa-exchange"></i> flow data</span></c:if></div>
    <td class="col-md-2"><%=notice.getResponder()!=null ? WebSecurityUtils.sanitizeString(notice.getResponder()) : "&nbsp;"%></td>
      <td><%=WebSecurityUtils.sanitizeString(sentTo.getUserId())%></td>

import static org.junit.Assert.assertTrue;
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")));
        }
    }