CVE-2014-125071: Origin Validation Error (CWE-346)
value: 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.
CWE description: The product does not properly verify that the source of data or communication is valid.
CVSS v3 base score: 9.8
Published: 2023-01-09T21:15Z
Repository: https://github.com/lukehutch/gribbit
Language: Java
Files changed: src/gribbit/auth/Cookie.java, src/gribbit/auth/User.java, src/gribbit/request/HttpRequestHandler.java, src/gribbit/request/Request.java
Commit date: 2014-12-31 08:17:34 -0800
Commit message: Protect against CSWSH: (Cross-Site WebSocket Hijacking)
Diff: diff --git a/src/gribbit/auth/Cookie.java b/src/gribbit/auth/Cookie.java
index a27daa4..05f7e3e 100644
--- a/src/gribbit/auth/Cookie.java
+++ b/src/gribbit/auth/Cookie.java
@@ -165,11 +165,13 @@ public class Cookie {
         }
         nettyCookie.setMaxAge(maxAgeSeconds);
         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;
     }
 
     // -----------------------------------------------------------------------------------------------------
diff --git a/src/gribbit/auth/User.java b/src/gribbit/auth/User.java
index 80faa88..defafe9 100644
--- a/src/gribbit/auth/User.java
+++ b/src/gribbit/auth/User.java
@@ -396,18 +396,23 @@ public class User extends DBModelStringKey {
         // Check user against login whitelist, if it exists
         if (GribbitServer.loginWhitelistChecker == null || GribbitServer.loginWhitelistChecker.allowUserToLogin(id)) {
 
             // 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));
             response.setCookie(new Cookie(Cookie.EMAIL_COOKIE_NAME, "/", id, Cookie.SESSION_COOKIE_MAX_AGE_SECONDS));
 
diff --git a/src/gribbit/request/HttpRequestHandler.java b/src/gribbit/request/HttpRequestHandler.java
index af46a39..60fe2f1 100644
--- a/src/gribbit/request/HttpRequestHandler.java
+++ b/src/gribbit/request/HttpRequestHandler.java
@@ -23,26 +23,27 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 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;
 import static io.netty.handler.codec.http.HttpHeaderNames.ETAG;
 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;
 import gribbit.response.HTMLPageResponse;
 import gribbit.response.HTMLResponse;
@@ -107,10 +108,11 @@ import io.netty.util.CharsetUtil;
 import java.io.File;
 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;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
@@ -824,19 +826,87 @@ 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);
                 handshaker = wsFactory.newHandshaker(httpReq);
                 if (handshaker == null) {
diff --git a/src/gribbit/request/Request.java b/src/gribbit/request/Request.java
index 094703c..6a499a4 100644
--- a/src/gribbit/request/Request.java
+++ b/src/gribbit/request/Request.java
@@ -73,13 +73,15 @@ public class Request {
     private HashMap<String, String> postParamToValue;
     private HashMap<String, FileUpload> postParamToFileUpload;
     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;
 
     /**
      * Header for CSRF protection of AJAX requests (regular GETs and POSTs don't allow for header manipulation.)

Vulnerable code:             nettyCookie.setSecure(true);  // TODO
            save();
import static io.netty.handler.codec.http.HttpHeaderNames.CACHE_CONTROL;
import static io.netty.handler.codec.http.HttpHeaderNames.SERVER;
            // TODO: Read WS routes from class annotations
                    && reqURI.endsWith("/websocket")) {
                // 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);
     * Header for CORS.
     * See http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
Fixed code:             // If SSL is enabled, force cookies to only be delivered over SSL, to prevent cookie hijacking
            // on public wifi networks
            nettyCookie.setSecure(true);
            
            // Create new random CSRF token every time user logs in
            
            // Save tokens in database
            save();

import static io.netty.handler.codec.http.HttpHeaderNames.CACHE_CONTROL;
import static io.netty.handler.codec.http.HttpHeaderNames.SERVER;
import gribbit.auth.CSRF;
import java.net.URI;
            // FIXME: Make these into class annotations
            String websocketPath = "/websocket";
            boolean isAuthenticatedWebsocket = true;

            // TODO: Read WS routes from class annotations, rather than using hardcoded "/websocket"
                    && reqURI.endsWith(websocketPath)) {
                // 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;
                }
     * Header for CORS, and for protecting against CSWSH. See:
     * http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
     * 
     * http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html