YARN-2528. Relaxed http response split vulnerability protection for the origins header and made it accept multiple origins in CrossOriginFilter. Contributed by Jonathan Eagles.

This commit is contained in:
Zhijie Shen 2014-09-12 21:33:01 -07:00
parent 3c9181722b
commit 98588cf044
3 changed files with 61 additions and 31 deletions

View File

@ -351,6 +351,10 @@ Release 2.6.0 - UNRELEASED
YARN-2542. Fixed NPE when retrieving ApplicationReport from TimeLineServer.
(Zhijie Shen via jianhe)
YARN-2528. Relaxed http response split vulnerability protection for the origins
header and made it accept multiple origins in CrossOriginFilter. (Jonathan
Eagles via zjshen)
Release 2.5.1 - 2014-09-05
INCOMPATIBLE CHANGES

View File

@ -19,8 +19,6 @@
package org.apache.hadoop.yarn.server.timeline.webapp;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -106,12 +104,12 @@ public class CrossOriginFilter implements Filter {
private void doCrossFilter(HttpServletRequest req, HttpServletResponse res) {
String origin = encodeHeader(req.getHeader(ORIGIN));
if (!isCrossOrigin(origin)) {
String originsList = encodeHeader(req.getHeader(ORIGIN));
if (!isCrossOrigin(originsList)) {
return;
}
if (!isOriginAllowed(origin)) {
if (!areOriginsAllowed(originsList)) {
return;
}
@ -127,7 +125,7 @@ public class CrossOriginFilter implements Filter {
return;
}
res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, originsList);
res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString());
res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, getAllowedMethodsHeader());
res.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, getAllowedHeadersHeader());
@ -191,35 +189,36 @@ public class CrossOriginFilter implements Filter {
if (header == null) {
return null;
}
try {
// Protect against HTTP response splitting vulnerability
// since value is written as part of the response header
return URLEncoder.encode(header, "ASCII");
} catch (UnsupportedEncodingException e) {
return null;
}
// Protect against HTTP response splitting vulnerability
// since value is written as part of the response header
// Ensure this header only has one header by removing
// CRs and LFs
return header.split("\n|\r")[0].trim();
}
static boolean isCrossOrigin(String origin) {
return origin != null;
static boolean isCrossOrigin(String originsList) {
return originsList != null;
}
@VisibleForTesting
boolean isOriginAllowed(String origin) {
boolean areOriginsAllowed(String originsList) {
if (allowAllOrigins) {
return true;
}
for (String allowedOrigin : allowedOrigins) {
if (allowedOrigin.contains("*")) {
String regex = allowedOrigin.replace(".", "\\.").replace("*", ".*");
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(origin);
if (m.matches()) {
String[] origins = originsList.trim().split("\\s+");
for (String origin : origins) {
for (String allowedOrigin : allowedOrigins) {
if (allowedOrigin.contains("*")) {
String regex = allowedOrigin.replace(".", "\\.").replace("*", ".*");
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(origin);
if (m.matches()) {
return true;
}
} else if (allowedOrigin.equals(origin)) {
return true;
}
} else if (allowedOrigin.equals(origin)) {
return true;
}
}
return false;

View File

@ -77,7 +77,27 @@ public class TestCrossOriginFilter {
// Object under test
CrossOriginFilter filter = new CrossOriginFilter();
filter.init(filterConfig);
Assert.assertTrue(filter.isOriginAllowed("example.com"));
Assert.assertTrue(filter.areOriginsAllowed("example.com"));
}
@Test
public void testEncodeHeaders() {
String validOrigin = "http://localhost:12345";
String encodedValidOrigin = CrossOriginFilter.encodeHeader(validOrigin);
Assert.assertEquals("Valid origin encoding should match exactly",
validOrigin, encodedValidOrigin);
String httpResponseSplitOrigin = validOrigin + " \nSecondHeader: value";
String encodedResponseSplitOrigin =
CrossOriginFilter.encodeHeader(httpResponseSplitOrigin);
Assert.assertEquals("Http response split origin should be protected against",
validOrigin, encodedResponseSplitOrigin);
// Test Origin List
String validOriginList = "http://foo.example.com:12345 http://bar.example.com:12345";
String encodedValidOriginList = CrossOriginFilter.encodeHeader(validOriginList);
Assert.assertEquals("Valid origin list encoding should match exactly",
validOriginList, encodedValidOriginList);
}
@Test
@ -93,10 +113,17 @@ public class TestCrossOriginFilter {
filter.init(filterConfig);
// match multiple sub-domains
Assert.assertFalse(filter.isOriginAllowed("example.com"));
Assert.assertFalse(filter.isOriginAllowed("foo:example.com"));
Assert.assertTrue(filter.isOriginAllowed("foo.example.com"));
Assert.assertTrue(filter.isOriginAllowed("foo.bar.example.com"));
Assert.assertFalse(filter.areOriginsAllowed("example.com"));
Assert.assertFalse(filter.areOriginsAllowed("foo:example.com"));
Assert.assertTrue(filter.areOriginsAllowed("foo.example.com"));
Assert.assertTrue(filter.areOriginsAllowed("foo.bar.example.com"));
// First origin is allowed
Assert.assertTrue(filter.areOriginsAllowed("foo.example.com foo.nomatch.com"));
// Second origin is allowed
Assert.assertTrue(filter.areOriginsAllowed("foo.nomatch.com foo.example.com"));
// No origin in list is allowed
Assert.assertFalse(filter.areOriginsAllowed("foo.nomatch1.com foo.nomatch2.com"));
}
@Test
@ -238,7 +265,7 @@ public class TestCrossOriginFilter {
Assert.assertTrue("Allowed methods do not match",
filter.getAllowedMethodsHeader()
.compareTo("GET,POST") == 0);
Assert.assertTrue(filter.isOriginAllowed("example.com"));
Assert.assertTrue(filter.areOriginsAllowed("example.com"));
//destroy filter values and clear conf
filter.destroy();
@ -260,7 +287,7 @@ public class TestCrossOriginFilter {
Assert.assertTrue("Allowed methods do not match",
filter.getAllowedMethodsHeader()
.compareTo("GET,HEAD") == 0);
Assert.assertTrue(filter.isOriginAllowed("newexample.com"));
Assert.assertTrue(filter.areOriginsAllowed("newexample.com"));
//destroy filter values
filter.destroy();