Protect against RFD exploits
Issue: SPR-13548
This commit is contained in:
committed by
Stephane Nicoll
parent
161fd98656
commit
2bd1daa75e
@@ -417,7 +417,7 @@ public abstract class AbstractSockJsService implements SockJsService, CorsConfig
|
||||
response.setStatusCode(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
else if (!validateRequest(serverId, sessionId, transport)) {
|
||||
else if (!validateRequest(serverId, sessionId, transport) || !validatePath(request)) {
|
||||
if (requestInfo != null) {
|
||||
logger.debug("Ignoring transport request: " + requestInfo);
|
||||
}
|
||||
@@ -452,6 +452,21 @@ public abstract class AbstractSockJsService implements SockJsService, CorsConfig
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the path does not contain a file extension, either in the filename
|
||||
* (e.g. "/jsonp.bat") or possibly after path parameters ("/jsonp;Setup.bat")
|
||||
* which could be used for RFD exploits.
|
||||
* <p>Since the last part of the path is expected to be a transport type, the
|
||||
* presence of an extension would not work. All we need to do is check if
|
||||
* there are any path parameters, which would have been removed from the
|
||||
* SockJS path during request mapping, and if found reject the request.
|
||||
*/
|
||||
private boolean validatePath(ServerHttpRequest request) {
|
||||
String path = request.getURI().getPath();
|
||||
int index = path.lastIndexOf('/') + 1;
|
||||
String filename = path.substring(index);
|
||||
return filename.indexOf(';') == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle request for raw WebSocket communication, i.e. without any SockJS message framing.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,10 +18,12 @@ package org.springframework.web.socket.sockjs.transport.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
@@ -43,6 +45,12 @@ import org.springframework.web.util.UriUtils;
|
||||
public abstract class AbstractHttpSendingTransportHandler extends AbstractTransportHandler
|
||||
implements SockJsSessionFactory {
|
||||
|
||||
/**
|
||||
* Pattern for validating jsonp callback parameter values.
|
||||
*/
|
||||
private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*");
|
||||
|
||||
|
||||
@Override
|
||||
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler wsHandler, SockJsSession wsSession) throws SockJsException {
|
||||
@@ -109,8 +117,12 @@ public abstract class AbstractHttpSendingTransportHandler extends AbstractTransp
|
||||
String query = request.getURI().getQuery();
|
||||
MultiValueMap<String, String> params = UriComponentsBuilder.newInstance().query(query).build().getQueryParams();
|
||||
String value = params.getFirst("c");
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (!StringUtils.isEmpty(value) ? UriUtils.decode(value, "UTF-8") : null);
|
||||
String result = UriUtils.decode(value, "UTF-8");
|
||||
return (CALLBACK_PARAM_PATTERN.matcher(result).matches() ? result : null);
|
||||
}
|
||||
catch (UnsupportedEncodingException ex) {
|
||||
// should never happen
|
||||
|
||||
@@ -84,7 +84,7 @@ public class JsonpPollingTransportHandler extends AbstractHttpSendingTransportHa
|
||||
// We already validated the parameter above...
|
||||
String callback = getCallbackParam(request);
|
||||
|
||||
return new DefaultSockJsFrameFormat(callback + "(\"%s\");\r\n") {
|
||||
return new DefaultSockJsFrameFormat("/**/" + callback + "(\"%s\");\r\n") {
|
||||
@Override
|
||||
protected String preProcessContent(String content) {
|
||||
return JavaScriptUtils.javaScriptEscape(content);
|
||||
|
||||
@@ -76,6 +76,7 @@ public class SockJsServiceTests extends AbstractHttpRequestTests {
|
||||
resetResponseAndHandleRequest("GET", "/echo/server/session/", HttpStatus.NOT_FOUND);
|
||||
resetResponseAndHandleRequest("GET", "/echo/s.erver/session/websocket", HttpStatus.NOT_FOUND);
|
||||
resetResponseAndHandleRequest("GET", "/echo/server/s.ession/websocket", HttpStatus.NOT_FOUND);
|
||||
resetResponseAndHandleRequest("GET", "/echo/server/session/jsonp;Setup.pl", HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.Test;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.web.socket.AbstractHttpRequestTests;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
|
||||
import org.springframework.web.socket.sockjs.frame.SockJsFrame;
|
||||
import org.springframework.web.socket.sockjs.frame.SockJsFrameFormat;
|
||||
import org.springframework.web.socket.sockjs.transport.session.AbstractSockJsSession;
|
||||
@@ -31,8 +32,13 @@ import org.springframework.web.socket.sockjs.transport.session.PollingSockJsSess
|
||||
import org.springframework.web.socket.sockjs.transport.session.StreamingSockJsSession;
|
||||
import org.springframework.web.socket.sockjs.transport.session.StubSockJsServiceConfig;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Test fixture for {@link AbstractHttpSendingTransportHandler} and sub-classes.
|
||||
@@ -91,24 +97,45 @@ public class HttpSendingTransportHandlerTests extends AbstractHttpRequestTests
|
||||
|
||||
@Test
|
||||
public void jsonpTransport() throws Exception {
|
||||
testJsonpTransport(null, false);
|
||||
testJsonpTransport("_jp123xYz", true);
|
||||
testJsonpTransport("A..B__3..4", true);
|
||||
testJsonpTransport("!jp!abc", false);
|
||||
testJsonpTransport("<script>", false);
|
||||
testJsonpTransport("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_.", true);
|
||||
}
|
||||
|
||||
private void testJsonpTransport(String callbackValue, boolean expectSuccess) throws Exception {
|
||||
JsonpPollingTransportHandler transportHandler = new JsonpPollingTransportHandler();
|
||||
transportHandler.initialize(this.sockJsConfig);
|
||||
PollingSockJsSession session = transportHandler.createSession("1", this.webSocketHandler, null);
|
||||
|
||||
transportHandler.handleRequest(this.request, this.response, this.webSocketHandler, session);
|
||||
|
||||
assertEquals(500, this.servletResponse.getStatus());
|
||||
assertEquals("\"callback\" parameter required", this.servletResponse.getContentAsString());
|
||||
|
||||
resetRequestAndResponse();
|
||||
setRequest("POST", "/");
|
||||
this.servletRequest.setQueryString("c=callback");
|
||||
this.servletRequest.addParameter("c", "callback");
|
||||
transportHandler.handleRequest(this.request, this.response, this.webSocketHandler, session);
|
||||
|
||||
assertEquals("application/javascript;charset=UTF-8", this.response.getHeaders().getContentType().toString());
|
||||
assertFalse("Polling request should complete after open frame", this.servletRequest.isAsyncStarted());
|
||||
verify(this.webSocketHandler).afterConnectionEstablished(session);
|
||||
if (callbackValue != null) {
|
||||
this.servletRequest.setQueryString("c=" + callbackValue);
|
||||
this.servletRequest.addParameter("c", callbackValue);
|
||||
}
|
||||
|
||||
try {
|
||||
transportHandler.handleRequest(this.request, this.response, this.webSocketHandler, session);
|
||||
}
|
||||
catch (SockJsTransportFailureException ex) {
|
||||
if (expectSuccess) {
|
||||
throw new AssertionError("Unexpected transport failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectSuccess) {
|
||||
assertEquals(200, this.servletResponse.getStatus());
|
||||
assertEquals("application/javascript;charset=UTF-8", this.response.getHeaders().getContentType().toString());
|
||||
verify(this.webSocketHandler).afterConnectionEstablished(session);
|
||||
}
|
||||
else {
|
||||
assertEquals(500, this.servletResponse.getStatus());
|
||||
verifyNoMoreInteractions(this.webSocketHandler);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -184,7 +211,7 @@ public class HttpSendingTransportHandlerTests extends AbstractHttpRequestTests
|
||||
|
||||
format = new JsonpPollingTransportHandler().getFrameFormat(this.request);
|
||||
formatted = format.format(frame);
|
||||
assertEquals("callback(\"" + frame.getContent() + "\");\r\n", formatted);
|
||||
assertEquals("/**/callback(\"" + frame.getContent() + "\");\r\n", formatted);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user