diff options
author | zpencer <spencerfang@google.com> | 2018-07-12 16:45:17 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-07-12 16:45:17 -0700 |
commit | 9925049dceb03fea2077f1e24d78e4d9616df76d (patch) | |
tree | 1aaec963ef31f4464f64d8a7ee9e63bfaafba666 /services | |
parent | 818d7246c32d9825d847409b93904caf8be1507a (diff) | |
download | grpc-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')
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()); + } +} |