aboutsummaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
authorzpencer <spencerfang@google.com>2018-07-12 16:45:17 -0700
committerGitHub <noreply@github.com>2018-07-12 16:45:17 -0700
commit9925049dceb03fea2077f1e24d78e4d9616df76d (patch)
tree1aaec963ef31f4464f64d8a7ee9e63bfaafba666 /services
parent818d7246c32d9825d847409b93904caf8be1507a (diff)
downloadgrpc-grpc-java-9925049dceb03fea2077f1e24d78e4d9616df76d.tar.gz
services: double submit cookie interceptor for channelz (#4628)
Add this form of XSRF protection to channelz. Non web browser clients calling channelz must also include the cookie header and metadata key.
Diffstat (limited to 'services')
-rw-r--r--services/src/main/java/io/grpc/services/ChannelzService.java19
-rw-r--r--services/src/main/java/io/grpc/services/RequireDoubleSubmitCookieInterceptor.java106
-rw-r--r--services/src/test/java/io/grpc/services/RequireDoubleSubmitCookieInterceptorTest.java124
3 files changed, 249 insertions, 0 deletions
diff --git a/services/src/main/java/io/grpc/services/ChannelzService.java b/services/src/main/java/io/grpc/services/ChannelzService.java
index e7e3c0e72..5a5e6586a 100644
--- a/services/src/main/java/io/grpc/services/ChannelzService.java
+++ b/services/src/main/java/io/grpc/services/ChannelzService.java
@@ -18,6 +18,8 @@ package io.grpc.services;
import com.google.common.annotations.VisibleForTesting;
import io.grpc.ExperimentalApi;
+import io.grpc.ServerInterceptors;
+import io.grpc.ServerServiceDefinition;
import io.grpc.Status;
import io.grpc.channelz.v1.ChannelzGrpc;
import io.grpc.channelz.v1.GetChannelRequest;
@@ -48,10 +50,27 @@ public final class ChannelzService extends ChannelzGrpc.ChannelzImplBase {
private final Channelz channelz;
private final int maxPageSize;
+ /**
+ * Creates an instance.
+ * @deprecated Call {@link #createInstance(int)}, which includes security interceptors.
+ */
+ @Deprecated
public static ChannelzService newInstance(int maxPageSize) {
return new ChannelzService(Channelz.instance(), maxPageSize);
}
+ /**
+ * Creates an instance. The return value may contain built in interceptors for web security.
+ *
+ * @param maxPageSize the number of items per set of paginated results.
+ * @return a {@link ServerServiceDefinition} that represents the channelz service.
+ */
+ public static ServerServiceDefinition createInstance(int maxPageSize) {
+ return ServerInterceptors.intercept(
+ new ChannelzService(Channelz.instance(), maxPageSize),
+ new RequireDoubleSubmitCookieInterceptor("grpc-channelz-v1-channelz-token"));
+ }
+
@VisibleForTesting
ChannelzService(Channelz channelz, int maxPageSize) {
this.channelz = channelz;
diff --git a/services/src/main/java/io/grpc/services/RequireDoubleSubmitCookieInterceptor.java b/services/src/main/java/io/grpc/services/RequireDoubleSubmitCookieInterceptor.java
new file mode 100644
index 000000000..2347bb521
--- /dev/null
+++ b/services/src/main/java/io/grpc/services/RequireDoubleSubmitCookieInterceptor.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2018 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.grpc.services;
+
+import com.google.common.base.Objects;
+import io.grpc.Metadata;
+import io.grpc.Metadata.Key;
+import io.grpc.ServerCall;
+import io.grpc.ServerCall.Listener;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
+import io.grpc.Status;
+import java.net.HttpCookie;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * An interceptor that checks for a double submit cookie, a form of XSRF protection. This
+ * interceptor is intended for grpc-web based applications where the web page and grpc-web requests
+ * are served from the same origin, namely behind a reverse proxy.
+ *
+ * <p>This interceptor works by requiring that for each RPC, a pseudo-random value session ID is
+ * set as both a cookie as well as a request parameter in the form of a header. We rely on the
+ * fact that web browsers only send a cookie when the origin and the cookie domain's
+ * match, so RPCs invoked from other places can be detected and blocked.
+ *
+ * <p>This scheme requires the client app and server cooperate, and this interceptor implements
+ * only the server side logic.
+ *
+ * <p>On the client side, the application is responsible for setting the cookie and the header.
+ */
+final class RequireDoubleSubmitCookieInterceptor implements ServerInterceptor {
+ private static final Logger log
+ = Logger.getLogger(RequireDoubleSubmitCookieInterceptor.class.getName());
+
+ static final Key<String> COOKIE = Key.of("cookie", Metadata.ASCII_STRING_MARSHALLER);
+
+ @SuppressWarnings("rawtypes")
+ static final ServerCall.Listener NOOP = new ServerCall.Listener() {};
+
+ private final String tokenName;
+ private final Key<String> headerKey;
+ private final Status failStatus;
+
+ RequireDoubleSubmitCookieInterceptor(String tokenName) {
+ this.tokenName = tokenName;
+ headerKey = Key.of(tokenName, Metadata.ASCII_STRING_MARSHALLER);
+ failStatus
+ = Status.FAILED_PRECONDITION.withDescription(
+ String.format("Double submit cookie failure. There must be both a cookie and "
+ + "metadata with matching values, for XSRF protection. "
+ + "The cookie and metadata keys must both be: %s", tokenName));
+ }
+
+ @SuppressWarnings("unchecked")
+ private <ReqT, RespT> Listener<ReqT> failCall(ServerCall<ReqT, RespT> call) {
+ call.close(failStatus, new Metadata());
+ return NOOP;
+ }
+
+ @Override
+ public <ReqT, RespT> Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers,
+ ServerCallHandler<ReqT, RespT> next) {
+ String xsrfCookie = null;
+ Iterable<String> cookieHeaders = headers.getAll(COOKIE);
+ if (cookieHeaders == null) {
+ return failCall(call);
+ }
+ for (String cookieHeader : cookieHeaders) {
+ try {
+ for (HttpCookie cookie : HttpCookie.parse(cookieHeader)) {
+ if (cookie.getName().equals(tokenName)) {
+ if (xsrfCookie == null) {
+ xsrfCookie = cookie.getValue();
+ } else {
+ log.log(Level.FINE, "Multiple cookies set for {}, this is not allowed", tokenName);
+ return failCall(call);
+ }
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ log.log(Level.FINE, "Failed to parse cookie header", e);
+ return failCall(call);
+ }
+ }
+ String xsrfHeader = headers.get(headerKey);
+ if (xsrfCookie == null || !Objects.equal(xsrfCookie, xsrfHeader)) {
+ return failCall(call);
+ }
+ return next.startCall(call, headers);
+ }
+}
diff --git a/services/src/test/java/io/grpc/services/RequireDoubleSubmitCookieInterceptorTest.java b/services/src/test/java/io/grpc/services/RequireDoubleSubmitCookieInterceptorTest.java
new file mode 100644
index 000000000..d6874d1f2
--- /dev/null
+++ b/services/src/test/java/io/grpc/services/RequireDoubleSubmitCookieInterceptorTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2018 The gRPC Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.grpc.services;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.ServerCall;
+import io.grpc.ServerCall.Listener;
+import io.grpc.ServerCallHandler;
+import io.grpc.Status;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link RequireDoubleSubmitCookieInterceptor}. */
+@RunWith(JUnit4.class)
+public final class RequireDoubleSubmitCookieInterceptorTest {
+ private static final String XSRF_TOKEN_VALUE = "abc123";
+ private static final ServerCall.Listener<Void> NEXT_LISTENER = new ServerCall.Listener<Void>() {};
+ private static final ServerCallHandler<Void, Void> NEXT = new ServerCallHandler<Void, Void>() {
+ @Override
+ public Listener<Void> startCall(ServerCall<Void, Void> call, Metadata headers) {
+ return NEXT_LISTENER;
+ }
+ };
+
+ private Status closeStatus;
+ private final ServerCall<Void, Void> call = new ServerCall<Void, Void>() {
+ @Override
+ public void close(Status status, Metadata trailers) {
+ closeStatus = status;
+ }
+
+ @Override
+ public void request(int numMessages) {}
+
+ @Override
+ public void sendHeaders(Metadata headers) {}
+
+ @Override
+ public void sendMessage(Void message) {}
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public MethodDescriptor<Void, Void> getMethodDescriptor() {
+ return null;
+ }
+ };
+
+ private final RequireDoubleSubmitCookieInterceptor interceptor
+ = new RequireDoubleSubmitCookieInterceptor("test-xsrf-token");
+ private final Metadata.Key<String> xsrfHeader
+ = Metadata.Key.of("test-xsrf-token", Metadata.ASCII_STRING_MARSHALLER);
+
+ @Test
+ public void noCookieNoHeader() throws Exception {
+ Metadata headers = new Metadata();
+ Listener<Void> listener = interceptor.interceptCall(call, headers, NEXT);
+ assertSame(RequireDoubleSubmitCookieInterceptor.NOOP, listener);
+ assertEquals(Status.FAILED_PRECONDITION.getCode(), closeStatus.getCode());
+ }
+
+ @Test
+ public void noHeader() throws Exception {
+ Metadata headers = new Metadata();
+ Listener<Void> listener = interceptor.interceptCall(call, headers, NEXT);
+ headers.put(RequireDoubleSubmitCookieInterceptor.COOKIE, "test-xsrf-token=" + XSRF_TOKEN_VALUE);
+ assertSame(RequireDoubleSubmitCookieInterceptor.NOOP, listener);
+ assertEquals(Status.FAILED_PRECONDITION.getCode(), closeStatus.getCode());
+ }
+
+ @Test
+ public void noCookie() throws Exception {
+ Metadata headers = new Metadata();
+ Listener<Void> listener = interceptor.interceptCall(call, headers, NEXT);
+ headers.put(xsrfHeader, XSRF_TOKEN_VALUE);
+ assertSame(RequireDoubleSubmitCookieInterceptor.NOOP, listener);
+ assertEquals(Status.FAILED_PRECONDITION.getCode(), closeStatus.getCode());
+ }
+
+ @Test
+ public void matchingCookieAndHeader() throws Exception {
+ Metadata headers = new Metadata();
+ headers.put(
+ RequireDoubleSubmitCookieInterceptor.COOKIE, "test-xsrf-token=" + XSRF_TOKEN_VALUE);
+ headers.put(xsrfHeader, XSRF_TOKEN_VALUE);
+ Listener<Void> listener = interceptor.interceptCall(call, headers, NEXT);
+ assertSame(NEXT_LISTENER, listener);
+ assertNull(closeStatus);
+ }
+
+ @Test
+ public void mismatchedCookieAndHeader() throws Exception {
+ Metadata headers = new Metadata();
+ headers.put(
+ RequireDoubleSubmitCookieInterceptor.COOKIE, "test-xsrf-token=" + XSRF_TOKEN_VALUE);
+ headers.put(xsrfHeader, "foobar");
+ Listener<Void> listener = interceptor.interceptCall(call, headers, NEXT);
+ assertSame(RequireDoubleSubmitCookieInterceptor.NOOP, listener);
+ assertEquals(Status.FAILED_PRECONDITION.getCode(), closeStatus.getCode());
+ }
+}