Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import io.opencensus.trace.Tracer;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
Expand Down Expand Up @@ -860,6 +861,10 @@ public HttpResponse execute() throws IOException {
Preconditions.checkNotNull(requestMethod);
Preconditions.checkNotNull(url);

final String originalScheme = url.getScheme();
final String originalHost = url.getHost();
final int originalPort = url.getPort();

Span span =
tracer
.spanBuilder(OpenCensusUtils.SPAN_NAME_HTTP_REQUEST_EXECUTE)
Expand All @@ -879,6 +884,11 @@ public HttpResponse execute() throws IOException {
if (executeInterceptor != null) {
executeInterceptor.intercept(this);
}
// Prevent credential leak on cross-origin redirects
if (!isSameOrigin(originalScheme, originalHost, originalPort, url.getScheme(), url.getHost(), url.getPort())) {
headers.setAuthorization((String) null);
headers.setCookie((String) null);
}
// build low-level HTTP request
String urlString = url.build();
addSpanAttribute(span, HttpTraceAttributeConstants.HTTP_METHOD, requestMethod);
Expand Down Expand Up @@ -1180,7 +1190,25 @@ public boolean handleRedirect(int statusCode, HttpHeaders responseHeaders) {
&& HttpStatusCodes.isRedirect(statusCode)
&& redirectLocation != null) {
// resolve the redirect location relative to the current location
setUrl(new GenericUrl(url.toURL(redirectLocation), useRawRedirectUrls));
URL newURL = url.toURL(redirectLocation);

// Check if redirecting to a different origin
String oldScheme = url.getScheme();
String oldHost = url.getHost();
int oldPort = url.getPort();

String newScheme = newURL.getProtocol();
String newHost = newURL.getHost();
int newPort = newURL.getPort();

int oldEffectivePort = getEffectivePort(oldScheme, oldPort);
int newEffectivePort = getEffectivePort(newScheme, newPort);

boolean sameOrigin = (oldScheme == null ? newScheme == null : oldScheme.equalsIgnoreCase(newScheme))
&& (oldHost == null ? newHost == null : oldHost.equalsIgnoreCase(newHost))
&& (oldEffectivePort == newEffectivePort);

setUrl(new GenericUrl(newURL, useRawRedirectUrls));
// on 303 change method to GET
if (statusCode == HttpStatusCodes.STATUS_CODE_SEE_OTHER) {
setRequestMethod(HttpMethods.GET);
Expand All @@ -1194,11 +1222,39 @@ public boolean handleRedirect(int statusCode, HttpHeaders responseHeaders) {
headers.setIfModifiedSince((String) null);
headers.setIfUnmodifiedSince((String) null);
headers.setIfRange((String) null);

// remove Cookie header if redirect is cross-origin
if (!sameOrigin) {
headers.setCookie((String) null);
}
return true;
}
return false;
}

private static int getEffectivePort(String scheme, int port) {
if (port != -1) {
return port;
}
if ("http".equalsIgnoreCase(scheme)) {
return 80;
}
if ("https".equalsIgnoreCase(scheme)) {
return 443;
}
return -1;
}

private static boolean isSameOrigin(
String scheme1, String host1, int port1,
String scheme2, String host2, int port2) {
int effectivePort1 = getEffectivePort(scheme1, port1);
int effectivePort2 = getEffectivePort(scheme2, port2);
return (scheme1 == null ? scheme2 == null : scheme1.equalsIgnoreCase(scheme2))
&& (host1 == null ? host2 == null : host1.equalsIgnoreCase(host2))
&& (effectivePort1 == effectivePort2);
}

/**
* Returns the sleeper.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.api.client.http.BasicAuthentication;
import com.google.api.client.testing.http.HttpTesting;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockHttpUnsuccessfulResponseHandler;
Expand Down Expand Up @@ -1327,4 +1328,154 @@ public void testVersion_matchesAcceptablePatterns() throws Exception {
String.format("the loaded version '%s' did not match the acceptable pattern", version),
version.matches(acceptableVersionPattern));
}

@Test
public void testHandleRedirect_crossOriginCookieRemoval() throws IOException {
// 1. Same-origin redirect (http://some.org/a/b -> http://some.org/a/z)
{
HttpTransport transport = new MockHttpTransport();
HttpRequest req = transport.createRequestFactory().buildGetRequest(new GenericUrl("http://some.org/a/b"));
req.getHeaders().setCookie("foo=bar");
HttpHeaders responseHeaders = new HttpHeaders().setLocation("http://some.org/a/z");
req.handleRedirect(HttpStatusCodes.STATUS_CODE_SEE_OTHER, responseHeaders);
assertEquals("foo=bar", req.getHeaders().getCookie());
assertEquals("http://some.org/a/z", req.getUrl().toString());
}

// 2. Cross-origin redirect due to host change (http://some.org/a/b -> http://other.org/c)
{
HttpTransport transport = new MockHttpTransport();
HttpRequest req = transport.createRequestFactory().buildGetRequest(new GenericUrl("http://some.org/a/b"));
req.getHeaders().setCookie("foo=bar");
HttpHeaders responseHeaders = new HttpHeaders().setLocation("http://other.org/c");
req.handleRedirect(HttpStatusCodes.STATUS_CODE_SEE_OTHER, responseHeaders);
assertNull(req.getHeaders().getCookie());
assertEquals("http://other.org/c", req.getUrl().toString());
}

// 3. Cross-origin redirect due to scheme change (https://some.org/a/b -> http://some.org/a/z)
{
HttpTransport transport = new MockHttpTransport();
HttpRequest req = transport.createRequestFactory().buildGetRequest(new GenericUrl("https://some.org/a/b"));
req.getHeaders().setCookie("foo=bar");
HttpHeaders responseHeaders = new HttpHeaders().setLocation("http://some.org/a/z");
req.handleRedirect(HttpStatusCodes.STATUS_CODE_SEE_OTHER, responseHeaders);
assertNull(req.getHeaders().getCookie());
assertEquals("http://some.org/a/z", req.getUrl().toString());
}

// 4. Cross-origin redirect due to port change (http://some.org/a/b -> http://some.org:8080/a/z)
{
HttpTransport transport = new MockHttpTransport();
HttpRequest req = transport.createRequestFactory().buildGetRequest(new GenericUrl("http://some.org/a/b"));
req.getHeaders().setCookie("foo=bar");
HttpHeaders responseHeaders = new HttpHeaders().setLocation("http://some.org:8080/a/z");
req.handleRedirect(HttpStatusCodes.STATUS_CODE_SEE_OTHER, responseHeaders);
assertNull(req.getHeaders().getCookie());
assertEquals("http://some.org:8080/a/z", req.getUrl().toString());
}

// 5. Same-origin redirect with implicit/explicit default ports matching (http://some.org/a/b -> http://some.org:80/a/z)
{
HttpTransport transport = new MockHttpTransport();
HttpRequest req = transport.createRequestFactory().buildGetRequest(new GenericUrl("http://some.org/a/b"));
req.getHeaders().setCookie("foo=bar");
HttpHeaders responseHeaders = new HttpHeaders().setLocation("http://some.org:80/a/z");
req.handleRedirect(HttpStatusCodes.STATUS_CODE_SEE_OTHER, responseHeaders);
assertEquals("foo=bar", req.getHeaders().getCookie());
assertEquals("http://some.org:80/a/z", req.getUrl().toString());
}
}

@Test
public void testExecute_crossOriginRedirectCredentialLeakPrevention() throws Exception {
final List<MockLowLevelHttpRequest> recordedRequests = Lists.newArrayList();

HttpTransport transport = new MockHttpTransport() {
@Override
public LowLevelHttpRequest buildRequest(String method, final String url) {
MockLowLevelHttpRequest req = new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
recordedRequests.add(this);
MockLowLevelHttpResponse resp = new MockLowLevelHttpResponse();
if (recordedRequests.size() == 1) {
resp.setStatusCode(302);
resp.addHeader("Location", "https://untrusted-target.com/path");
} else {
resp.setStatusCode(200);
}
return resp;
}
};
return req;
}
};

HttpRequest req = transport.createRequestFactory().buildGetRequest(new GenericUrl("https://example.com/start"));
req.setInterceptor(new BasicAuthentication("myuser", "mypass"));
req.getHeaders().setCookie("mycookie=val");

HttpResponse response = req.execute();
assertEquals(200, response.getStatusCode());
assertEquals(2, recordedRequests.size());

// First request (https://example.com/start)
MockLowLevelHttpRequest firstReq = recordedRequests.get(0);
assertTrue(firstReq.getUrl().contains("example.com"));
assertNotNull(firstReq.getFirstHeaderValue("Authorization"));
assertEquals("mycookie=val", firstReq.getFirstHeaderValue("Cookie"));

// Second request (https://untrusted-target.com/path) - redirect to cross-origin
MockLowLevelHttpRequest secondReq = recordedRequests.get(1);
assertTrue(secondReq.getUrl().contains("untrusted-target.com"));
assertNull(secondReq.getFirstHeaderValue("Authorization"));
assertNull(secondReq.getFirstHeaderValue("Cookie"));
}

@Test
public void testExecute_sameOriginRedirectCredentialLeakPrevention() throws Exception {
final List<MockLowLevelHttpRequest> recordedRequests = Lists.newArrayList();

HttpTransport transport = new MockHttpTransport() {
@Override
public LowLevelHttpRequest buildRequest(String method, final String url) {
MockLowLevelHttpRequest req = new MockLowLevelHttpRequest(url) {
@Override
public LowLevelHttpResponse execute() throws IOException {
recordedRequests.add(this);
MockLowLevelHttpResponse resp = new MockLowLevelHttpResponse();
if (recordedRequests.size() == 1) {
resp.setStatusCode(302);
resp.addHeader("Location", "https://example.com/redirect-path");
} else {
resp.setStatusCode(200);
}
return resp;
}
};
return req;
}
};

HttpRequest req = transport.createRequestFactory().buildGetRequest(new GenericUrl("https://example.com/start"));
req.setInterceptor(new BasicAuthentication("myuser", "mypass"));
req.getHeaders().setCookie("mycookie=val");

HttpResponse response = req.execute();
assertEquals(200, response.getStatusCode());
assertEquals(2, recordedRequests.size());

// First request
MockLowLevelHttpRequest firstReq = recordedRequests.get(0);
assertTrue(firstReq.getUrl().contains("example.com/start"));
assertNotNull(firstReq.getFirstHeaderValue("Authorization"));
assertEquals("mycookie=val", firstReq.getFirstHeaderValue("Cookie"));

// Second request - redirect same-origin
MockLowLevelHttpRequest secondReq = recordedRequests.get(1);
assertTrue(secondReq.getUrl().contains("example.com/redirect-path"));
assertNotNull(secondReq.getFirstHeaderValue("Authorization"));
assertEquals("mycookie=val", secondReq.getFirstHeaderValue("Cookie"));
}
}
Loading