Send XSRF token as a cookie

For XSRF protection, the REST API requires a special header
"X-Gerrit-Auth" containing a token known only to the JS running on the
server's domain. Previously, this was set as a JS literal in a
<script> tag directly in the output served at /.

To support a purely static JS application, we can't depend on
injecting JS literals into the body of /. Instead, provide the XSRF
token via a cookie on the response for /. Note that this only affects
how the server communicates the XSRF token to the client; we still
require clients to send the token back in the X-Gerrit-Auth header.
The server must ignore an XSRF token cookie sent by the client, since
the cookie will be sent on all requests, including possibly-forged
cross-site requests. As a minor optimization and to avoid confusion
when looking at request traces, the client code discards the cookie as
soon as it is stored in a JS variable.

Change-Id: Ie24051b48186d6f85bccadfe139e2103b4228cbe
This commit is contained in:
Dave Borowitz 2015-11-02 11:17:21 -05:00
parent b459ab1d76
commit aea5d3e2fd
5 changed files with 40 additions and 9 deletions

View File

@ -21,9 +21,20 @@ import java.util.List;
/** Data sent as part of the host page, to bootstrap the UI. */
public class HostPageData {
/**
* Name of the cookie in which the XSRF token is sent from the server to the
* client during host page bootstrapping.
*/
public static final String XSRF_COOKIE_NAME = "XSRF_TOKEN";
/**
* Name of the HTTP header in which the client must send the XSRF token to the
* server on each request.
*/
public static final String XSRF_HEADER_NAME = "X-Gerrit-Auth";
public String version;
public DiffPreferencesInfo accountDiffPref;
public String xGerritAuth;
public Theme theme;
public List<String> plugins;
public List<Message> messages;

View File

@ -17,6 +17,7 @@ package com.google.gerrit.client;
import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
import static com.google.gerrit.common.data.GlobalCapability.VIEW_PLUGINS;
import static com.google.gerrit.common.data.HostPageData.XSRF_COOKIE_NAME;
import com.google.gerrit.client.account.AccountApi;
import com.google.gerrit.client.account.AccountCapabilities;
@ -477,8 +478,7 @@ public class Gerrit implements EntryPoint {
if (result.accountDiffPref != null) {
myAccountDiffPref = result.accountDiffPref;
}
if (result.xGerritAuth != null) {
xGerritAuth = result.xGerritAuth;
if (result.accountDiffPref != null) {
// TODO: Support options on the GetDetail REST endpoint so that it can
// also return the preferences. Then we can fetch everything with a
// single request and we don't need the callback group anymore.
@ -513,6 +513,8 @@ public class Gerrit implements EntryPoint {
editPrefs = null;
onModuleLoad2(result);
}
xGerritAuth = Cookies.getCookie(XSRF_COOKIE_NAME);
Cookies.removeCookie(XSRF_COOKIE_NAME);
}
}));
}

View File

@ -21,6 +21,7 @@ import static com.google.gwt.http.client.RequestBuilder.PUT;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.RpcStatus;
import com.google.gerrit.common.data.HostPageData;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
@ -449,7 +450,7 @@ public class RestApi {
}
req.setHeader("Accept", JSON_TYPE);
if (Gerrit.getXGerritAuth() != null) {
req.setHeader("X-Gerrit-Auth", Gerrit.getXGerritAuth());
req.setHeader(HostPageData.XSRF_HEADER_NAME, Gerrit.getXGerritAuth());
}
return req;
}

View File

@ -16,6 +16,7 @@ package com.google.gerrit.httpd;
import static java.util.concurrent.TimeUnit.HOURS;
import com.google.gerrit.common.data.HostPageData;
import com.google.gerrit.httpd.WebSessionManager.Key;
import com.google.gerrit.httpd.WebSessionManager.Val;
import com.google.gerrit.reviewdb.client.Account;
@ -80,7 +81,7 @@ public abstract class CacheBasedWebSession implements WebSession {
val = manager.createVal(key, val);
}
String token = request.getHeader("X-Gerrit-Auth");
String token = request.getHeader(HostPageData.XSRF_HEADER_NAME);
if (val != null && token != null && token.equals(val.getAuth())) {
okPaths.add(AccessPath.REST_API);
}

View File

@ -68,6 +68,7 @@ import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -193,10 +194,7 @@ public class HostPageServlet extends HttpServlet {
StringWriter w = new StringWriter();
CurrentUser user = currentUser.get();
if (user.isIdentifiedUser()) {
w.write(HPD_ID + ".xGerritAuth=");
json(session.get().getXGerritAuth(), w);
w.write(";");
setXGerritAuthCookie(req, rsp, session.get());
w.write(HPD_ID + ".accountDiffPref=");
json(getDiffPreferences(user.asIdentifiedUser()), w);
w.write(";");
@ -205,6 +203,7 @@ public class HostPageServlet extends HttpServlet {
json(signedInTheme, w);
w.write(";");
} else {
setXGerritAuthCookie(req, rsp, null);
w.write(HPD_ID + ".theme=");
json(signedOutTheme, w);
w.write(";");
@ -231,6 +230,23 @@ public class HostPageServlet extends HttpServlet {
}
}
private static void setXGerritAuthCookie(HttpServletRequest req,
HttpServletResponse rsp, WebSession session) {
String v = session != null ? session.getXGerritAuth() : "";
Cookie c = new Cookie(HostPageData.XSRF_COOKIE_NAME, v);
c.setPath("/");
c.setHttpOnly(false);
c.setSecure(isSecure(req));
c.setMaxAge(session != null
? -1 // Set the cookie for this browser session.
: 0); // Remove the cookie (expire immediately).
rsp.addCookie(c);
}
private static boolean isSecure(HttpServletRequest req) {
return req.isSecure() || "https".equals(req.getScheme());
}
private DiffPreferencesInfo getDiffPreferences(IdentifiedUser user) {
try {
return getDiff.apply(new AccountResource(user));