CVE-2014-125071 (published 2023-01-09T21:15Z)

A vulnerability was found in lukehutch Gribbit. It has been classified as problematic. Affected is the function messageReceived of the file src/gribbit/request/HttpRequestHandler.java. The manipulation leads to missing origin validation in websockets. The name of the patch is 620418df247aebda3dd4be1dda10fe229ea505dd. It is recommended to apply a patch to fix this issue. The identifier of this vulnerability is VDB-217716.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-346
cwe_name: Origin Validation Error
description: The product does not properly verify that the source of data or communication is valid.
extended_description: 
            
            
            
         
         

Fix information:
repo_url: https://github.com/lukehutch/gribbit
commit_details: repo_url: https://github.com/lukehutch/gribbit
author: Luke Hutchison
author_date: 2014-12-31 08:17:34-08:00
msg: Protect against CSWSH: (Cross-Site WebSocket Hijacking)
repository: repo_url: https://github.com/lukehutch/gribbit
repo_name: lukehutch/gribbit
description: The Gribbit Web Framework
repo_language: Java
owner: lukehutch
file_changes: filename: Cookie.java
change_type: ModificationType.MODIFY
diff: @@ -167,7 +167,9 @@ public class Cookie {
         nettyCookie.setDiscard(discardAtEndOfBrowserSession);
         nettyCookie.setHttpOnly(true);  // TODO
         if (GribbitProperties.SSL) {
-            nettyCookie.setSecure(true);  // TODO
+            // If SSL is enabled, force cookies to only be delivered over SSL, to prevent cookie hijacking
+            // on public wifi networks
+            nettyCookie.setSecure(true);
         }
         return nettyCookie;
     }

programming_language: Java
filename: User.java
change_type: ModificationType.MODIFY
diff: @@ -398,14 +398,19 @@ public class User extends DBModelStringKey {
 
             // Create new session token
             sessionTok = new Token(TokenType.SESSION, Cookie.SESSION_COOKIE_MAX_AGE_SECONDS);
+            
+            // Create new random CSRF token every time user logs in
             csrfTok = CSRF.generateRandomCSRFToken();
-            save();
+            
             if (sessionTokHasExpired()) {
                 // Shouldn't happen, since we just created session tok, but just in case
                 clearSessionTok();
                 throw new UnauthorizedException("Couldn't create auth session");
             }
 
+            // Save tokens in database
+            save();
+
             // Save login cookies in result
             response.setCookie(new Cookie(Cookie.SESSION_COOKIE_NAME, "/", sessionTok.token,
                     Cookie.SESSION_COOKIE_MAX_AGE_SECONDS));

programming_language: Java
filename: HttpRequestHandler.java
change_type: ModificationType.MODIFY
diff: @@ -25,11 +25,10 @@
  */
 package gribbit.request;
 
-import static io.netty.handler.codec.http.HttpHeaderNames.CACHE_CONTROL;
 import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT_ENCODING;
+import static io.netty.handler.codec.http.HttpHeaderNames.CACHE_CONTROL;
 import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
 import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_ENCODING;
-import static io.netty.handler.codec.http.HttpHeaderNames.SERVER;
 import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
 import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
 import static io.netty.handler.codec.http.HttpHeaderNames.DATE;
@@ -38,9 +37,11 @@ import static io.netty.handler.codec.http.HttpHeaderNames.EXPECT;
 import static io.netty.handler.codec.http.HttpHeaderNames.EXPIRES;
 import static io.netty.handler.codec.http.HttpHeaderNames.LAST_MODIFIED;
 import static io.netty.handler.codec.http.HttpHeaderNames.PRAGMA;
+import static io.netty.handler.codec.http.HttpHeaderNames.SERVER;
 import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE;
 import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
 import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
+import gribbit.auth.CSRF;
 import gribbit.auth.Cookie;
 import gribbit.auth.User;
 import gribbit.response.ErrorResponse;
@@ -109,6 +110,7 @@ import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
+import java.net.URI;
 import java.nio.charset.Charset;
 import java.time.Instant;
 import java.time.ZoneId;
@@ -826,15 +828,83 @@ public class HttpRequestHandler extends SimpleChannelInboundHandler<Object> {
             // Complete websocket handshake if requested
             // ------------------------------------------------------------------------------
 
+            // FIXME: Make these into class annotations
+            String websocketPath = "/websocket";
+            boolean isAuthenticatedWebsocket = true;
+
             if (response == null && authorizedRoute == null && msg instanceof HttpRequest
-            // TODO: Read WS routes from class annotations
-                    && reqURI.endsWith("/websocket")) {
+            // TODO: Read WS routes from class annotations, rather than using hardcoded "/websocket"
+                    && reqURI.endsWith(websocketPath)) {
                 HttpRequest httpReq = (HttpRequest) msg;
 
-                // Record which user was authenticated (if any) when websocket upgrade request was made.
-                // TODO: Reject WS upgrade request for websockets that require authentication.
-                // TODO: Also provide a means for revoking WS login.
-                wsAuthenticatedUser = User.getLoggedInUser(request);
+                // Protect against CSWSH: (Cross-Site WebSocket Hijacking)
+                // http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
+                // http://tools.ietf.org/html/rfc6455#page-7
+                CharSequence origin = request.getOrigin();
+                URI originUri = null;
+                if (origin != null && origin.length() > 0) {
+                    try {
+                        // Try parsing origin URI
+                        originUri = new URI(origin.toString());
+                    } catch (Exception e) {
+                    }
+                }
+                // If port number is set but it is the default for the URI scheme, revert the port number
+                // back to -1 (which means unspecified), so that it matches the server port number, 
+                // which is unspecified when serving http on port 80 and https on port 443
+                int originPort = originUri == null ? -1 //
+                        : originUri.getPort() == 80 && "http".equals(originUri.getScheme()) ? -1 //
+                                : originUri.getPort() == 443 && "https".equals(originUri.getScheme()) ? -1 //
+                                        : originUri.getPort();
+                // Scheme, host and port all must match to forbid cross-origin requests
+                if (originUri == null //
+                        || !GribbitServer.uri.getScheme().equals(originUri.getScheme()) //
+                        || !GribbitServer.uri.getHost().equals(originUri.getHost()) //
+                        || GribbitServer.uri.getPort() != originPort) { //
+                    // Reject scripted requests to open this websocket from a different domain
+                    sendHttpErrorResponse(ctx, null, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
+                            HttpResponseStatus.FORBIDDEN));
+                    return;
+                }
+                // Log.info("Origin: " + origin.toString());
+
+                if (isAuthenticatedWebsocket) {
+                    // For authenticated websockets, check if the user is logged in
+                    User loggedInUser = User.getLoggedInUser(request);
+                    if (loggedInUser == null) {
+                        // Not logged in, so can't connect to this websocket
+                        sendHttpErrorResponse(ctx, null, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
+                                HttpResponseStatus.FORBIDDEN));
+                        return;
+                    }
+
+                    // To further mitigate CSWSH attacks: check for the CSRF token in the URL parameter "_csrf";
+                    // the passed token must match the user's CSRF token. This means the websocket URL has to
+                    // be dynamically generated and inserted into the webpage that opened the websocket.
+                    // TODO: generate this URL an insert into the page somehow
+                    String csrfTok = loggedInUser.csrfTok;
+                    if (csrfTok == null || csrfTok.isEmpty() || csrfTok.equals(CSRF.CSRF_TOKEN_UNKNOWN)
+                            || csrfTok.equals(CSRF.CSRF_TOKEN_PLACEHOLDER)) {
+                        // No valid CSRF token in User object
+                        sendHttpErrorResponse(ctx, null, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
+                                HttpResponseStatus.FORBIDDEN));
+                        return;
+                    }
+                    String csrfParam = request.getQueryParam("_csrf");
+                    if (csrfParam == null || csrfParam.isEmpty() || !csrfParam.equals(csrfTok)) {
+                        // The CSRF URL query parameter is missing, or doesn't match the user's token 
+                        sendHttpErrorResponse(ctx, null, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
+                                HttpResponseStatus.FORBIDDEN));
+                        return;                        
+                    }
+
+                    // Record which user was authenticated when the websocket upgrade request was made.
+                    // TODO: Also provide a means for revoking user's session while WS is still open,
+                    // e.g. poll the user table every few seconds to see if user's session token has
+                    // changed in the database? (Although this would mean that logging in on a new
+                    // device would log you out of all other sessions...)
+                    wsAuthenticatedUser = loggedInUser;
+                }
 
                 WebSocketServerHandshakerFactory wsFactory =
                         new WebSocketServerHandshakerFactory(GribbitServer.wsUri.toString(), null, true);

programming_language: Java
filename: Request.java
change_type: ModificationType.MODIFY
diff: @@ -75,9 +75,11 @@ public class Request {
     private Map<String, List<String>> queryParamToVals;
 
     /**
-     * Header for CORS.
+     * Header for CORS, and for protecting against CSWSH. See:
      * 
-     * See http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
+     * http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
+     * 
+     * http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
      **/
     private CharSequence origin;
 

programming_language: Java