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.)
This commit is contained in:
+13
-2
@@ -27,6 +27,16 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</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>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
<resource> <!-- include the LICENSE file in the jar -->
|
<resource> <!-- include the LICENSE file in the jar -->
|
||||||
@@ -66,14 +76,15 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<target>
|
<target>
|
||||||
<tstamp>
|
<tstamp>
|
||||||
<format property="build_time" pattern="yyyy-MM-dd HH:mm"/>
|
<format property="getdown.build.time" pattern="yyyy-MM-dd HH:mm"/>
|
||||||
</tstamp>
|
</tstamp>
|
||||||
<copy file="${project.build.sourceDirectory}/com/threerings/getdown/data/Build.java.tmpl"
|
<copy file="${project.build.sourceDirectory}/com/threerings/getdown/data/Build.java.tmpl"
|
||||||
tofile="${project.build.sourceDirectory}/com/threerings/getdown/data/Build.java"
|
tofile="${project.build.sourceDirectory}/com/threerings/getdown/data/Build.java"
|
||||||
overwrite="true">
|
overwrite="true">
|
||||||
<filterset>
|
<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="build_version" value="${project.version}"/>
|
||||||
|
<filter token="host_whitelist" value="${getdown.host.whitelist}"/>
|
||||||
</filterset>
|
</filterset>
|
||||||
</copy>
|
</copy>
|
||||||
</target>
|
</target>
|
||||||
|
|||||||
@@ -484,7 +484,7 @@ public class Application
|
|||||||
String suffix = _trackingURLSuffix == null ? "" : _trackingURLSuffix;
|
String suffix = _trackingURLSuffix == null ? "" : _trackingURLSuffix;
|
||||||
String ga = getGATrackingCode();
|
String ga = getGATrackingCode();
|
||||||
return _trackingURL == null ? null :
|
return _trackingURL == null ? null :
|
||||||
new URL(_trackingURL + encodePath(event + suffix + ga));
|
HostWhitelist.INSTANCE.verify(new URL(_trackingURL + encodePath(event + suffix + ga)));
|
||||||
} catch (MalformedURLException mue) {
|
} catch (MalformedURLException mue) {
|
||||||
log.warning("Invalid tracking URL", "path", _trackingURL, "event", event, "error", mue);
|
log.warning("Invalid tracking URL", "path", _trackingURL, "event", event, "error", mue);
|
||||||
return null;
|
return null;
|
||||||
@@ -586,7 +586,7 @@ public class Application
|
|||||||
|
|
||||||
// if we are a versioned deployment, create a versioned appbase
|
// if we are a versioned deployment, create a versioned appbase
|
||||||
try {
|
try {
|
||||||
_vappbase = (_version < 0) ? new URL(_appbase) : createVAppBase(_version);
|
_vappbase = createVAppBase(_version);
|
||||||
} catch (MalformedURLException mue) {
|
} catch (MalformedURLException mue) {
|
||||||
String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
|
String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
|
||||||
throw (IOException) new IOException(err).initCause(mue);
|
throw (IOException) new IOException(err).initCause(mue);
|
||||||
@@ -601,7 +601,7 @@ public class Application
|
|||||||
latest = SysProps.replaceDomain(latest);
|
latest = SysProps.replaceDomain(latest);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
_latest = new URL(latest);
|
_latest = HostWhitelist.INSTANCE.verify(new URL(latest));
|
||||||
} catch (MalformedURLException mue) {
|
} catch (MalformedURLException mue) {
|
||||||
log.warning("Invalid URL for latest attribute.", mue);
|
log.warning("Invalid URL for latest attribute.", mue);
|
||||||
}
|
}
|
||||||
@@ -1408,7 +1408,8 @@ public class Application
|
|||||||
protected URL createVAppBase (long version)
|
protected URL createVAppBase (long version)
|
||||||
throws MalformedURLException
|
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;
|
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.
|
* Contains static data provided during the build process.
|
||||||
*/
|
*/
|
||||||
@@ -19,4 +24,20 @@ public class Build {
|
|||||||
public static String version () {
|
public static String version () {
|
||||||
return "@build_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.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -17,8 +19,6 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.threerings.getdown.util.StringUtil;
|
|
||||||
|
|
||||||
import static com.threerings.getdown.Log.log;
|
import static com.threerings.getdown.Log.log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -320,11 +320,21 @@ public class Config
|
|||||||
public String getUrl (String name, String def)
|
public String getUrl (String name, String def)
|
||||||
{
|
{
|
||||||
String value = getString(name + "." + Locale.getDefault().getLanguage());
|
String value = getString(name + "." + Locale.getDefault().getLanguage());
|
||||||
if (!StringUtil.isBlank(value)) {
|
if (StringUtil.isBlank(value)) {
|
||||||
return value;
|
value = getString(name);
|
||||||
}
|
}
|
||||||
value = getString(name);
|
if (StringUtil.isBlank(value)) {
|
||||||
return StringUtil.isBlank(value) ? def : 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user