Browse Source

Verify CSRF token before accepting new CSRF connection (#11635)

* Can't open push connection during client-side init because CSRF token
is not available at that point. This allows simplifying the
initialization because the push state will not be checked until the
first response has been processed.
* Add helper for checking the CSRF token

Change-Id: I31da1ac669dc9a581cbd66f58c07f10ea4b8b676
tags/7.1.0.beta1
Leif Åstrand 11 years ago
parent
commit
a46c97bd79

+ 0
- 6
WebContent/VAADIN/vaadinBootstrap.js View File

url += '&theme=' + encodeURIComponent(theme); url += '&theme=' + encodeURIComponent(theme);
} }
// Tell the UI what pushMode it is configured to use
var pushMode = getConfig('pushMode');
if (pushMode !== undefined) {
url += '&v-pushMode=' + encodeURIComponent(pushMode);
}
var extraParams = getConfig('extraParams') var extraParams = getConfig('extraParams')
if (extraParams !== undefined) { if (extraParams !== undefined) {
url += extraParams; url += extraParams;

+ 0
- 14
client/src/com/vaadin/client/ApplicationConfiguration.java View File

import com.vaadin.client.metadata.TypeData; import com.vaadin.client.metadata.TypeData;
import com.vaadin.client.ui.UnknownComponentConnector; import com.vaadin.client.ui.UnknownComponentConnector;
import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.communication.PushMode;
import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.shared.ui.ui.UIConstants;


public class ApplicationConfiguration implements EntryPoint { public class ApplicationConfiguration implements EntryPoint {
private ErrorMessage authorizationError; private ErrorMessage authorizationError;
private ErrorMessage sessionExpiredError; private ErrorMessage sessionExpiredError;
private int heartbeatInterval; private int heartbeatInterval;
private PushMode pushMode;


private HashMap<Integer, String> unknownComponents; private HashMap<Integer, String> unknownComponents;


return heartbeatInterval; return heartbeatInterval;
} }


public PushMode getPushMode() {
return pushMode;
}

public JavaScriptObject getVersionInfoJSObject() { public JavaScriptObject getVersionInfoJSObject() {
return getJsoConfiguration(id).getVersionInfoJSObject(); return getJsoConfiguration(id).getVersionInfoJSObject();
} }
heartbeatInterval = jsoConfiguration heartbeatInterval = jsoConfiguration
.getConfigInteger("heartbeatInterval"); .getConfigInteger("heartbeatInterval");


String pushMode = jsoConfiguration.getConfigString("pushMode");
if (pushMode != null) {
this.pushMode = Enum
.valueOf(PushMode.class, pushMode.toUpperCase());
} else {
this.pushMode = PushMode.DISABLED;
}

communicationError = jsoConfiguration.getConfigError("comErrMsg"); communicationError = jsoConfiguration.getConfigError("comErrMsg");
authorizationError = jsoConfiguration.getConfigError("authErrMsg"); authorizationError = jsoConfiguration.getConfigError("authErrMsg");
sessionExpiredError = jsoConfiguration.getConfigError("sessExpMsg"); sessionExpiredError = jsoConfiguration.getConfigError("sessExpMsg");

+ 16
- 45
client/src/com/vaadin/client/ApplicationConnection.java View File

*/ */
public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh"; public static final String UIDL_REFRESH_TOKEN = "Vaadin-Refresh";


// will hold the UIDL security key (for XSS protection) once received
private String uidlSecurityKey = "init";
// will hold the CSRF token once received
private String csrfToken = "init";


private final HashMap<String, String> resourcesMap = new HashMap<String, String>(); private final HashMap<String, String> resourcesMap = new HashMap<String, String>();




protected boolean applicationRunning = false; protected boolean applicationRunning = false;


/**
* Keep track of whether the initialization JSON has been handled. We should
* not process any push messages until the initial JSON has been processed.
*/
private boolean initJsonHandled = false;

/**
* Keep track of any push messages that arrive before
* {@link #initJsonHandled} is set to true.
*/
private JsArrayString incommingPushMessageQueue = JsArrayString
.createArray().cast();

private boolean hasActiveRequest = false; private boolean hasActiveRequest = false;


/** /**


scheduleHeartbeat(); scheduleHeartbeat();


setPushEnabled(getConfiguration().getPushMode().isEnabled());

Window.addWindowClosingHandler(new ClosingHandler() { Window.addWindowClosingHandler(new ClosingHandler() {
@Override @Override
public void onWindowClosing(ClosingEvent event) { public void onWindowClosing(ClosingEvent event) {
final String extraParams) { final String extraParams) {
startRequest(); startRequest();
// Security: double cookie submission pattern // Security: double cookie submission pattern
final String payload = uidlSecurityKey + VAR_BURST_SEPARATOR
final String payload = getCsrfToken() + VAR_BURST_SEPARATOR
+ requestData; + requestData;
VConsole.log("Making UIDL Request with params: " + payload); VConsole.log("Making UIDL Request with params: " + payload);
String uri = translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX String uri = translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX
runPostRequestHooks(configuration.getRootPanelId()); runPostRequestHooks(configuration.getRootPanelId());
} }


if (!initJsonHandled) {
/*
* Assume that the first request that is fully handled is the one
* with the initialization data.
*/
initJsonHandled = true;

int queueLength = incommingPushMessageQueue.length();
if (queueLength > 0) {
VConsole.log("Init handled, processing " + queueLength
+ " enqueued messages");
for (int i = 0; i < queueLength; i++) {
handlePushMessage(incommingPushMessageQueue.get(i));
}
incommingPushMessageQueue.setLength(0);
}

}

// deferring to avoid flickering // deferring to avoid flickering
Scheduler.get().scheduleDeferred(new Command() { Scheduler.get().scheduleDeferred(new Command() {
@Override @Override


// Get security key // Get security key
if (json.containsKey(ApplicationConstants.UIDL_SECURITY_TOKEN_ID)) { if (json.containsKey(ApplicationConstants.UIDL_SECURITY_TOKEN_ID)) {
uidlSecurityKey = json
csrfToken = json
.getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); .getString(ApplicationConstants.UIDL_SECURITY_TOKEN_ID);
} }
VConsole.log(" * Handling resources from server"); VConsole.log(" * Handling resources from server");
private ConnectorMap connectorMap = GWT.create(ConnectorMap.class); private ConnectorMap connectorMap = GWT.create(ConnectorMap.class);


protected String getUidlSecurityKey() { protected String getUidlSecurityKey() {
return uidlSecurityKey;
return getCsrfToken();
}

/**
* Gets the token (aka double submit cookie) that the server uses to protect
* against Cross Site Request Forgery attacks.
*
* @return the CSRF token string
*/
public String getCsrfToken() {
return csrfToken;
} }


/** /**
} }


public void handlePushMessage(String message) { public void handlePushMessage(String message) {
if (initJsonHandled) {
handleJSONText(message, 200);
} else {
VConsole.log("Enqueuing push message has init has not yet been handled");
incommingPushMessageQueue.push(message);
}
handleJSONText(message, 200);
} }
} }

+ 2
- 0
client/src/com/vaadin/client/communication/AtmospherePushConnection.java View File

+ ApplicationConstants.PUSH_PATH + '/'); + ApplicationConstants.PUSH_PATH + '/');
String extraParams = UIConstants.UI_ID_PARAMETER + "=" String extraParams = UIConstants.UI_ID_PARAMETER + "="
+ connection.getConfiguration().getUIId(); + connection.getConfiguration().getUIId();
extraParams += "&" + ApplicationConstants.CSRF_TOKEN_PARAMETER + "="
+ connection.getCsrfToken();


// uri is needed to identify the right connection when closing // uri is needed to identify the right connection when closing
uri = ApplicationConnection.addGetParameters(baseUrl, extraParams); uri = ApplicationConnection.addGetParameters(baseUrl, extraParams);

+ 0
- 2
server/src/com/vaadin/server/BootstrapHandler.java View File

appConfig.put("heartbeatInterval", vaadinService appConfig.put("heartbeatInterval", vaadinService
.getDeploymentConfiguration().getHeartbeatInterval()); .getDeploymentConfiguration().getHeartbeatInterval());


appConfig.put("pushMode", context.getPushMode().toString());

String serviceUrl = getServiceUrl(context); String serviceUrl = getServiceUrl(context);
if (serviceUrl != null) { if (serviceUrl != null) {
appConfig.put(ApplicationConstants.SERVICE_URL, serviceUrl); appConfig.put(ApplicationConstants.SERVICE_URL, serviceUrl);

+ 39
- 0
server/src/com/vaadin/server/VaadinService.java View File

import com.vaadin.server.communication.PublishedFileHandler; import com.vaadin.server.communication.PublishedFileHandler;
import com.vaadin.server.communication.SessionRequestHandler; import com.vaadin.server.communication.SessionRequestHandler;
import com.vaadin.server.communication.UidlRequestHandler; import com.vaadin.server.communication.UidlRequestHandler;
import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.JsonConstants; import com.vaadin.shared.JsonConstants;
import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.shared.ui.ui.UIConstants;
import com.vaadin.ui.UI; import com.vaadin.ui.UI;
return false; return false;
} }


/**
* Verifies that the given CSRF token (aka double submit cookie) is valid
* for the given session. This is used to protect against Cross Site Request
* Forgery attacks.
* <p>
* This protection is enabled by default, but it might need to be disabled
* to allow a certain type of testing. For these cases, the check can be
* disabled by setting the init parameter
* {@value Constants#SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION} to
* <code>true</code>.
*
* @see DeploymentConfiguration#isXsrfProtectionEnabled()
*
* @since 7.1
*
* @param session
* the vaadin session for which the check should be done
* @param requestToken
* the CSRF token provided in the request
* @return <code>true</code> if the token is valid or if the protection is
* disabled; <code>false</code> if protection is enabled and the
* token is invalid
*/
public static boolean isCsrfTokenValid(VaadinSession session,
String requestToken) {

if (session.getService().getDeploymentConfiguration()
.isXsrfProtectionEnabled()) {
String keyInSession = (String) session.getSession().getAttribute(
ApplicationConstants.UIDL_SECURITY_TOKEN_ID);

if (keyInSession == null || !keyInSession.equals(requestToken)) {
return false;
}
}
return true;
}

} }

+ 28
- 9
server/src/com/vaadin/server/communication/PushHandler.java View File

import com.vaadin.server.VaadinServletService; import com.vaadin.server.VaadinServletService;
import com.vaadin.server.VaadinSession; import com.vaadin.server.VaadinSession;
import com.vaadin.server.WebBrowser; import com.vaadin.server.WebBrowser;
import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.communication.PushMode;
import com.vaadin.ui.UI; import com.vaadin.ui.UI;


"New push connection with transport {0}", "New push connection with transport {0}",
resource.transport()); resource.transport());
resource.getResponse().setContentType("text/plain; charset=UTF-8"); resource.getResponse().setContentType("text/plain; charset=UTF-8");

VaadinSession session = ui.getSession();
if (resource.transport() == TRANSPORT.STREAMING) { if (resource.transport() == TRANSPORT.STREAMING) {
// IE8 requires a longer padding to work properly if the // IE8 requires a longer padding to work properly if the
// initial message is small (#11573). Chrome does not work // initial message is small (#11573). Chrome does not work
// without the original padding... // without the original padding...
WebBrowser browser = ui.getSession().getBrowser();
WebBrowser browser = session.getBrowser();
if (browser.isIE() && browser.getBrowserMajorVersion() == 8) { if (browser.isIE() && browser.getBrowserMajorVersion() == 8) {
resource.padding(LONG_PADDING); resource.padding(LONG_PADDING);
} }
} }

String requestToken = resource.getRequest().getParameter(
ApplicationConstants.CSRF_TOKEN_PARAMETER);
if (!VaadinService.isCsrfTokenValid(session, requestToken)) {
getLogger()
.log(Level.WARNING,
"Invalid CSRF token in new connection received from {0}",
resource.getRequest().getRemoteHost());
// Refresh on client side, create connection just for
// sending a message
AtmospherePushConnection connection = new AtmospherePushConnection(
ui);
connection.connect(resource);
sendRefresh(connection);
return;
}

resource.suspend(); resource.suspend();


AtmospherePushConnection connection = new AtmospherePushConnection( AtmospherePushConnection connection = new AtmospherePushConnection(
getLogger().log(Level.SEVERE, "Error writing JSON to response", getLogger().log(Level.SEVERE, "Error writing JSON to response",
e); e);
// Refresh on client side // Refresh on client side
connection
.sendMessage(VaadinService
.createCriticalNotificationJSON(null, null,
null, null));
sendRefresh(connection);
} catch (InvalidUIDLSecurityKeyException e) { } catch (InvalidUIDLSecurityKeyException e) {
getLogger().log(Level.WARNING, getLogger().log(Level.WARNING,
"Invalid security key received from {0}", "Invalid security key received from {0}",
resource.getRequest().getRemoteHost()); resource.getRequest().getRemoteHost());
// Refresh on client side // Refresh on client side
connection
.sendMessage(VaadinService
.createCriticalNotificationJSON(null, null,
null, null));
sendRefresh(connection);
} }
} }
}; };
public void destroy() { public void destroy() {
} }


private static void sendRefresh(AtmospherePushConnection connection) {
connection.sendMessage(VaadinService.createCriticalNotificationJSON(
null, null, null, null));
}

private static final Logger getLogger() { private static final Logger getLogger() {
return Logger.getLogger(PushHandler.class.getName()); return Logger.getLogger(PushHandler.class.getName());
} }

+ 3
- 23
server/src/com/vaadin/server/communication/ServerRpcHandler.java View File

import com.vaadin.server.ServerRpcManager.RpcInvocationException; import com.vaadin.server.ServerRpcManager.RpcInvocationException;
import com.vaadin.server.ServerRpcMethodInvocation; import com.vaadin.server.ServerRpcMethodInvocation;
import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
import com.vaadin.server.VariableOwner; import com.vaadin.server.VariableOwner;
import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.ApplicationConstants;
import com.vaadin.shared.Connector; import com.vaadin.shared.Connector;


// Security: double cookie submission pattern unless disabled by // Security: double cookie submission pattern unless disabled by
// property // property
if (ui.getSession().getConfiguration().isXsrfProtectionEnabled()) {
if (bursts.length == 1 && "init".equals(bursts[0])) {
// init request; don't handle any variables, key sent in
// response.
// TODO This seems to be dead code
request.setAttribute(
LegacyCommunicationManager.WRITE_SECURITY_TOKEN_FLAG,
true);
return;
} else {
// ApplicationServlet has stored the security token in the
// session; check that it matched the one sent in the UIDL
String sessId = (String) ui
.getSession()
.getSession()
.getAttribute(
ApplicationConstants.UIDL_SECURITY_TOKEN_ID);

if (sessId == null || !sessId.equals(bursts[0])) {
throw new InvalidUIDLSecurityKeyException("");
}
}

if (!VaadinService.isCsrfTokenValid(ui.getSession(), bursts[0])) {
throw new InvalidUIDLSecurityKeyException("");
} }
handleBurst(ui, unescapeBurst(bursts[1])); handleBurst(ui, unescapeBurst(bursts[1]));
} }

+ 8
- 0
server/src/com/vaadin/server/communication/UIInitHandler.java View File

import com.vaadin.server.VaadinResponse; import com.vaadin.server.VaadinResponse;
import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinService;
import com.vaadin.server.VaadinSession; import com.vaadin.server.VaadinSession;
import com.vaadin.shared.communication.PushMode;
import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.shared.ui.ui.UIConstants;
import com.vaadin.ui.UI; import com.vaadin.ui.UI;




ui.doInit(request, uiId.intValue()); ui.doInit(request, uiId.intValue());


PushMode pushMode = provider.getPushMode(event);
if (pushMode == null) {
pushMode = session.getService().getDeploymentConfiguration()
.getPushMode();
}
ui.setPushMode(pushMode);

session.addUI(ui); session.addUI(ui);


// Remember if it should be remembered // Remember if it should be remembered

+ 0
- 4
server/src/com/vaadin/ui/UI.java View File

// Actual theme - used for finding CustomLayout templates // Actual theme - used for finding CustomLayout templates
theme = request.getParameter("theme"); theme = request.getParameter("theme");


PushMode pushMode = PushMode.valueOf(request.getParameter("v-pushMode")
.toUpperCase());
setPushMode(pushMode);

getPage().init(request); getPage().init(request);


// Call the init overridden by the application developer // Call the init overridden by the application developer

+ 5
- 0
shared/src/com/vaadin/shared/ApplicationConstants.java View File

* in the VAADIN directory. * in the VAADIN directory.
*/ */
public static final String VAADIN_PUSH_JS = "vaadinPush.js"; public static final String VAADIN_PUSH_JS = "vaadinPush.js";

/**
* Name of the parameter used to transmit the CSRF token.
*/
public static final String CSRF_TOKEN_PARAMETER = "v-csrfToken";
} }

Loading…
Cancel
Save