CVE-2023-29016: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: The Goobi viewer is a web application that allows digitised material to be displayed in a web browser. A cross-site scripting vulnerability has been identified in Goobi viewer core prior to version 23.03 when using nicknames. An attacker could create a user account and enter malicious scripts into their profile's nickname, resulting in the execution in the user's browser when displaying the nickname on certain pages. The vulnerability has been fixed in version 23.03.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-04-06T20:15Z
Repository: https://github.com/intranda/goobi-viewer-core
Language: Other
Files changed: goobi-viewer-core/src/main/java/io/goobi/viewer/faces/validators/NameValidator.java, goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminDashboard.xhtml, goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminUsers.xhtml, goobi-viewer-core/src/main/resources/META-INF/resources/resources/tags/user/userData.xhtml, goobi-viewer-core/src/test/java/io/goobi/viewer/faces/validators/NameValidatorTest.java
Commit date: 2023-03-23 15:06:43 +0100
Commit message: added nickname validator; html escape existing values when displaying;
refs #24571
Diff: diff --git a/goobi-viewer-core/src/main/java/io/goobi/viewer/faces/validators/NameValidator.java b/goobi-viewer-core/src/main/java/io/goobi/viewer/faces/validators/NameValidator.java
new file mode 100644
index 0000000000..ddf5d183a3
--- /dev/null
+++ b/goobi-viewer-core/src/main/java/io/goobi/viewer/faces/validators/NameValidator.java
@@ -0,0 +1,75 @@
+/*
+ * This file is part of the Goobi viewer - a content presentation and management
+ * application for digitized objects.
+ *
+ * Visit these websites for more information.
+ *          - http://www.intranda.com
+ *          - http://digiverso.com
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 2 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package io.goobi.viewer.faces.validators;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.faces.application.FacesMessage;
+import javax.faces.component.UIComponent;
+import javax.faces.context.FacesContext;
+import javax.faces.validator.FacesValidator;
+import javax.faces.validator.Validator;
+import javax.faces.validator.ValidatorException;
+
+import io.goobi.viewer.messages.ViewerResourceBundle;
+
+/**
+ * Syntax validator for names (e.g. nickname).
+ */
+@FacesValidator("nameValidator")
+public class NameValidator implements Validator<String> {
+
+    private static final String REGEX =  "^[\\w ]+$"; //NOSONAR   input size is limited
+    private static final Pattern PATTERN = Pattern.compile(REGEX);
+
+    /* (non-Javadoc)
+     * @see javax.faces.validator.Validator#validate(javax.faces.context.FacesContext, javax.faces.component.UIComponent, java.lang.Object)
+     */
+    /** {@inheritDoc} */
+    @Override
+    public void validate(FacesContext context, UIComponent component, String value) throws ValidatorException {
+        if (!validate(value)) {
+            FacesMessage msg = new FacesMessage(ViewerResourceBundle.getTranslation("pi_errInvalid", null), "");
+            msg.setSeverity(FacesMessage.SEVERITY_ERROR);
+            throw new ValidatorException(msg);
+        }
+    }
+
+    /**
+     * <p>
+     * validateEmailAddress.
+     * </p>
+     *
+     * @param email a {@link java.lang.String} object.
+     * @should match correct name
+     * @should not match invalid name
+     * @return a boolean.
+     */
+    public static boolean validate(String name) {
+        if (name == null || name.length() > 10_000) {
+            return false;
+        }
+        Matcher m = PATTERN.matcher(name.toLowerCase());
+        return m.find();
+    }
+}
diff --git a/goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminDashboard.xhtml b/goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminDashboard.xhtml
index e49a911d14..500750c23d 100644
--- a/goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminDashboard.xhtml
+++ b/goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminDashboard.xhtml
@@ -32,11 +32,11 @@
 
                             <!-- WELCOME -->
                             <div class="admin__boxed-entry -no-actions -w33">
                                 <div class="admin__dashboard-user-welcome">
                                 	<div class="admin__dashboard-user-welcome-left">
-	                                    <h2>#{msg.admin__dashboard_hello} #{userBean.user.nickName}</h2>
+	                                    <h2>#{msg.admin__dashboard_hello} <h:outputText value="#{userBean.user.nickName}" escape="true" /></h2>
 	                                    <p>#{msg.admin__dashboard_documentation}:<br/>
 	                                        <a class="-bluelink" href="https://docs.goobi.io" target="_blank" rel="noopener">https://docs.goobi.io</a>
 	                                    </p>
 	                                    <p>#{msg.admin__dashboard_community_board}:<br/>
 	                                        <a class="-bluelink" href="https://community.goobi.io" target="_blank" rel="noopener">https://community.goobi.io</a>
diff --git a/goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminUsers.xhtml b/goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminUsers.xhtml
index 914c0653a6..256353c268 100644
--- a/goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminUsers.xhtml
+++ b/goobi-viewer-core/src/main/resources/META-INF/resources/resources/admin/views/adminUsers.xhtml
@@ -68,11 +68,11 @@
                                     <div class="admin__table-img" role="presentation">
                                     	<img src="#{user.getAvatarUrl(80)}" alt="User avatar" />
                                     </div>
                                     <div class="admin__table-data" role="gridcell">
                                         <!-- USER NAME -->
-                                        <span class="admin__table-name">#{user.nickName}</span>
+                                        <span class="admin__table-name"><h:outputText value="#{user.nickName}" escape="true" /></span>
                                         <!-- USER EMAIL -->
                                         <a href="mailto:#{user.email}" class="admin__table-email -textlink">#{user.email}</a>
                                         <!-- EDIT -->
                                         <a href="#{navigationHelper.applicationUrl}admin/users/edit/#{user.id}/" class="admin__table-action-link" aria-label="#{msg.edit}: #{user.email}">
                                            #{msg.edit}
diff --git a/goobi-viewer-core/src/main/resources/META-INF/resources/resources/tags/user/userData.xhtml b/goobi-viewer-core/src/main/resources/META-INF/resources/resources/tags/user/userData.xhtml
index 3708769921..5920abece8 100644
--- a/goobi-viewer-core/src/main/resources/META-INF/resources/resources/tags/user/userData.xhtml
+++ b/goobi-viewer-core/src/main/resources/META-INF/resources/resources/tags/user/userData.xhtml
@@ -22,11 +22,12 @@
 								<label for="displayName">#{msg.admin__user_display_name}:</label>
 							</div>
 							<div class="col-11 col-lg-9 admin__form-input">
 								<h:inputText value="#{user.nickName}" id="displayName"
 									label="displayName" styleClass="form-control" required="false"
-									requiredMessage="#{msg.loginEnterDisplayName}" />
+									requiredMessage="#{msg.loginEnterDisplayName}"
+									validator="nameValidator" />
 								<span class="admin__form-help-text">#{msg.admin__user_display_name__help}</span>
 							</div>
 							<div class="col-1 admin__form-help-icon">
 								<button type="button" class="btn btn--clean"
 									data-toggle="helptext" aria-label="inline help icon">
diff --git a/goobi-viewer-core/src/test/java/io/goobi/viewer/faces/validators/NameValidatorTest.java b/goobi-viewer-core/src/test/java/io/goobi/viewer/faces/validators/NameValidatorTest.java
new file mode 100644
index 0000000000..724251bc78
--- /dev/null
+++ b/goobi-viewer-core/src/test/java/io/goobi/viewer/faces/validators/NameValidatorTest.java
@@ -0,0 +1,46 @@
+/*
+ * This file is part of the Goobi viewer - a content presentation and management
+ * application for digitized objects.
+ *
+ * Visit these websites for more information.
+ *          - http://www.intranda.com
+ *          - http://digiverso.com
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 2 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package io.goobi.viewer.faces.validators;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class NameValidatorTest {
+
+    /**
+     * @see NameValidator#validate(String)
+     * @verifies match correct name
+     */
+    @Test
+    public void validate_shouldMatchCorrectName() throws Exception {
+        Assert.assertTrue(NameValidator.validate("John Doe"));
+    }
+
+    /**
+     * @see NameValidator#validate(String)
+     * @verifies not match invalid name
+     */
+    @Test
+    public void validate_shouldNotMatchInvalidName() throws Exception {
+        Assert.assertTrue(NameValidator.validate("John Doe<script />"));
+    }
+}
\ No newline at end of file

Vulnerable code: 	                                    <h2>#{msg.admin__dashboard_hello} #{userBean.user.nickName}</h2>
                                        <span class="admin__table-name">#{user.nickName}</span>
									requiredMessage="#{msg.loginEnterDisplayName}" />
Fixed code: /*
 * This file is part of the Goobi viewer - a content presentation and management
 * application for digitized objects.
 *
 * Visit these websites for more information.
 *          - http://www.intranda.com
 *          - http://digiverso.com
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package io.goobi.viewer.faces.validators;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.FacesValidator;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

import io.goobi.viewer.messages.ViewerResourceBundle;

/**
 * Syntax validator for names (e.g. nickname).
 */
@FacesValidator("nameValidator")
public class NameValidator implements Validator<String> {

    private static final String REGEX =  "^[\\w ]+$"; //NOSONAR   input size is limited
    private static final Pattern PATTERN = Pattern.compile(REGEX);

    /* (non-Javadoc)
     * @see javax.faces.validator.Validator#validate(javax.faces.context.FacesContext, javax.faces.component.UIComponent, java.lang.Object)
     */
    /** {@inheritDoc} */
    @Override
    public void validate(FacesContext context, UIComponent component, String value) throws ValidatorException {
        if (!validate(value)) {
            FacesMessage msg = new FacesMessage(ViewerResourceBundle.getTranslation("pi_errInvalid", null), "");
            msg.setSeverity(FacesMessage.SEVERITY_ERROR);
            throw new ValidatorException(msg);
        }
    }

    /**
     * <p>
     * validateEmailAddress.
     * </p>
     *
     * @param email a {@link java.lang.String} object.
     * @should match correct name
     * @should not match invalid name
     * @return a boolean.
     */
    public static boolean validate(String name) {
        if (name == null || name.length() > 10_000) {
            return false;
        }
        Matcher m = PATTERN.matcher(name.toLowerCase());
        return m.find();
    }
}
	                                    <h2>#{msg.admin__dashboard_hello} <h:outputText value="#{userBean.user.nickName}" escape="true" /></h2>
                                        <span class="admin__table-name"><h:outputText value="#{user.nickName}" escape="true" /></span>
									requiredMessage="#{msg.loginEnterDisplayName}"
									validator="nameValidator" />
/*
 * This file is part of the Goobi viewer - a content presentation and management
 * application for digitized objects.
 *
 * Visit these websites for more information.
 *          - http://www.intranda.com
 *          - http://digiverso.com
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package io.goobi.viewer.faces.validators;

import org.junit.Assert;
import org.junit.Test;

public class NameValidatorTest {

    /**
     * @see NameValidator#validate(String)
     * @verifies match correct name
     */
    @Test
    public void validate_shouldMatchCorrectName() throws Exception {
        Assert.assertTrue(NameValidator.validate("John Doe"));
    }

    /**
     * @see NameValidator#validate(String)
     * @verifies not match invalid name
     */
    @Test
    public void validate_shouldNotMatchInvalidName() throws Exception {
        Assert.assertTrue(NameValidator.validate("John Doe<script />"));
    }
}