From 9929d446959e942c175611dc3deb4756f1046c4b Mon Sep 17 00:00:00 2001 From: mbiuki Date: Mon, 29 Jun 2026 12:26:16 -0400 Subject: [PATCH] fix(security): validate user names on the API save path to block stored XSS (private-issues#651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The modern UserAPIImpl.save() path (used by PUT /api/v1/users/current) only checked name length, so HTML metacharacters in first/last name were persisted verbatim and rendered unescaped in the admin Users panel — enabling stored XSS. Add UserHelper.validateName(), driven by the existing UserName.regexp.pattern (the same allow-list the legacy UserLocalManagerImpl enforced), and call it from UserAPIImpl.save() so every save path is covered. Co-Authored-By: Claude Opus 4.8 --- .../dotmarketing/business/UserAPIImpl.java | 2 ++ .../com/dotmarketing/business/UserHelper.java | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/dotCMS/src/main/java/com/dotmarketing/business/UserAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/business/UserAPIImpl.java index 26b1f60a1388..06279cfb4d6b 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/UserAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/UserAPIImpl.java @@ -44,6 +44,7 @@ import java.util.Optional; import static com.dotmarketing.business.UserHelper.validateMaximumLength; +import static com.dotmarketing.business.UserHelper.validateName; import static com.dotmarketing.util.Constants.DONT_RESPECT_FRONT_END_ROLES; /** @@ -442,6 +443,7 @@ public void save(final User userToSave, final User user, final boolean respectFr throw new DotSecurityException( "User doesn't have permission to save the user which is trying to be saved"); } + validateName(userToSave.getFirstName(), userToSave.getLastName()); validateMaximumLength(userToSave.getFirstName(),userToSave.getLastName(),userToSave.getEmailAddress()); userFactory.save(userToSave); PasswordTrackerLocalManager passwordTracker = PasswordTrackerLocalManagerFactory diff --git a/dotCMS/src/main/java/com/dotmarketing/business/UserHelper.java b/dotCMS/src/main/java/com/dotmarketing/business/UserHelper.java index f9dbee600e9e..7d2552678367 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/UserHelper.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/UserHelper.java @@ -5,7 +5,9 @@ import com.dotmarketing.exception.DotDataException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.util.Logger; +import com.dotmarketing.util.RegEX; import com.dotmarketing.util.UtilMethods; +import com.liferay.util.SystemProperties; import com.liferay.portal.NoSuchUserException; import com.liferay.portal.*; import com.liferay.portal.ejb.UserUtil; @@ -187,6 +189,30 @@ else if (!PwdToolkitUtil.validate(password1) || } } + /** + * Validates that user-supplied names do not contain HTML/control metacharacters + * (e.g. {@code <}, {@code >}, CR/LF). This mirrors the legacy + * {@code UserLocalManagerImpl} check but is enforced here so it also covers the + * modern REST/API save path (e.g. {@code PUT /api/v1/users/current}), which would + * otherwise persist names verbatim and enable stored XSS in the admin UI. + * + * The allowed character set is driven by the configurable + * {@code UserName.regexp.pattern} system property; validation is skipped only when + * that property is not configured. + */ + public static void validateName(final String firstName, final String lastName) throws DotDataException { + final String pattern = GetterUtil.getString(SystemProperties.get("UserName.regexp.pattern")); + if (!UtilMethods.isSet(pattern)) { + return; + } + if (UtilMethods.isSet(firstName) && !RegEX.contains(firstName, pattern)) { + throw new DotDataException("First Name contains invalid characters"); + } + if (UtilMethods.isSet(lastName) && !RegEX.contains(lastName, pattern)) { + throw new DotDataException("Last Name contains invalid characters"); + } + } + public static void validateMaximumLength(final String firstName, final String lastName, final String email) throws DotDataException { if (UtilMethods.exceedsMaxLength(firstName, MAX_FIELD_LENGTH)) { throw new DotDataException("Length of First Name provided exceeds the maximum limit " + MAX_FIELD_LENGTH);