From c7a3e271dbeb588f8557d7925fb75aee0e3c1240 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 11 Oct 2017 16:26:37 +0000 Subject: [PATCH] Console: Validate host header (thx Kevin Froman) --- .../net/i2p/router/web/HostCheckHandler.java | 127 ++++++++++++++++++ .../i2p/router/web/RouterConsoleRunner.java | 49 +++++-- history.txt | 5 + .../src/net/i2p/router/RouterVersion.java | 2 +- 4 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java diff --git a/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java new file mode 100644 index 000000000..0343fe645 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/web/HostCheckHandler.java @@ -0,0 +1,127 @@ +package net.i2p.router.web; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +import org.apache.http.conn.util.InetAddressUtils; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.HandlerWrapper; + +/** + * Block certain Host headers to prevent DNS rebinding attacks. + * + * This Handler wraps the ContextHandlerCollection, which handles + * all the webapps (not just routerconsole). + * Therefore, this protects all the webapps. + * + * @since 0.9.32 + */ +public class HostCheckHandler extends HandlerWrapper +{ + private final I2PAppContext _context; + private final Set _listenHosts; + + /** + * MUST call setListenHosts() afterwards. + */ + public HostCheckHandler(I2PAppContext ctx) { + super(); + _context = ctx; + _listenHosts = new HashSet(8); + } + + /** + * Set the legal hosts. + * Not synched. Call this BEFORE starting. + * If empty, all are allowed. + * + * @param hosts contains hostnames or IPs. But we allow all IPs anyway. + */ + public void setListenHosts(Set hosts) { + _listenHosts.clear(); + _listenHosts.addAll(hosts); + } + + /** + * Block by Host header, pass everything else to the delegate. + */ + public void handle(String pathInContext, + Request baseRequest, + HttpServletRequest httpRequest, + HttpServletResponse httpResponse) + throws IOException, ServletException + { + + String host = httpRequest.getHeader("Host"); + if (!allowHost(host)) { + Log log = _context.logManager().getLog(HostCheckHandler.class); + host = DataHelper.stripHTML(getHost(host)); + String s = "Console request denied.\n" + + " To allow access using the hostname \"" + host + "\", add the line \"" + + RouterConsoleRunner.PROP_ALLOWED_HOSTS + '=' + host + + "\" to advanced configuration and restart."; + log.logAlways(Log.WARN, s); + httpResponse.sendError(403, s); + return; + } + + super.handle(pathInContext, baseRequest, httpRequest, httpResponse); + } + + /** + * Should we allow a request with this Host header? + * + * ref: https://en.wikipedia.org/wiki/DNS_rebinding + * + * @param host the HTTP Host header, null ok + * @return true if OK + */ + private boolean allowHost(String host) { + if (host == null) + return true; + // common cases + if (host.equals("127.0.0.1:7657") || + host.equals("localhost:7657")) + return true; + // all allowed? + if (_listenHosts.isEmpty()) + return true; + host = getHost(host); + if (_listenHosts.contains(host)) + return true; + // allow all IP addresses + if (InetAddressUtils.isIPv4Address(host) || InetAddressUtils.isIPv6Address(host)) + return true; + //System.out.println(host + " not found in " + s); + return false; + } + + /** + * Strip [] and port from a host header + * + * @param host the HTTP Host header non-null + */ + private static String getHost(String host) { + if (host.startsWith("[")) { + host = host.substring(1); + int brack = host.indexOf(']'); + if (brack >= 0) + host = host.substring(0, brack); + } else { + int colon = host.indexOf(':'); + if (colon >= 0) + host = host.substring(0, colon); + } + return host; + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java index e77d150f7..f953d1d48 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; @@ -140,7 +141,8 @@ public class RouterConsoleRunner implements RouterApp { private static final int MAX_IDLE_TIME = 90*1000; private static final String THREAD_NAME = "RouterConsole Jetty"; public static final String PROP_DTG_ENABLED = "desktopgui.enabled"; - + static final String PROP_ALLOWED_HOSTS = "routerconsole.allowedHosts"; + /** *
      *  non-SSL:
@@ -360,14 +362,15 @@ public class RouterConsoleRunner implements RouterApp {
      *
      *	Server
      *		HandlerCollection
-     *			ContextHandlerCollection
-     *				WebAppContext (i.e. ContextHandler)
-     *					SessionHandler
-     *					SecurityHandler
-     *					ServletHandler
-     *						servlets...
-     *				WebAppContext
-     *				...
+     *			HostCheckHandler
+     *				ContextHandlerCollection
+     *					WebAppContext (i.e. ContextHandler)
+     *						SessionHandler
+     *						SecurityHandler
+     *						ServletHandler
+     *							servlets...
+     *					WebAppContext
+     *					...
      *			DefaultHandler
      *			RequestLogHandler (opt)
      *
@@ -442,10 +445,12 @@ public class RouterConsoleRunner implements RouterApp { HandlerCollection hColl = new HandlerCollection(); ContextHandlerCollection chColl = new ContextHandlerCollection(); + HostCheckHandler chCollWrapper = new HostCheckHandler(_context); + chCollWrapper.setHandler(chColl); // gone in Jetty 7 //_server.addHandler(hColl); _server.setHandler(hColl); - hColl.addHandler(chColl); + hColl.addHandler(chCollWrapper); hColl.addHandler(new DefaultHandler()); String log = _context.getProperty("routerconsole.log"); @@ -480,6 +485,7 @@ public class RouterConsoleRunner implements RouterApp { if (!_webAppsDir.endsWith("/")) _webAppsDir += '/'; + Set listenHosts = new HashSet(8); HandlerWrapper rootWebApp = null; ServletHandler rootServletHandler = null; List connectors = new ArrayList(4); @@ -549,6 +555,7 @@ public class RouterConsoleRunner implements RouterApp { Collections.sort(hosts, new HostComparator()); _context.portMapper().register(PortMapper.SVC_CONSOLE, hosts.get(0), lport); // note that we could still fail in connector.start() below + listenHosts.addAll(hosts); } } @@ -627,6 +634,7 @@ public class RouterConsoleRunner implements RouterApp { Collections.sort(hosts, new HostComparator()); _context.portMapper().register(PortMapper.SVC_HTTPS_CONSOLE, hosts.get(0), sslPort); // note that we could still fail in connector.start() below + listenHosts.addAll(hosts); } } else { System.err.println("Unable to create or access keystore for SSL: " + keyStore.getAbsolutePath()); @@ -670,6 +678,27 @@ public class RouterConsoleRunner implements RouterApp { ioe.printStackTrace(); } + // fix up the allowed hosts set (see HostCheckHandler) + if (listenHosts.contains("0.0.0.0") || + listenHosts.contains("::") || + listenHosts.contains("0:0:0:0:0:0:0:0")) { + // empty set says all are valid + listenHosts.clear(); + } else { + listenHosts.add("localhost"); + listenHosts.add("127.0.0.1"); + listenHosts.add("::1"); + listenHosts.add("0:0:0:0:0:0:0:1"); + String allowed = _context.getProperty(PROP_ALLOWED_HOSTS); + if (allowed != null) { + StringTokenizer tok = new StringTokenizer(allowed, " ,"); + while (tok.hasMoreTokens()) { + listenHosts.add(tok.nextToken()); + } + } + } + chCollWrapper.setListenHosts(listenHosts); + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=364936 // WARN:oejw.WebAppContext:Failed startup of context o.e.j.w.WebAppContext{/,jar:file:/.../webapps/routerconsole.war!/},/.../webapps/routerconsole.war // java.lang.IllegalStateException: zip file closed diff --git a/history.txt b/history.txt index fc1e53d2a..55f994105 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,8 @@ +2017-10-11 zzz + * Console: Validate host header (thx Kevin Froman) + * Router: Honor IPv6 setting when converting configured hostnames + to IP addresses (proposal #141) + 2017-10-04 zzz * Router: Convert configured hostnames to IP addresses before publishing (proposal #141) diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index f6e6df3a5..4e28a2c8d 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 4; + public final static long BUILD = 5; /** for example "-test" */ public final static String EXTRA = "";