diff --git a/module-client/src/main/java/org/apache/http/impl/cookie/PublicSuffixFilter.java b/module-client/src/main/java/org/apache/http/impl/cookie/PublicSuffixFilter.java new file mode 100644 index 000000000..4faf6d280 --- /dev/null +++ b/module-client/src/main/java/org/apache/http/impl/cookie/PublicSuffixFilter.java @@ -0,0 +1,92 @@ +package org.apache.http.impl.cookie; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.apache.http.client.utils.Punycode; +import org.apache.http.cookie.Cookie; +import org.apache.http.cookie.CookieAttributeHandler; +import org.apache.http.cookie.CookieOrigin; +import org.apache.http.cookie.MalformedCookieException; +import org.apache.http.cookie.SetCookie; + +/** + * Wraps a CookieAttributeHandler and leverages its match method + * to never match a suffix from a black list. May be used to provide + * additional security for cross-site attack types by preventing + * cookies from apparent domains that are not publicly available. + * An uptodate list of suffixes can be obtained from + * publicsuffix.org + * + * @author Ortwin Glück + */ +public class PublicSuffixFilter implements CookieAttributeHandler { + private CookieAttributeHandler wrapped; + private Set exceptions; + private Set suffixes; + + public PublicSuffixFilter(CookieAttributeHandler wrapped) { + this.wrapped = wrapped; + } + + /** + * Sets the suffix blacklist patterns. + * A pattern can be "com", "*.jp" + * TODO add support for patterns like "lib.*.us" + * @param suffixes + */ + public void setPublicSuffixes(Collection suffixes) { + this.suffixes = new HashSet(suffixes); + } + + /** + * Sets the exceptions from the blacklist. Exceptions can not be patterns. + * TODO add support for patterns + * @param exceptions + */ + public void setExceptions(Collection exceptions) { + this.exceptions = new HashSet(exceptions); + } + + /** + * Never matches if the cookie's domain is from the blacklist. + */ + public boolean match(Cookie cookie, CookieOrigin origin) { + if (isForPublicSuffix(cookie)) return false; + return wrapped.match(cookie, origin); + } + + public void parse(SetCookie cookie, String value) throws MalformedCookieException { + wrapped.parse(cookie, value); + } + + public void validate(Cookie cookie, CookieOrigin origin) throws MalformedCookieException { + wrapped.validate(cookie, origin); + } + + private boolean isForPublicSuffix(Cookie cookie) { + String domain = cookie.getDomain(); + if (domain.startsWith(".")) domain = domain.substring(1); + domain = Punycode.toUnicode(domain); + + // An exception rule takes priority over any other matching rule. + if (this.exceptions != null) { + if (this.exceptions.contains(domain)) return false; + } + + + if (this.suffixes == null) return false; + + do { + if (this.suffixes.contains(domain)) return true; + // patterns + if (domain.startsWith("*.")) domain = domain.substring(2); + int nextdot = domain.indexOf('.'); + if (nextdot == -1) break; + domain = "*" + domain.substring(nextdot); + } while (domain.length() > 0); + + return false; + } +} diff --git a/module-client/src/main/java/org/apache/http/impl/cookie/PublicSuffixListParser.java b/module-client/src/main/java/org/apache/http/impl/cookie/PublicSuffixListParser.java new file mode 100644 index 000000000..ad51838ce --- /dev/null +++ b/module-client/src/main/java/org/apache/http/impl/cookie/PublicSuffixListParser.java @@ -0,0 +1,79 @@ +package org.apache.http.impl.cookie; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Parses the list from publicsuffix.org + * and configures a PublicSuffixFilter. + * + * @author Ortwin Glück + */ +public class PublicSuffixListParser { + private static final int MAX_LINE_LEN = 256; + private PublicSuffixFilter filter; + + PublicSuffixListParser(PublicSuffixFilter filter) { + this.filter = filter; + } + + /** + * Parses the public suffix list format. + * When creating the reader from the file, make sure to + * use the correct encoding (the original list is in UTF-8). + * + * @param list the suffix list. The caller is responsible for closing the reader. + * @throws IOException on error while reading from list + */ + public void parse(Reader list) throws IOException { + Collection rules = new ArrayList(); + Collection exceptions = new ArrayList(); + BufferedReader r = new BufferedReader(list); + StringBuilder sb = new StringBuilder(256); + boolean more = true; + while (more) { + more = readLine(r, sb); + String line = sb.toString(); + if (line.length() == 0) continue; + if (line.startsWith("//")) continue; //entire lines can also be commented using // + if (line.startsWith(".")) line = line.substring(1); // A leading dot is optional + // An exclamation mark (!) at the start of a rule marks an exception to a previous wildcard rule + boolean isException = line.startsWith("!"); + if (isException) line = line.substring(1); + + if (isException) { + exceptions.add(line); + } else { + rules.add(line); + } + } + + filter.setPublicSuffixes(rules); + filter.setExceptions(exceptions); + } + + /** + * + * @param r + * @param sb + * @return false when the end of the stream is reached + * @throws IOException + */ + private boolean readLine(Reader r, StringBuilder sb) throws IOException { + sb.setLength(0); + int b; + boolean hitWhitespace = false; + while ((b = r.read()) != -1) { + char c = (char) b; + if (c == '\n') break; + // Each line is only read up to the first whitespace + if (Character.isWhitespace(c)) hitWhitespace = true; + if (!hitWhitespace) sb.append(c); + if (sb.length() > MAX_LINE_LEN) throw new IOException("Line too long"); // prevent excess memory usage + } + return (b != -1); + } +} diff --git a/module-client/src/test/java/org/apache/http/impl/cookie/TestAllCookieImpl.java b/module-client/src/test/java/org/apache/http/impl/cookie/TestAllCookieImpl.java index 68c2746f6..08be0a96c 100644 --- a/module-client/src/test/java/org/apache/http/impl/cookie/TestAllCookieImpl.java +++ b/module-client/src/test/java/org/apache/http/impl/cookie/TestAllCookieImpl.java @@ -53,6 +53,7 @@ public class TestAllCookieImpl extends TestCase { suite.addTest(TestCookieRFC2109Spec.suite()); suite.addTest(TestCookieRFC2965Spec.suite()); suite.addTest(TestCookieBestMatchSpec.suite()); + suite.addTest(TestPublicSuffixListParser.suite()); return suite; } diff --git a/module-client/src/test/java/org/apache/http/impl/cookie/TestBasicCookieAttribHandlers.java b/module-client/src/test/java/org/apache/http/impl/cookie/TestBasicCookieAttribHandlers.java index b312bef32..73d3636de 100644 --- a/module-client/src/test/java/org/apache/http/impl/cookie/TestBasicCookieAttribHandlers.java +++ b/module-client/src/test/java/org/apache/http/impl/cookie/TestBasicCookieAttribHandlers.java @@ -32,6 +32,7 @@ package org.apache.http.impl.cookie; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; import java.util.Locale; @@ -42,6 +43,16 @@ import junit.framework.TestSuite; import org.apache.http.cookie.CookieAttributeHandler; import org.apache.http.cookie.CookieOrigin; import org.apache.http.cookie.MalformedCookieException; +import org.apache.http.impl.cookie.BasicClientCookie; +import org.apache.http.impl.cookie.BasicCommentHandler; +import org.apache.http.impl.cookie.BasicDomainHandler; +import org.apache.http.impl.cookie.BasicExpiresHandler; +import org.apache.http.impl.cookie.BasicMaxAgeHandler; +import org.apache.http.impl.cookie.BasicPathHandler; +import org.apache.http.impl.cookie.BasicSecureHandler; +import org.apache.http.impl.cookie.DateUtils; +import org.apache.http.impl.cookie.PublicSuffixFilter; +import org.apache.http.impl.cookie.RFC2109DomainHandler; public class TestBasicCookieAttribHandlers extends TestCase { @@ -458,4 +469,26 @@ public class TestBasicCookieAttribHandlers extends TestCase { } } + public void testPublicSuffixFilter() throws Exception { + BasicClientCookie cookie = new BasicClientCookie("name", "value"); + + PublicSuffixFilter h = new PublicSuffixFilter(new RFC2109DomainHandler()); + h.setPublicSuffixes(Arrays.asList(new String[] { "co.uk", "com" })); + + cookie.setDomain(".co.uk"); + assertFalse(h.match(cookie, new CookieOrigin("apache.co.uk", 80, "/stuff", false))); + + cookie.setDomain("co.uk"); + assertFalse(h.match(cookie, new CookieOrigin("apache.co.uk", 80, "/stuff", false))); + + cookie.setDomain(".com"); + assertFalse(h.match(cookie, new CookieOrigin("apache.com", 80, "/stuff", false))); + + cookie.setDomain("com"); + assertFalse(h.match(cookie, new CookieOrigin("apache.com", 80, "/stuff", false))); + + cookie.setDomain("localhost"); + assertTrue(h.match(cookie, new CookieOrigin("localhost", 80, "/stuff", false))); + } + } diff --git a/module-client/src/test/java/org/apache/http/impl/cookie/TestPublicSuffixListParser.java b/module-client/src/test/java/org/apache/http/impl/cookie/TestPublicSuffixListParser.java new file mode 100644 index 000000000..c0c0761c8 --- /dev/null +++ b/module-client/src/test/java/org/apache/http/impl/cookie/TestPublicSuffixListParser.java @@ -0,0 +1,84 @@ +package org.apache.http.impl.cookie; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.apache.http.cookie.CookieOrigin; + +public class TestPublicSuffixListParser extends TestCase { + private static final String LIST_FILE = "suffixlist.txt"; + private PublicSuffixFilter filter; + + public TestPublicSuffixListParser(String testName) { + super(testName); + try { + Reader r = new InputStreamReader(getClass().getResourceAsStream(LIST_FILE), "UTF-8"); + filter = new PublicSuffixFilter(new RFC2109DomainHandler()); + PublicSuffixListParser parser = new PublicSuffixListParser(filter); + parser.parse(r); + } catch (IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + public static Test suite() { + return new TestSuite(TestPublicSuffixListParser.class); + } + + public static void main(String args[]) { + String[] testCaseName = { TestPublicSuffixListParser.class.getName() }; + junit.textui.TestRunner.main(testCaseName); + } + + public void testParse() throws Exception { + BasicClientCookie cookie = new BasicClientCookie("name", "value"); + + cookie.setDomain(".jp"); + assertFalse(filter.match(cookie, new CookieOrigin("apache.jp", 80, "/stuff", false))); + + cookie.setDomain(".ac.jp"); + assertFalse(filter.match(cookie, new CookieOrigin("apache.ac.jp", 80, "/stuff", false))); + + cookie.setDomain(".any.tokyo.jp"); + assertFalse(filter.match(cookie, new CookieOrigin("apache.any.tokyo.jp", 80, "/stuff", false))); + + // exception + cookie.setDomain(".metro.tokyo.jp"); + assertTrue(filter.match(cookie, new CookieOrigin("apache.metro.tokyo.jp", 80, "/stuff", false))); + } + + public void testUnicode() throws Exception { + BasicClientCookie cookie = new BasicClientCookie("name", "value"); + + cookie.setDomain(".h\u00E5.no"); // \u00E5 is + assertFalse(filter.match(cookie, new CookieOrigin("apache.h\u00E5.no", 80, "/stuff", false))); + + cookie.setDomain(".xn--h-2fa.no"); + assertFalse(filter.match(cookie, new CookieOrigin("apache.xn--h-2fa.no", 80, "/stuff", false))); + + cookie.setDomain(".h\u00E5.no"); + assertFalse(filter.match(cookie, new CookieOrigin("apache.xn--h-2fa.no", 80, "/stuff", false))); + + cookie.setDomain(".xn--h-2fa.no"); + assertFalse(filter.match(cookie, new CookieOrigin("apache.h\u00E5.no", 80, "/stuff", false))); + } + + public void testWhitespace() throws Exception { + BasicClientCookie cookie = new BasicClientCookie("name", "value"); + cookie.setDomain(".xx"); + assertFalse(filter.match(cookie, new CookieOrigin("apache.xx", 80, "/stuff", false))); + + // yy appears after whitespace + cookie.setDomain(".yy"); + assertTrue(filter.match(cookie, new CookieOrigin("apache.yy", 80, "/stuff", false))); + + // zz is commented + cookie.setDomain(".zz"); + assertTrue(filter.match(cookie, new CookieOrigin("apache.zz", 80, "/stuff", false))); + } +} diff --git a/module-client/src/test/java/org/apache/http/impl/cookie/suffixlist.txt b/module-client/src/test/java/org/apache/http/impl/cookie/suffixlist.txt new file mode 100644 index 000000000..9a18a34ac --- /dev/null +++ b/module-client/src/test/java/org/apache/http/impl/cookie/suffixlist.txt @@ -0,0 +1,13 @@ +jp +ac.jp +*.tokyo.jp +!metro.tokyo.jp + +// unicode +no +hÃ¥.no + + +// invalid +xx yy +//zz \ No newline at end of file