From dac72d166568b30d3d2e70643cdd92bd43ca22d9 Mon Sep 17 00:00:00 2001 From: Daniel Gredler Date: Thu, 13 Sep 2018 12:18:33 -0400 Subject: [PATCH] Implement URL host whitelist Allows users to build customized Getdown JARs which can only talk to whitelisted hosts. This can be useful for very security-conscious organizations which want to distribute Getdown internally as a standard application bootstrapping mechanism, but want to ensure that it can only be used for internal applications. This is an adaptation of the whitelist proof-of-concept discussed on the mailing list, adjusted to use the new Build class. (Triggered by internal security audit and Fortify analysis.) --- core/pom.xml | 15 +- .../threerings/getdown/data/Application.java | 9 +- .../threerings/getdown/data/Build.java.tmpl | 21 +++ .../com/threerings/getdown/util/Config.java | 22 ++- .../getdown/util/HostWhitelist.java | 68 ++++++++ .../getdown/util/HostWhitelistTest.java | 158 ++++++++++++++++++ 6 files changed, 281 insertions(+), 12 deletions(-) create mode 100644 core/src/main/java/com/threerings/getdown/util/HostWhitelist.java create mode 100644 core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java diff --git a/core/pom.xml b/core/pom.xml index 085db37..2156eea 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -27,6 +27,16 @@ + + + + + @@ -66,14 +76,15 @@ - + - + + diff --git a/core/src/main/java/com/threerings/getdown/data/Application.java b/core/src/main/java/com/threerings/getdown/data/Application.java index ef282d6..2d0d352 100644 --- a/core/src/main/java/com/threerings/getdown/data/Application.java +++ b/core/src/main/java/com/threerings/getdown/data/Application.java @@ -484,7 +484,7 @@ public class Application String suffix = _trackingURLSuffix == null ? "" : _trackingURLSuffix; String ga = getGATrackingCode(); return _trackingURL == null ? null : - new URL(_trackingURL + encodePath(event + suffix + ga)); + HostWhitelist.INSTANCE.verify(new URL(_trackingURL + encodePath(event + suffix + ga))); } catch (MalformedURLException mue) { log.warning("Invalid tracking URL", "path", _trackingURL, "event", event, "error", mue); return null; @@ -586,7 +586,7 @@ public class Application // if we are a versioned deployment, create a versioned appbase try { - _vappbase = (_version < 0) ? new URL(_appbase) : createVAppBase(_version); + _vappbase = createVAppBase(_version); } catch (MalformedURLException mue) { String err = MessageUtil.tcompose("m.invalid_appbase", _appbase); throw (IOException) new IOException(err).initCause(mue); @@ -601,7 +601,7 @@ public class Application latest = SysProps.replaceDomain(latest); } try { - _latest = new URL(latest); + _latest = HostWhitelist.INSTANCE.verify(new URL(latest)); } catch (MalformedURLException mue) { log.warning("Invalid URL for latest attribute.", mue); } @@ -1408,7 +1408,8 @@ public class Application protected URL createVAppBase (long version) throws MalformedURLException { - return new URL(_appbase.replace("%VERSION%", "" + version)); + String url = version < 0 ? _appbase : _appbase.replace("%VERSION%", "" + version); + return HostWhitelist.INSTANCE.verify(new URL(url)); } /** diff --git a/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl b/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl index d3fdccf..60a8ff3 100644 --- a/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl +++ b/core/src/main/java/com/threerings/getdown/data/Build.java.tmpl @@ -5,6 +5,11 @@ package com.threerings.getdown.data; +import java.util.Arrays; +import java.util.List; + +import com.threerings.getdown.util.StringUtil; + /** * Contains static data provided during the build process. */ @@ -19,4 +24,20 @@ public class Build { public static String version () { return "@build_version@"; } + + /** + *

The hosts which Getdown is allowed to communicate with. An empty list indicates that + * no whitelist is configured and there are no limitations. By default, no host whitelist + * is added to the binary, so it can be used to download and run applications from any + * server. + * + *

To create a custom Getdown build that can only talk to whitelisted servers, set + * the {@code getdown.host.whitelist} property on the command line while building the JAR + * (e.g. {@code mvn package -Dgetdown.host.whitelist=my.server.com}). Wildcards can be used + * (e.g. {@code *.mycompany.com}) and multiple values can be separated by commas + * (e.g. {@code app1.foo.com,app2.bar.com,app3.baz.com}). + */ + public static List hostWhitelist () { + return Arrays.asList(StringUtil.parseStringArray("@host_whitelist@")); + } } diff --git a/core/src/main/java/com/threerings/getdown/util/Config.java b/core/src/main/java/com/threerings/getdown/util/Config.java index ed7303b..8a34b1b 100644 --- a/core/src/main/java/com/threerings/getdown/util/Config.java +++ b/core/src/main/java/com/threerings/getdown/util/Config.java @@ -10,6 +10,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; @@ -17,8 +19,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import com.threerings.getdown.util.StringUtil; - import static com.threerings.getdown.Log.log; /** @@ -320,11 +320,21 @@ public class Config public String getUrl (String name, String def) { String value = getString(name + "." + Locale.getDefault().getLanguage()); - if (!StringUtil.isBlank(value)) { - return value; + if (StringUtil.isBlank(value)) { + value = getString(name); } - value = getString(name); - return StringUtil.isBlank(value) ? def : value; + if (StringUtil.isBlank(value)) { + value = def; + } + if (!StringUtil.isBlank(value)) { + try { + HostWhitelist.INSTANCE.verify(new URL(value)); + } catch (MalformedURLException e) { + log.warning("Invalid URL.", "url", value, e); + value = null; + } + } + return value; } /** diff --git a/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java b/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java new file mode 100644 index 0000000..c44c3af --- /dev/null +++ b/core/src/main/java/com/threerings/getdown/util/HostWhitelist.java @@ -0,0 +1,68 @@ +// +// Getdown - application installer, patcher and launcher +// Copyright (C) 2004-2016 Getdown authors +// https://github.com/threerings/getdown/blob/master/LICENSE + +package com.threerings.getdown.util; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import com.threerings.getdown.data.Build; + +/** + * Optional support for compiling a URL host whitelist into the Getdown JAR. + * Useful if you're on the paranoid end of the security spectrum. + * + * @see Build#hostWhitelist() + */ +public final class HostWhitelist +{ + public static final HostWhitelist INSTANCE = new HostWhitelist(); + + private final List _hosts; + + HostWhitelist () + { + this(Build.hostWhitelist()); + } + + HostWhitelist (List hosts) + { + _hosts = Collections.unmodifiableList(hosts); + } + + /** + * Verifies that the specified URL should be accessible, per this host whitelist. + * If the URL should not be accessible, this method throws a {@link MalformedURLException}. + * If the URL should be accessible, this method simply returns the {@link URL} passed in. + */ + public final URL verify (URL url) throws MalformedURLException + { + if (url == null || _hosts.isEmpty()) { + // either there is no URL to validate or no whitelist was configured + return url; + } + + String urlHost = url.getHost(); + + for (String host : _hosts) { + String regex = host.replace(".", "\\.").replace("*", ".*"); + if (urlHost.matches(regex)) { + return url; + } + } + + throw new MalformedURLException("The host for the specified URL (" + url + + ") is not in the host whitelist: " + _hosts); + } + + /** {@inheritDoc} */ + @Override + public String toString() + { + return _hosts.toString(); + } +} diff --git a/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java b/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java new file mode 100644 index 0000000..c8fc24f --- /dev/null +++ b/core/src/test/java/com/threerings/getdown/util/HostWhitelistTest.java @@ -0,0 +1,158 @@ +// +// Getdown - application installer, patcher and launcher +// Copyright (C) 2004-2016 Getdown authors +// https://github.com/threerings/getdown/blob/master/LICENSE + +package com.threerings.getdown.util; + +import static org.junit.Assert.assertEquals; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + +import org.junit.Test; + +/** + * Tests {@link HostWhitelist}. + */ +public class HostWhitelistTest +{ + @Test + public void testVerify () throws MalformedURLException + { + checkCanVerify("foo.com", "http://foo.com", true); + checkCanVerify("foo.com", "http://foo.com/", true); + checkCanVerify("foo.com", "http://foo.com/x/y/z", true); + checkCanVerify("foo.com", "http://www.foo.com", false); + checkCanVerify("foo.com", "http://www.foo.com/", false); + checkCanVerify("foo.com", "http://www.foo.com/x/y/z", false); + checkCanVerify("foo.com", "http://a.b.foo.com", false); + checkCanVerify("foo.com", "http://a.b.foo.com/", false); + checkCanVerify("foo.com", "http://a.b.foo.com/x/y/z", false); + checkCanVerify("foo.com", "http://oo.com", false); + checkCanVerify("foo.com", "http://f.oo.com", false); + checkCanVerify("foo.com", "http://a.f.oo.com", false); + + checkCanVerify("*.foo.com", "http://foo.com", false); + checkCanVerify("*.foo.com", "http://foo.com/", false); + checkCanVerify("*.foo.com", "http://foo.com/x/y/z", false); + checkCanVerify("*.foo.com", "http://www.foo.com", true); + checkCanVerify("*.foo.com", "http://www.foo.com/", true); + checkCanVerify("*.foo.com", "http://www.foo.com/x/y/z", true); + checkCanVerify("*.foo.com", "http://a.b.foo.com", true); + checkCanVerify("*.foo.com", "http://a.b.foo.com/", true); + checkCanVerify("*.foo.com", "http://a.b.foo.com/x/y/z", true); + checkCanVerify("*.foo.com", "http://oo.com", false); + checkCanVerify("*.foo.com", "http://f.oo.com", false); + checkCanVerify("*.foo.com", "http://a.f.oo.com", false); + + checkCanVerify("*.com", "http://foo.com", true); + checkCanVerify("*.com", "http://foo.com/", true); + checkCanVerify("*.com", "http://foo.com/x/y/z", true); + checkCanVerify("*.com", "http://www.foo.com", true); + checkCanVerify("*.com", "http://www.foo.com/", true); + checkCanVerify("*.com", "http://www.foo.com/x/y/z", true); + checkCanVerify("*.com", "http://a.b.foo.com", true); + checkCanVerify("*.com", "http://a.b.foo.com/", true); + checkCanVerify("*.com", "http://a.b.foo.com/x/y/z", true); + checkCanVerify("*.com", "http://oo.com", true); + checkCanVerify("*.com", "http://f.oo.com", true); + checkCanVerify("*.com", "http://a.f.oo.com", true); + + checkCanVerify("*.net", "http://foo.com", false); + checkCanVerify("*.net", "http://foo.com/", false); + checkCanVerify("*.net", "http://foo.com/x/y/z", false); + checkCanVerify("*.net", "http://www.foo.com", false); + checkCanVerify("*.net", "http://www.foo.com/", false); + checkCanVerify("*.net", "http://www.foo.com/x/y/z", false); + checkCanVerify("*.net", "http://a.b.foo.com", false); + checkCanVerify("*.net", "http://a.b.foo.com/", false); + checkCanVerify("*.net", "http://a.b.foo.com/x/y/z", false); + checkCanVerify("*.net", "http://oo.com", false); + checkCanVerify("*.net", "http://f.oo.com", false); + checkCanVerify("*.net", "http://a.f.oo.com", false); + + checkCanVerify("www.*.com", "http://foo.com", false); + checkCanVerify("www.*.com", "http://foo.com/", false); + checkCanVerify("www.*.com", "http://foo.com/x/y/z", false); + checkCanVerify("www.*.com", "http://www.foo.com", true); + checkCanVerify("www.*.com", "http://www.foo.com/", true); + checkCanVerify("www.*.com", "http://www.foo.com/x/y/z", true); + checkCanVerify("www.*.com", "http://a.b.foo.com", false); + checkCanVerify("www.*.com", "http://a.b.foo.com/", false); + checkCanVerify("www.*.com", "http://a.b.foo.com/x/y/z", false); + checkCanVerify("www.*.com", "http://oo.com", false); + checkCanVerify("www.*.com", "http://f.oo.com", false); + checkCanVerify("www.*.com", "http://a.f.oo.com", false); + checkCanVerify("www.*.com", "http://www.a.f.oo.com", true); + + checkCanVerify("foo.*", "http://foo.com", true); + checkCanVerify("foo.*", "http://foo.com/", true); + checkCanVerify("foo.*", "http://foo.com/x/y/z", true); + checkCanVerify("foo.*", "http://www.foo.com", false); + checkCanVerify("foo.*", "http://www.foo.com/", false); + checkCanVerify("foo.*", "http://www.foo.com/x/y/z", false); + checkCanVerify("foo.*", "http://a.b.foo.com", false); + checkCanVerify("foo.*", "http://a.b.foo.com/", false); + checkCanVerify("foo.*", "http://a.b.foo.com/x/y/z", false); + checkCanVerify("foo.*", "http://oo.com", false); + checkCanVerify("foo.*", "http://f.oo.com", false); + checkCanVerify("foo.*", "http://a.f.oo.com", false); + + checkCanVerify("*.foo.*", "http://foo.com", false); + checkCanVerify("*.foo.*", "http://foo.com/", false); + checkCanVerify("*.foo.*", "http://foo.com/x/y/z", false); + checkCanVerify("*.foo.*", "http://www.foo.com", true); + checkCanVerify("*.foo.*", "http://www.foo.com/", true); + checkCanVerify("*.foo.*", "http://www.foo.com/x/y/z", true); + checkCanVerify("*.foo.*", "http://a.b.foo.com", true); + checkCanVerify("*.foo.*", "http://a.b.foo.com/", true); + checkCanVerify("*.foo.*", "http://a.b.foo.com/x/y/z", true); + checkCanVerify("*.foo.*", "http://oo.com", false); + checkCanVerify("*.foo.*", "http://f.oo.com", false); + checkCanVerify("*.foo.*", "http://a.f.oo.com", false); + + checkCanVerify("127.0.0.1", "http://127.0.0.1", true); + checkCanVerify("127.0.0.1", "http://127.0.0.1/", true); + checkCanVerify("127.0.0.1", "http://127.0.0.1/x/y/z", true); + checkCanVerify("*.0.0.1", "http://127.0.0.1/abc", true); + checkCanVerify("127.*.0.1", "http://127.0.0.1/abc", true); + checkCanVerify("127.0.*.1", "http://127.0.0.1/abc", true); + checkCanVerify("127.0.0.*", "http://127.0.0.1/abc", true); + checkCanVerify("127.*.1", "http://127.0.0.1/abc", true); + checkCanVerify("*.0.1", "http://127.0.0.1/abc", true); + checkCanVerify("127.0.*", "http://127.0.0.1/abc", true); + checkCanVerify("*", "http://127.0.0.1/abc", true); + checkCanVerify("127.0.0.2", "http://127.0.0.1", false); + checkCanVerify("127.0.2.1", "http://127.0.0.1", false); + checkCanVerify("127.2.0.1", "http://127.0.0.1", false); + checkCanVerify("222.0.0.1", "http://127.0.0.1", false); + + checkCanVerify("", "http://foo.com", true); + checkCanVerify("", "http://aaa.bbb.net/xyz", true); + checkCanVerify("", "https://127.0.0.1/abc", true); + + checkCanVerify("aaa.bbb.com,xxx.yyy.com, *.jjj.net", "http://aaa.bbb.com/m", true); + checkCanVerify("aaa.bbb.com, xxx.yyy.com,*.jjj.net", "http://xxx.yyy.com/n", true); + checkCanVerify("aaa.bbb.com,xxx.yyy.com, *.jjj.net", "http://www.jjj.net/o", true); + } + + private static void checkCanVerify (String whitelist, String url, boolean expectedToPass) + throws MalformedURLException + { + HostWhitelist w = new HostWhitelist(Arrays.asList(StringUtil.parseStringArray(whitelist))); + URL u = new URL(url); + boolean passed; + + try { + w.verify(u); + passed = true; + } catch (MalformedURLException e) { + passed = false; + } + + assertEquals("with whitelist '" + whitelist + "' and URL '" + url + "'", + expectedToPass, passed); + } +}