Merge pull request #138 from sdgx/url-whitelist-2

Implement URL host whitelist
This commit is contained in:
Michael Bayne
2018-09-14 14:19:23 -07:00
committed by GitHub
6 changed files with 281 additions and 12 deletions
+13 -2
View File
@@ -27,6 +27,16 @@
</dependency>
</dependencies>
<!-- 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 this
property on the command line, e.g. -Dgetdown.host.whitelist=my.server.com
Wildcards can be used (*.mycompany.com) and multiple values can be
separated by commas (app1.foo.com,app2.bar.com,app3.baz.com). -->
<properties>
<getdown.host.whitelist></getdown.host.whitelist>
</properties>
<build>
<resources>
<resource> <!-- include the LICENSE file in the jar -->
@@ -66,14 +76,15 @@
<configuration>
<target>
<tstamp>
<format property="build_time" pattern="yyyy-MM-dd HH:mm"/>
<format property="getdown.build.time" pattern="yyyy-MM-dd HH:mm"/>
</tstamp>
<copy file="${project.build.sourceDirectory}/com/threerings/getdown/data/Build.java.tmpl"
tofile="${project.build.sourceDirectory}/com/threerings/getdown/data/Build.java"
overwrite="true">
<filterset>
<filter token="build_time" value="${build_time}"/>
<filter token="build_time" value="${getdown.build.time}"/>
<filter token="build_version" value="${project.version}"/>
<filter token="host_whitelist" value="${getdown.host.whitelist}"/>
</filterset>
</copy>
</target>
@@ -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));
}
/**
@@ -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@";
}
/**
* <p>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.
*
* <p>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<String> hostWhitelist () {
return Arrays.asList(StringUtil.parseStringArray("@host_whitelist@"));
}
}
@@ -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);
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;
}
/**
@@ -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<String> _hosts;
HostWhitelist ()
{
this(Build.hostWhitelist());
}
HostWhitelist (List<String> 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();
}
}
@@ -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);
}
}