This is an automated email from the ASF dual-hosted git repository.

solomax pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openmeetings.git


The following commit(s) were added to refs/heads/master by this push:
     new 725ac73a2 [OPENMEETINGS-2829] OM user pictureUri is properly validated
725ac73a2 is described below

commit 725ac73a2b6d07317e97e7b252f936a8e00ce00f
Author: Maxim Solodovnik <[email protected]>
AuthorDate: Thu Oct 22 22:07:54 2026 +0700

    [OPENMEETINGS-2829] OM user pictureUri is properly validated
---
 .../openmeetings/db/util/ApplicationHelper.java    | 17 ++++-
 .../apache/openmeetings/web/room/RoomPanel.java    | 21 ++++--
 .../openmeetings/web/user/rooms/RoomsPanel.java    | 40 ++---------
 .../web/util/ProfileImageResourceReference.java    | 67 +++++++++++++-----
 .../webapp/WEB-INF/classes/openmeetings.properties |  3 +
 .../openmeetings/web/util/TestProfileImage.java    | 82 ++++++++++++++++++++++
 6 files changed, 171 insertions(+), 59 deletions(-)

diff --git 
a/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ApplicationHelper.java
 
b/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ApplicationHelper.java
index 8c3db65f8..bfe6e4113 100644
--- 
a/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ApplicationHelper.java
+++ 
b/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ApplicationHelper.java
@@ -24,7 +24,11 @@ import static 
org.apache.openmeetings.util.OpenmeetingsVariables.isInitComplete;
 import static 
org.springframework.web.context.WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE;
 import static 
org.springframework.web.context.support.WebApplicationContextUtils.getWebApplicationContext;
 
+import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Enumeration;
 import java.util.Map;
 import java.util.UUID;
@@ -58,6 +62,7 @@ import 
org.springframework.web.context.support.XmlWebApplicationContext;
 public class ApplicationHelper {
        private static final Logger log = 
LoggerFactory.getLogger(ApplicationHelper.class);
        private static final Object SYNC_OBJ = new Object();
+       private static final String TEMP_PREFIX = "_OM-temp-test_";
 
        private ApplicationHelper() {}
 
@@ -68,7 +73,7 @@ public class ApplicationHelper {
                        try {
                                app = 
(WebApplication)getAppClass().getDeclaredConstructor().newInstance();
                                app.setName(String.format("--%s--", 
UUID.randomUUID())); //temporary name for temporary application
-                               final ServletContext sc = new 
MockServletContext(app, null);
+                               final ServletContext sc = new 
MockServletContext(app, Files.createTempDirectory(TEMP_PREFIX).toString());
                                XmlWebApplicationContext xmlContext = new 
XmlWebApplicationContext();
                                
xmlContext.setConfigLocation("classpath:applicationContext.xml");
                                xmlContext.setServletContext(sc);
@@ -109,7 +114,7 @@ public class ApplicationHelper {
                                        };
                                        app.setWicketFilter(filter);
                                }
-                       } catch (InstantiationException | 
IllegalAccessException | ClassNotFoundException | InvocationTargetException | 
NoSuchMethodException e) {
+                       } catch (InstantiationException | 
IllegalAccessException | ClassNotFoundException | InvocationTargetException | 
NoSuchMethodException | IOException e) {
                                log.error("Failed to create Application");
                        }
                }
@@ -178,6 +183,14 @@ public class ApplicationHelper {
 
        public static void destroyApplication() {
                WebApplication app = 
(WebApplication)Application.get(getWicketApplicationName());
+               Path root = Paths.get(app.getServletContext().getRealPath("/"));
+               if (root.getFileName().startsWith(TEMP_PREFIX)) {
+                       try {
+                               Files.deleteIfExists(root);
+                       } catch (IOException e) {
+                               log.error("Failed to delete temp root");
+                       }
+               }
                WebApplicationContext ctx = 
getWebApplicationContext(app.getServletContext());
                app.internalDestroy(); //need to be called too
                if (ctx != null) {
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
index 1436d30eb..a563c04bd 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
@@ -598,7 +598,10 @@ public class RoomPanel extends BasePanel {
                        return;
                }
                if (!curClient.getUid().equals(c.getUid())) {
-                       handler.appendJavaScript(String.format("if 
(typeof(VideoManager) !== 'undefined') {VideoManager.mute('%s', %s);}", 
obj.getString("uid"), obj.getBoolean("mute")));
+                       handler.appendJavaScript(String.format("""
+                               if (typeof(VideoManager) !== 'undefined') {
+                                       VideoManager.mute('%s', %s);
+                               }""", obj.getString("uid"), 
obj.getBoolean("mute")));
                }
        }
 
@@ -609,7 +612,10 @@ public class RoomPanel extends BasePanel {
                        // no luck
                        return;
                }
-               handler.appendJavaScript(String.format("if 
(typeof(VideoManager) !== 'undefined') {VideoManager.muteOthers('%s');}", uid));
+               handler.appendJavaScript(String.format("""
+                       if (typeof(VideoManager) !== 'undefined') {
+                               VideoManager.muteOthers('%s');
+                       }""", uid));
        }
 
        private void onWbPutFile(TextRoomMessage m) {
@@ -629,11 +635,18 @@ public class RoomPanel extends BasePanel {
                Client curClient = getClient();
                if (r.isInterview() && curClient.hasRight(Right.MODERATOR)) {
                        if (streamProcessor.isRecording(r.getId())) {
-                               handler.appendJavaScript("if (typeof(WbArea) 
=== 'object') {WbArea.setRecStarted(true);}");
+                               handler.appendJavaScript("""
+                                       if (typeof(WbArea) === 'object') {
+                                               WbArea.setRecStarted(true);
+                                       }""");
                        } else if 
(streamProcessor.recordingAllowed(getClient())) {
                                boolean hasStreams = cm.streamByRoom(r.getId())
                                                .anyMatch(cl -> 
!cl.getStreams().isEmpty());
-                               handler.appendJavaScript(String.format("if 
(typeof(WbArea) === 'object') 
{WbArea.setRecStarted(false);WbArea.setRecEnabled(%s);}", hasStreams));
+                               handler.appendJavaScript(String.format("""
+                                       if (typeof(WbArea) === 'object') {
+                                               WbArea.setRecStarted(false);
+                                               WbArea.setRecEnabled(%s);
+                                       }""", hasStreams));
                        }
                }
        }
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomsPanel.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomsPanel.java
index 6e6275bd4..6125b7bdd 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomsPanel.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/user/rooms/RoomsPanel.java
@@ -18,18 +18,14 @@
  */
 package org.apache.openmeetings.web.user.rooms;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
 import java.util.List;
 
 import org.apache.openmeetings.db.dao.room.RoomDao;
-import org.apache.openmeetings.db.dao.user.UserDao;
 import org.apache.openmeetings.db.entity.basic.Client;
 import org.apache.openmeetings.db.entity.room.Room;
 import org.apache.openmeetings.web.app.ClientManager;
 import org.apache.openmeetings.web.common.UserPanel;
-import org.apache.openmeetings.util.OmFileHelper;
+import org.apache.openmeetings.web.util.ProfileImageResourceReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.basic.Label;
@@ -37,9 +33,6 @@ import org.apache.wicket.markup.html.image.Image;
 import org.apache.wicket.markup.html.list.ListItem;
 import org.apache.wicket.markup.html.list.ListView;
 import org.apache.wicket.model.Model;
-import org.apache.wicket.request.resource.ByteArrayResource;
-
-import org.apache.wicket.util.io.IOUtils;
 
 import jakarta.inject.Inject;
 
@@ -55,8 +48,6 @@ public class RoomsPanel extends UserPanel {
        private final List<Room> rooms;
        private Long roomId = 0L;
 
-       @Inject
-       private UserDao userDao;
        @Inject
        private ClientManager cm;
        @Inject
@@ -71,32 +62,9 @@ public class RoomsPanel extends UserPanel {
                        @Override
                        protected void populateItem(final ListItem<Client> 
item) {
                                Client client = item.getModelObject();
-                               final Long userId = client.getUserId();
-                               item.add(new Image("clientImage", new 
ByteArrayResource("image/png") {
-                                       private static final long 
serialVersionUID = 1L;
-
-                                       @Override
-                                       protected ResourceResponse 
newResourceResponse(Attributes attributes) {
-                                               ResourceResponse rr = 
super.newResourceResponse(attributes);
-                                               rr.disableCaching();
-                                               return rr;
-                                       }
-
-                                       @Override
-                                       protected byte[] getData(Attributes 
attributes) {
-                                               String uri = null;
-                                               if (userId != null) {
-                                                       uri = 
userDao.get(userId > 0 ? userId : -userId).getPictureUri();
-                                               }
-                                               File img = 
OmFileHelper.getUserProfilePicture(userId, uri);
-                                               try (InputStream is = new 
FileInputStream(img)) {
-                                                       return 
IOUtils.toByteArray(is);
-                                               } catch (Exception e) {
-                                                       //no-op
-                                               }
-                                               return null;
-                                       }
-                               }));
+                               item.add(new Image("clientImage",
+                                               new 
ProfileImageResourceReference(),
+                                               
ProfileImageResourceReference.getImgParams(client.getUser())));
                                item.add(new Label("clientLogin", 
client.getUser().getLogin()));
                                item.add(new Label("from", 
client.getConnectedSince()));
                        }
diff --git 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/ProfileImageResourceReference.java
 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/ProfileImageResourceReference.java
index 34397c17e..75bcfd7dd 100644
--- 
a/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/ProfileImageResourceReference.java
+++ 
b/openmeetings-web/src/main/java/org/apache/openmeetings/web/util/ProfileImageResourceReference.java
@@ -37,19 +37,38 @@ import jakarta.inject.Inject;
 import jakarta.servlet.http.HttpServletResponse;
 
 import static org.apache.openmeetings.util.OmFileHelper.PNG_MIME_TYPE;
-import static org.apache.openmeetings.util.OmFileHelper.SIP_USER_ID;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.net.URI;
+import java.net.URL;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
 
 public class ProfileImageResourceReference extends ResourceReference {
        private static final long serialVersionUID = 1L;
        private static final Logger log = 
LoggerFactory.getLogger(ProfileImageResourceReference.class);
+       private static final Set<String> ALLOWED_SCHEMAS = new HashSet<>();
+       private static final URI EMPTY_URI = URI.create("");
        @Inject
        private UserDao userDao;
 
+       static {
+               Properties props = new Properties();
+               try (final InputStream is = 
ProfileImageResourceReference.class.getResourceAsStream("/openmeetings.properties"))
 {
+                       props.load(is);
+               } catch (Exception e) {
+                       log.error("Failed to load openmeetings.properties {}", 
e.getMessage());
+               }
+               String[] schemas = props.getProperty("valid.avatar.schemas", 
"https").split(",");
+               for (String schema : schemas) {
+                       ALLOWED_SCHEMAS.add(schema);
+               }
+       }
+
        public ProfileImageResourceReference() {
                super(ProfileImageResourceReference.class, "profile");
                Injector.get().inject(this);
@@ -59,24 +78,38 @@ public class ProfileImageResourceReference extends 
ResourceReference {
                return getUrl(rc, userDao.get(userId));
        }
 
+       public static PageParameters getImgParams(User u) {
+               return new PageParameters()
+                               .add("id", u.getId())
+                               .add("anticache", 
Optional.ofNullable(u.getUpdated())
+                                               
.orElse(u.getInserted()).getTime());
+       }
+
        public static String getUrl(RequestCycle rc, User u) {
-               String uri = u.getPictureUri();
-               if (isRelative(uri)) {
-                       File img = 
OmFileHelper.getUserProfilePicture(u.getId(), uri);
-                       uri = rc.urlFor(new ProfileImageResourceReference()
-                                       , new PageParameters().add("id", 
u.getId()).add("anticache", img.lastModified())).toString();
-               }
-               return uri;
+               URI uri = sanitize(u);
+               return !uri.isAbsolute()
+                       ? rc.urlFor(new ProfileImageResourceReference(), 
getImgParams(u)).toString()
+                       : uri.toString();
        }
 
-       private static boolean isRelative(String uri) {
-               boolean relative = true;
+       //package private for testing
+       static URI sanitize(User u) {
+               URI uri = EMPTY_URI;
                try {
-                       relative = !URI.create(uri).isAbsolute();
+                       uri = u == null ? EMPTY_URI : 
URI.create(u.getPictureUri());
+                       if (!uri.isAbsolute()) {
+                               return uri;
+                       }
+                       if (!ALLOWED_SCHEMAS.contains(uri.getScheme())) {
+                               return EMPTY_URI;
+                       }
+                       //let's check if we can construct URL out of this
+                       URL url = uri.toURL();
+                       uri = url.toURI().normalize();
                } catch (Exception e) {
-                       //no-op
+                       uri = EMPTY_URI;
                }
-               return relative;
+               return uri;
        }
 
        @Override
@@ -84,7 +117,7 @@ public class ProfileImageResourceReference extends 
ResourceReference {
                return new ByteArrayResource(PNG_MIME_TYPE) {
                        private static final long serialVersionUID = 1L;
                        private Long userId = null;
-                       private String uri = null;
+                       private URI uri = null;
 
                        @Override
                        protected ResourceResponse 
newResourceResponse(Attributes attributes) {
@@ -93,7 +126,7 @@ public class ProfileImageResourceReference extends 
ResourceReference {
                                        PageParameters params = 
attributes.getParameters();
                                        try {
                                                userId = 
params.get("id").toOptionalLong();
-                                               uri = 
SIP_USER_ID.equals(userId) ? null : userDao.get(userId).getPictureUri();
+                                               uri = 
sanitize(userDao.get(userId));
                                        } catch (Exception e) {
                                                // no-op, junk filter
                                        }
@@ -109,8 +142,8 @@ public class ProfileImageResourceReference extends 
ResourceReference {
 
                        @Override
                        protected byte[] getData(Attributes attributes) {
-                               if (isRelative(uri)) {
-                                       File img = 
OmFileHelper.getUserProfilePicture(userId, uri);
+                               if (!uri.isAbsolute()) {
+                                       File img = 
OmFileHelper.getUserProfilePicture(userId, uri.toString());
                                        try (InputStream is = new 
FileInputStream(img)) {
                                                return IOUtils.toByteArray(is);
                                        } catch (Exception e) {
diff --git 
a/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties 
b/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties
index 17aac4feb..505b745f0 100644
--- a/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties
+++ b/openmeetings-web/src/main/webapp/WEB-INF/classes/openmeetings.properties
@@ -89,3 +89,6 @@ otp.issuer=
 otp.ntp.server=pool.ntp.org
 ## milliseconds
 otp.ntp.timeout=3000
+
+## valid schemas for external avatar URLs, comma separated
+valid.avatar.schemas=https
diff --git 
a/openmeetings-web/src/test/java/org/apache/openmeetings/web/util/TestProfileImage.java
 
b/openmeetings-web/src/test/java/org/apache/openmeetings/web/util/TestProfileImage.java
new file mode 100644
index 000000000..8f3d9edb6
--- /dev/null
+++ 
b/openmeetings-web/src/test/java/org/apache/openmeetings/web/util/TestProfileImage.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") +  you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openmeetings.web.util;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.openmeetings.db.entity.user.User;
+import org.apache.openmeetings.db.util.ApplicationHelper;
+import org.apache.openmeetings.util.OpenmeetingsVariables;
+import org.apache.openmeetings.web.IsolatedTest;
+import org.apache.wicket.request.cycle.RequestCycle;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+@IsolatedTest
+class TestProfileImage {
+       @BeforeAll
+       static void setup() {
+               
OpenmeetingsVariables.setWicketApplicationName("TestProfileImage");
+               
ApplicationHelper.ensureRequestCycle(ApplicationHelper.ensureApplication());
+       }
+
+       @AfterAll
+       static void teardown() {
+               ApplicationHelper.destroyApplication();
+       }
+
+       private static List<String> invalidURLs() {
+               return List.of("../../../abracadabra"
+                       , "/etc/hosts"
+                       , "file:///etc/hosts"
+                       , 
"ftp://username:[email protected]/folder/abracadabra";
+                       , "smb://video/abracadabra"
+                       , "\\\\video/abracadabra"
+                       , "javascript:alert(document.cookie)"
+                       , "emailto:jane@doe";);
+       }
+
+       @ParameterizedTest
+       @MethodSource("invalidURLs")
+       void testSanitize(String url) {
+               User u = new User();
+               u.setId(666L);
+               u.setInserted(new Date());
+               u.setPictureUri(url);
+               
assertFalse(ProfileImageResourceReference.sanitize(u).isAbsolute()
+                               , "Invalid URL should be treated as relative");
+       }
+
+       @ParameterizedTest
+       @MethodSource("invalidURLs")
+       void testProfilePicture(String url) {
+               User u = new User();
+               u.setId(666L);
+               u.setInserted(new Date());
+               u.setPictureUri(url);
+               
assertTrue(ProfileImageResourceReference.getUrl(RequestCycle.get(), 
u).startsWith("./profile/666")
+                               , "There should be standard picture URL");
+       }
+}

Reply via email to