Uploaded image for project: 'Undertow'
  1. Undertow
  2. UNDERTOW-1935

CVE-2021-3690 buffer leak on incoming websocket PONG message

XMLWordPrintable

    • Hide
      1. Create webapp with the following class:
        package test;
        
        import java.io.IOException;
        import java.nio.ByteBuffer;
        import java.nio.charset.StandardCharsets;
        import java.util.Collections;
        import java.util.IdentityHashMap;
        import java.util.Set;
        
        import javax.servlet.ServletContextEvent;
        import javax.servlet.ServletContextListener;
        import javax.servlet.annotation.WebListener;
        import javax.websocket.CloseReason;
        import javax.websocket.DeploymentException;
        import javax.websocket.Endpoint;
        import javax.websocket.EndpointConfig;
        import javax.websocket.Session;
        import javax.websocket.server.ServerContainer;
        import javax.websocket.server.ServerEndpointConfig;
        import javax.websocket.server.ServerEndpointConfig.Configurator;
        
        /**
         * Application Lifecycle Listener implementation class PongLeakSample
         */
        @WebListener
        public final class PongLeakSample implements ServletContextListener {
        
            private Thread pingThread;
            private Set<WebsockEndpoint> endpoints;
        
            public PongLeakSample() {
                endpoints = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>()));
            }
        
            /**
             * @see ServletContextListener#contextDestroyed(ServletContextEvent)
             */
            public void contextDestroyed(ServletContextEvent sce) {
                pingThread.interrupt();
                endpoints.forEach(WebsockEndpoint::close);
                try {
                    pingThread.join();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        
            /**
             * @see ServletContextListener#contextInitialized(ServletContextEvent)
             */
            public void contextInitialized(ServletContextEvent sce) {
                try {
                    ((ServerContainer) sce.getServletContext().getAttribute(ServerContainer.class.getName()))
                            .addEndpoint(ServerEndpointConfig.Builder.create(WebsockEndpoint.class, "/")
                                    .configurator(new WebsockEndpointConfigurator(this)).build());
                } catch (DeploymentException e) {
                    e.printStackTrace();
                }
                pingThread = new Thread(this::pingRoutine);
                pingThread.setDaemon(true);
                pingThread.start();
            }
        
            private void pingRoutine() {
                try {
                    while (true) {
                        Thread.sleep(5000);
                        endpoints.forEach(WebsockEndpoint::ping);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        
            public static class WebsockEndpoint extends Endpoint {
        
                private static final ByteBuffer PINGMSG = ByteBuffer.wrap("PINGMSG".getBytes(StandardCharsets.UTF_8));
                private final PongLeakSample filter;
                private Session session;
        
                public WebsockEndpoint(PongLeakSample filter) {
                    this.filter = filter;
                }
        
                @Override
                public void onOpen(Session session, EndpointConfig config) {
                    this.session = session;
                    filter.endpoints.add(this);
                }
        
                @Override
                public void onClose(Session session, CloseReason closeReason) {
                    filter.endpoints.remove(this);
                }
        
                public void ping() {
                    try {
                        session.getAsyncRemote().sendPing(PINGMSG.asReadOnlyBuffer());
                    } catch (IllegalArgumentException | IOException e) {
                        e.printStackTrace();
                    }
                }
        
                public void close() {
                    try {
                        session.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        
            public static class WebsockEndpointConfigurator extends Configurator {
                private PongLeakSample filter;
        
                WebsockEndpointConfigurator(PongLeakSample filter) {
                    this.filter = filter;
                }
        
                @Override
                public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
                    WebsockEndpoint endpoint = new WebsockEndpoint(filter);
                    if (endpointClass.isInstance(endpoint))
                        return endpointClass.cast(endpoint);
                    throw new InstantiationException(
                            "Requested class is not assignable from " + WebsockEndpoint.class.getName());
                }
            }
        }
        
      2. Deploy webapp to wildfly (with standalone default configuration)
      3. Open any http page in web browser, open browser console and execute the following script several times to open several websocket connections (change localhost:8080 to wildfly location and test to war file name):
        var ws = new WebSocket('ws://localhost:8080/test/');ws.onopen=console.log;ws.onmessage=console.log;ws.onclose=console.log;ws.onerror=console.log;
      4. Monitor the java.nio:type=BufferPool,name=direct mbean, MemoryUsed attribute will grow until XNIO001007 errors in server.log; or alternatively setup byte buffer pool (with limited Buffer Size, small Max Pool Size Leak Detection Percent = 100) usage for websocket connections in hal management console, trigger gc and inspect server.log
      Show
      Create webapp with the following class: package test; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Set; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import javax.websocket.CloseReason; import javax.websocket.DeploymentException; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; import javax.websocket.Session; import javax.websocket.server.ServerContainer; import javax.websocket.server.ServerEndpointConfig; import javax.websocket.server.ServerEndpointConfig.Configurator; /** * Application Lifecycle Listener implementation class PongLeakSample */ @WebListener public final class PongLeakSample implements ServletContextListener { private Thread pingThread; private Set<WebsockEndpoint> endpoints; public PongLeakSample() { endpoints = Collections.synchronizedSet(Collections.newSetFromMap( new IdentityHashMap<>())); } /** * @see ServletContextListener#contextDestroyed(ServletContextEvent) */ public void contextDestroyed(ServletContextEvent sce) { pingThread.interrupt(); endpoints.forEach(WebsockEndpoint::close); try { pingThread.join(); } catch (InterruptedException e) { Thread .currentThread().interrupt(); } } /** * @see ServletContextListener#contextInitialized(ServletContextEvent) */ public void contextInitialized(ServletContextEvent sce) { try { ((ServerContainer) sce.getServletContext().getAttribute(ServerContainer. class. getName())) .addEndpoint(ServerEndpointConfig.Builder.create(WebsockEndpoint.class, "/" ) .configurator( new WebsockEndpointConfigurator( this )).build()); } catch (DeploymentException e) { e.printStackTrace(); } pingThread = new Thread ( this ::pingRoutine); pingThread.setDaemon( true ); pingThread.start(); } private void pingRoutine() { try { while ( true ) { Thread .sleep(5000); endpoints.forEach(WebsockEndpoint::ping); } } catch (InterruptedException e) { Thread .currentThread().interrupt(); } } public static class WebsockEndpoint extends Endpoint { private static final ByteBuffer PINGMSG = ByteBuffer.wrap( "PINGMSG" .getBytes(StandardCharsets.UTF_8)); private final PongLeakSample filter; private Session session; public WebsockEndpoint(PongLeakSample filter) { this .filter = filter; } @Override public void onOpen(Session session, EndpointConfig config) { this .session = session; filter.endpoints.add( this ); } @Override public void onClose(Session session, CloseReason closeReason) { filter.endpoints.remove( this ); } public void ping() { try { session.getAsyncRemote().sendPing(PINGMSG.asReadOnlyBuffer()); } catch (IllegalArgumentException | IOException e) { e.printStackTrace(); } } public void close() { try { session.close(); } catch (IOException e) { e.printStackTrace(); } } } public static class WebsockEndpointConfigurator extends Configurator { private PongLeakSample filter; WebsockEndpointConfigurator(PongLeakSample filter) { this .filter = filter; } @Override public <T> T getEndpointInstance( Class <T> endpointClass) throws InstantiationException { WebsockEndpoint endpoint = new WebsockEndpoint(filter); if (endpointClass.isInstance(endpoint)) return endpointClass. cast (endpoint); throw new InstantiationException( "Requested class is not assignable from " + WebsockEndpoint. class. getName()); } } } Deploy webapp to wildfly (with standalone default configuration) Open any http page in web browser, open browser console and execute the following script several times to open several websocket connections (change localhost:8080 to wildfly location and test to war file name): var ws = new WebSocket( 'ws: //localhost:8080/test/' );ws.onopen=console.log;ws.onmessage=console.log;ws.onclose=console.log;ws.onerror=console.log; Monitor the java.nio:type=BufferPool,name=direct mbean, MemoryUsed attribute will grow until XNIO001007 errors in server.log; or alternatively setup byte buffer pool (with limited Buffer Size, small Max Pool Size Leak Detection Percent = 100) usage for websocket connections in hal management console, trigger gc and inspect server.log
    • Workaround Exists
    • Hide

      Add the following code to javax.websocket.Endpoint.onOpen(Session, EndpointConfig) implementation:

                  session.addMessageHandler(PongMessage.class, new MessageHandler.Whole<PongMessage>() {
                      public void onMessage(PongMessage message) {
                          // empty
                      }
                  });
      
      Show
      Add the following code to javax.websocket.Endpoint.onOpen(Session, EndpointConfig) implementation: session.addMessageHandler(PongMessage.class, new MessageHandler.Whole<PongMessage>() { public void onMessage(PongMessage message) { // empty } });
    • Undefined

      If websocket endpoint did not added the handler for PongMessage and sends the PING messages.

      On unconfigured wildfly it leads to "XNIO001007: A channel event listener threw an exception: java.lang.OutOfMemoryError: Direct buffer memory" errors in server.log and server eventually stops servicing any requests (not only websocket connections).

      After configuring Byte Buffer Pool with Leak detection percent = 100, every gc leads to the following messages in log (one message per single PONG frame):

      server.log
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer) java.lang.Throwable: Buffer leak detected
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.server.DefaultByteBufferPool$LeakDetector.<init>(DefaultByteBufferPool.java:314)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.server.DefaultByteBufferPool$LeakDetector.<init>(DefaultByteBufferPool.java:308)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.server.DefaultByteBufferPool$DefaultPooledBuffer.<init>(DefaultByteBufferPool.java:254)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.server.DefaultByteBufferPool.allocate(DefaultByteBufferPool.java:157)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.websockets.core.BufferedBinaryMessage.read(BufferedBinaryMessage.java:90)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.websockets.core.AbstractReceiveListener.readBufferedBinary(AbstractReceiveListener.java:124)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.websockets.core.AbstractReceiveListener.bufferFullMessage(AbstractReceiveListener.java:94)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.websockets.core.AbstractReceiveListener.onPong(AbstractReceiveListener.java:66)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.websockets.core.AbstractReceiveListener.handleEvent(AbstractReceiveListener.java:46)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.websockets.core.AbstractReceiveListener.handleEvent(AbstractReceiveListener.java:33)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at org.xnio.ChannelListeners.invokeChannelListener(ChannelListeners.java:92)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.server.protocol.framed.AbstractFramedChannel$FrameReadListener.handleEvent(AbstractFramedChannel.java:952)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at io.undertow.server.protocol.framed.AbstractFramedChannel$FrameReadListener.handleEvent(AbstractFramedChannel.java:932)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at org.xnio.ChannelListeners.invokeChannelListener(ChannelListeners.java:92)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at org.xnio.conduits.ReadReadyHandler$ChannelListenerHandler.readReady(ReadReadyHandler.java:66)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at org.xnio.nio.NioSocketConduit.handleReady(NioSocketConduit.java:89)
      2021-07-30 23:04:28,400 ERROR [stderr] (Finalizer)      at org.xnio.nio.WorkerThread.run(WorkerThread.java:591)
      

       

              flaviarnn Flavia Rainone
              radist.nt Andrew Marinchuk (Inactive)
              Votes:
              0 Vote for this issue
              Watchers:
              14 Start watching this issue

                Created:
                Updated:
                Resolved: