Skip to content
Permalink
Browse files Browse the repository at this point in the history
Protect against CSWSH: (Cross-Site WebSocket Hijacking)
  • Loading branch information
lukehutch committed Dec 31, 2014
1 parent 704e3af commit 620418d
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 12 deletions.
4 changes: 3 additions & 1 deletion src/gribbit/auth/Cookie.java
Expand Up @@ -167,7 +167,9 @@ public io.netty.handler.codec.http.Cookie toNettyCookie() {
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;
}
Expand Down
7 changes: 6 additions & 1 deletion src/gribbit/auth/User.java
Expand Up @@ -398,14 +398,19 @@ public void logIn(Response response) throws UnauthorizedException {

// 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));
Expand Down
86 changes: 78 additions & 8 deletions src/gribbit/request/HttpRequestHandler.java
Expand Up @@ -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;
Expand All @@ -38,9 +37,11 @@
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;
Expand Down Expand Up @@ -109,6 +110,7 @@
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;
Expand Down Expand Up @@ -826,15 +828,83 @@ public void messageReceived(ChannelHandlerContext ctx, Object msg) throws Except
// 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);
Expand Down
6 changes: 4 additions & 2 deletions src/gribbit/request/Request.java
Expand Up @@ -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;

Expand Down

0 comments on commit 620418d

Please sign in to comment.