Author: markt Date: Thu May 24 19:41:06 2018 New Revision: 1832193 URL: http://svn.apache.org/viewvc?rev=1832193&view=rev Log: Add the RemoteCIDRFilter and RemoteCIDRValve that can be used to allow/deny requests based on IPv4 and/or IPv6 client address where the IP ranges are defined using CIDR notation. Based on a patch by Francis Galiegue.
Added: tomcat/trunk/java/org/apache/catalina/filters/RemoteCIDRFilter.java (with props) tomcat/trunk/java/org/apache/catalina/valves/RemoteCIDRValve.java (with props) Modified: tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties tomcat/trunk/java/org/apache/catalina/valves/LocalStrings.properties tomcat/trunk/webapps/docs/changelog.xml tomcat/trunk/webapps/docs/config/filter.xml tomcat/trunk/webapps/docs/config/valve.xml Modified: tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties?rev=1832193&r1=1832192&r2=1832193&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties Thu May 24 19:41:06 2018 @@ -49,6 +49,9 @@ expiresFilter.invalidDurationUnit=Invali httpHeaderSecurityFilter.committed=Unable to add HTTP headers since response is already committed on entry to the HTTP header security Filter httpHeaderSecurityFilter.clickjack.invalid=An invalid value [{0}] was specified for the anti click-jacking header +remoteCidrFilter.invalid=Invalid configuration provided for [{0}]. See previous messages for details. +remoteCidrFilter.noRemoteIp=Client does not have an IP address. Request denied. + requestFilter.deny=Denied request for [{0}] based on property [{1}] restCsrfPreventionFilter.invalidNonce=CSRF nonce validation failed \ No newline at end of file Added: tomcat/trunk/java/org/apache/catalina/filters/RemoteCIDRFilter.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/filters/RemoteCIDRFilter.java?rev=1832193&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/catalina/filters/RemoteCIDRFilter.java (added) +++ tomcat/trunk/java/org/apache/catalina/filters/RemoteCIDRFilter.java Thu May 24 19:41:06 2018 @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.filters; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import org.apache.catalina.util.NetMask; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public final class RemoteCIDRFilter extends FilterBase { + + /** + * text/plain MIME type: this is the MIME type we return when a + * {@link ServletResponse} is not an {@link HttpServletResponse} + */ + private static final String PLAIN_TEXT_MIME_TYPE = "text/plain"; + + /** + * Our logger + */ + private final Log log = LogFactory.getLog(RemoteCIDRFilter.class); + + /** + * The list of allowed {@link NetMask}s + */ + private final List<NetMask> allow = new ArrayList<>(); + + /** + * The list of denied {@link NetMask}s + */ + private final List<NetMask> deny = new ArrayList<>(); + + + /** + * Return a string representation of the {@link NetMask} list in #allow. + * + * @return the #allow list as a string, without the leading '[' and trailing + * ']' + */ + public String getAllow() { + return allow.toString().replace("[", "").replace("]", ""); + } + + + /** + * Fill the #allow list with the list of netmasks provided as an argument, + * if any. Calls #fillFromInput. + * + * @param input The list of netmasks, as a comma separated string + * @throws IllegalArgumentException One or more netmasks are invalid + */ + public void setAllow(final String input) { + final List<String> messages = fillFromInput(input, allow); + + if (messages.isEmpty()) { + return; + } + + for (final String message : messages) { + log.error(message); + } + + throw new IllegalArgumentException(sm.getString("remoteCidrFilter.invalid", "allow")); + } + + + /** + * Return a string representation of the {@link NetMask} list in #deny. + * + * @return the #deny list as string, without the leading '[' and trailing + * ']' + */ + public String getDeny() { + return deny.toString().replace("[", "").replace("]", ""); + } + + + /** + * Fill the #deny list with the list of netmasks provided as an argument, if + * any. Calls #fillFromInput. + * + * @param input The list of netmasks, as a comma separated string + * @throws IllegalArgumentException One or more netmasks are invalid + */ + public void setDeny(final String input) { + final List<String> messages = fillFromInput(input, deny); + + if (messages.isEmpty()) + return; + + for (final String message : messages) { + log.error(message); + } + + throw new IllegalArgumentException(sm.getString("remoteCidrFilter.invalid", "deny")); + } + + + + @Override + protected boolean isConfigProblemFatal() { + // Failure to configure a security related component should always be + // fatal. + return true; + } + + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + + if (isAllowed(request.getRemoteAddr())) { + chain.doFilter(request, response); + return; + } + + if (!(response instanceof HttpServletResponse)) { + sendErrorWhenNotHttp(response); + return; + } + + ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN); + } + + + @Override + public Log getLogger() { + return log; + } + + + /** + * Test if a remote's IP address is allowed to proceed. + * + * @param property The remote's IP address, as a string + * @return true if allowed + */ + private boolean isAllowed(final String property) { + final InetAddress addr; + + try { + addr = InetAddress.getByName(property); + } catch (UnknownHostException e) { + // This should be in the 'could never happen' category but handle it + // to be safe. + log.error(sm.getString("remoteCidrFilter.noRemoteIp"), e); + return false; + } + + for (final NetMask nm : deny) { + if (nm.matches(addr)) { + return false; + } + } + + for (final NetMask nm : allow) { + if (nm.matches(addr)) { + return true; + } + } + + // Allow if deny is specified but allow isn't + if (!deny.isEmpty() && allow.isEmpty()) { + return true; + } + + // Deny this request + return false; + } + + + private void sendErrorWhenNotHttp(ServletResponse response) throws IOException { + final PrintWriter writer = response.getWriter(); + response.setContentType(PLAIN_TEXT_MIME_TYPE); + writer.write(sm.getString("http.403")); + writer.flush(); + } + + + /** + * Fill a {@link NetMask} list from a string input containing a + * comma-separated list of (hopefully valid) {@link NetMask}s. + * + * @param input The input string + * @param target The list to fill + * @return a string list of processing errors (empty when no errors) + */ + private List<String> fillFromInput(final String input, final List<NetMask> target) { + target.clear(); + if (input == null || input.isEmpty()) { + return Collections.emptyList(); + } + + final List<String> messages = new LinkedList<>(); + NetMask nm; + + for (final String s : input.split("\\s*,\\s*")) { + try { + nm = new NetMask(s); + target.add(nm); + } catch (IllegalArgumentException e) { + messages.add(s + ": " + e.getMessage()); + } + } + + return Collections.unmodifiableList(messages); + } +} Propchange: tomcat/trunk/java/org/apache/catalina/filters/RemoteCIDRFilter.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: tomcat/trunk/java/org/apache/catalina/valves/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/valves/LocalStrings.properties?rev=1832193&r1=1832192&r2=1832193&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/valves/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/catalina/valves/LocalStrings.properties Thu May 24 19:41:06 2018 @@ -47,6 +47,9 @@ errorReportValve.noDescription=No descri errorReportValve.errorPageIOException=Unable to display error page at [{0}] due to an exception errorReportValve.errorPageNotFound=Unable to find a static error page at [{0}] +remoteCidrValve.invalid=Invalid configuration provided for [{0}]. See previous messages for details. +remoteCidrValve.noRemoteIp=Client does not have an IP address. Request denied. + # Remote IP valve remoteIpValve.invalidPortHeader=Invalid value [{0}] found for port in HTTP header [{1}] Added: tomcat/trunk/java/org/apache/catalina/valves/RemoteCIDRValve.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/valves/RemoteCIDRValve.java?rev=1832193&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/catalina/valves/RemoteCIDRValve.java (added) +++ tomcat/trunk/java/org/apache/catalina/valves/RemoteCIDRValve.java Thu May 24 19:41:06 2018 @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.valves; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.util.NetMask; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +public final class RemoteCIDRValve extends ValveBase { + + /** + * Our logger + */ + private static final Log log = LogFactory.getLog(RemoteCIDRValve.class); + + /** + * The list of allowed {@link NetMask}s + */ + private final List<NetMask> allow = new ArrayList<>(); + + /** + * The list of denied {@link NetMask}s + */ + private final List<NetMask> deny = new ArrayList<>(); + + + public RemoteCIDRValve() { + super(true); + } + + + /** + * Return a string representation of the {@link NetMask} list in #allow. + * + * @return the #allow list as a string, without the leading '[' and trailing + * ']' + */ + public String getAllow() { + return allow.toString().replace("[", "").replace("]", ""); + } + + + /** + * Fill the #allow list with the list of netmasks provided as an argument, + * if any. Calls #fillFromInput. + * + * @param input The list of netmasks, as a comma separated string + * @throws IllegalArgumentException One or more netmasks are invalid + */ + public void setAllow(final String input) { + final List<String> messages = fillFromInput(input, allow); + + if (messages.isEmpty()) { + return; + } + + for (final String message : messages) { + log.error(message); + } + + throw new IllegalArgumentException(sm.getString("remoteCidrValve.invalid", "allow")); + } + + + /** + * Return a string representation of the {@link NetMask} list in #deny. + * + * @return the #deny list as a string, without the leading '[' and trailing + * ']' + */ + public String getDeny() { + return deny.toString().replace("[", "").replace("]", ""); + } + + + /** + * Fill the #deny list with the list of netmasks provided as an argument, if + * any. Calls #fillFromInput. + * + * @param input The list of netmasks, as a comma separated string + * @throws IllegalArgumentException One or more netmasks are invalid + */ + + public void setDeny(final String input) { + final List<String> messages = fillFromInput(input, deny); + + if (messages.isEmpty()) { + return; + } + + for (final String message : messages) { + log.error(message); + } + + throw new IllegalArgumentException(sm.getString("remoteCidrValve.invalid", "deny")); + } + + + @Override + public void invoke(final Request request, final Response response) throws IOException, ServletException { + + if (isAllowed(request.getRequest().getRemoteAddr())) { + getNext().invoke(request, response); + } else { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } + } + + private boolean isAllowed(final String property) { + final InetAddress addr; + + try { + addr = InetAddress.getByName(property); + } catch (UnknownHostException e) { + // This should be in the 'could never happen' category but handle it + // to be safe. + log.error(sm.getString("remoteCidrValve.noRemoteIp"), e); + return false; + } + + for (final NetMask nm : deny) { + if (nm.matches(addr)) { + return false; + } + } + + for (final NetMask nm : allow) { + if (nm.matches(addr)) { + return true; + } + } + + // Allow if deny is specified but allow isn't + if (!deny.isEmpty() && allow.isEmpty()) { + return true; + } + + // Deny this request + return false; + } + + + /** + * Fill a {@link NetMask} list from a string input containing a + * comma-separated list of (hopefully valid) {@link NetMask}s. + * + * @param input The input string + * @param target The list to fill + * @return a string list of processing errors (empty when no errors) + */ + + private List<String> fillFromInput(final String input, final List<NetMask> target) { + target.clear(); + if (input == null || input.isEmpty()) { + return Collections.emptyList(); + } + + final List<String> messages = new LinkedList<>(); + NetMask nm; + + for (final String s : input.split("\\s*,\\s*")) { + try { + nm = new NetMask(s); + target.add(nm); + } catch (IllegalArgumentException e) { + messages.add(s + ": " + e.getMessage()); + } + } + + return Collections.unmodifiableList(messages); + } +} Propchange: tomcat/trunk/java/org/apache/catalina/valves/RemoteCIDRValve.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1832193&r1=1832192&r2=1832193&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Thu May 24 19:41:06 2018 @@ -97,6 +97,12 @@ <code>AccessLogValve</code> that causes IPv6 addresses to be output in canonical form defined by RFC 5952. (ognjen/markt) </add> + <add> + <bug>51953</bug>: Add the <code>RemoteCIDRFilter</code> and + <code>RemoteCIDRValve</code> that can be used to allow/deny requests + based on IPv4 and/or IPv6 client address where the IP ranges are defined + using CIDR notation. Based on a patch by Francis Galiegue. (markt) + </add> <fix> <bug>62343</bug>: Make CORS filter defaults more secure. This is the fix for CVE-2018-8014. (markt) Modified: tomcat/trunk/webapps/docs/config/filter.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/filter.xml?rev=1832193&r1=1832192&r2=1832193&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/config/filter.xml (original) +++ tomcat/trunk/webapps/docs/config/filter.xml Thu May 24 19:41:06 2018 @@ -1100,6 +1100,108 @@ FINE: Request "/docs/config/manager.html </section> +<section name="Remote CIDR Filter"> + + <subsection name="Introduction"> + + <p>The <strong>Remote CIDR Filter</strong> allows you to compare the + IP address of the client that submitted this request against one or more + netmasks following the CIDR notation, and either allow the request to + continue or refuse to process the request from this client. IPv4 and + IPv6 are both fully supported. + </p> + + <p>This filter mimicks Apache httpd's <code>Order</code>, + <code>Allow from</code> and <code>Deny from</code> directives, + with the following limitations: + </p> + + <ul> + <li><code>Order</code> will always be <code>allow, deny</code>;</li> + <li>dotted quad notations for netmasks are not supported (that is, you + cannot write <code>192.168.1.0/255.255.255.0</code>, you must write + <code>192.168.1.0/24</code>; + </li> + <li>shortcuts, like <code>10.10.</code>, which is equivalent to + <code>10.10.0.0/16</code>, are not supported; + </li> + <li>as the filter name says, this is a CIDR only filter, + therefore subdomain notations like <code>.mydomain.com</code> are not + supported either. + </li> + </ul> + + <p>Some more features of this filter are: + </p> + + <ul> + <li>if you omit the CIDR prefix, this filter becomes a single IP + filter;</li> + <li>unlike the <a href="#Remote_Host_Filter">Remote Host Filter</a>, + it can handle IPv6 addresses in condensed form (<code>::1</code>, + <code>fe80::/71</code>, etc).</li> + </ul> + + </subsection> + + <subsection name="Filter Class Name"> + + <p>The filter class name for the Remote Address Filter is + <strong><code>org.apache.catalina.filters.RemoteCIDRFilter</code> + </strong>.</p> + + </subsection> + + <subsection name="Initialisation parameters"> + + <p>The <strong>Remote CIDR Filter</strong> supports the following + initialisation parameters:</p> + + <attributes> + + <attribute name="allow" required="false"> + <p>A comma-separated list of IPv4 or IPv6 netmasks or addresses + that the remote client's IP address is matched against. + If this attribute is specified, the remote address MUST match + for this request to be accepted. If this attribute is not specified, + all requests will be accepted UNLESS the remote IP is matched by a + netmask in the <code>deny</code> attribute. + </p> + </attribute> + + <attribute name="deny" required="false"> + <p>A comma-separated list of IPv4 or IPv6 netmasks or addresses + that the remote client's IP address is matched against. + If this attribute is specified, the remote address MUST NOT match + for this request to be accepted. If this attribute is not specified, + request acceptance is governed solely by the <code>accept</code> + attribute. + </p> + </attribute> + + </attributes> + + </subsection> + + <subsection name="Example"> + <p>To allow access only for the clients connecting from localhost:</p> + <pre> + <filter> + <filter-name>Remote CIDR Filter</filter-name> + <filter-class>org.apache.catalina.filters.RemoteCIDRFilter</filter-class> + <init-param> + <param-name>allow</param-name> + <param-value>127.0.0.0/8, ::1</param-value> + </init-param> + </filter> + <filter-mapping> + <filter-name>Remote CIDR Filter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + </pre> + </subsection> + +</section> <section name="Remote IP Filter"> Modified: tomcat/trunk/webapps/docs/config/valve.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/valve.xml?rev=1832193&r1=1832192&r2=1832193&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/config/valve.xml (original) +++ tomcat/trunk/webapps/docs/config/valve.xml Thu May 24 19:41:06 2018 @@ -647,7 +647,7 @@ header. This is useful in combination with the context attribute <code>preemptiveAuthentication="true"</code>.</p> - <p><strong>Note:</strong> This filter processes the value returned by + <p><strong>Note:</strong> This valve processes the value returned by method <code>ServletRequest.getRemoteHost()</code>. To allow the method to return proper host names, you have to enable "DNS lookups" feature on a <strong>Connector</strong>.</p> @@ -718,6 +718,100 @@ </subsection> +<subsection name="Remote CIDR Valve"> + + <subsection name="Introduction"> + + <p>The <strong>Remote CIDR Valve</strong> allows you to compare the + IP address of the client that submitted this request against one or more + netmasks following the CIDR notation, and either allow the request to + continue or refuse to process the request from this client. IPv4 and + IPv6 are both fully supported. A Remote CIDR Valve can be associated + with any Catalina container (<a href="engine.html">Engine</a>, + <a href="host.html">Host</a>, or <a href="context.html">Context</a>), and + must accept any request presented to this container for processing before + it will be passed on. + </p> + + <p>This valve mimicks Apache's <code>Order</code>, + <code>Allow from</code> and <code>Deny from</code> directives, + with the following limitations: + </p> + + <ul> + <li><code>Order</code> will always be <code>allow, deny</code>;</li> + <li>dotted quad notations for netmasks are not supported (that is, you + cannot write <code>192.168.1.0/255.255.255.0</code>, you must write + <code>192.168.1.0/24</code>; + </li> + <li>shortcuts, like <code>10.10.</code>, which is equivalent to + <code>10.10.0.0/16</code>, are not supported; + </li> + <li>as the valve name says, this is a CIDR only valve, + therefore subdomain notations like <code>.mydomain.com</code> are not + supported either. + </li> + </ul> + + <p>Some more features of this valve are: + </p> + + <ul> + <li>if you omit the CIDR prefix, this valve becomes a single IP + valve;</li> + <li>unlike the <a href="#Remote_Host_Valve">Remote Host Valve</a>, + it can handle IPv6 addresses in condensed form (<code>::1</code>, + <code>fe80::/71</code>, etc).</li> + </ul> + + </subsection> + + <subsection name="Attributes"> + + <p>The <strong>Remote CIDR Valve</strong> supports the following + configuration attributes:</p> + + <attributes> + + <attribute name="className" required="true"> + <p>Java class name of the implementation to use. This MUST be set to + <strong>org.apache.catalina.valves.RemoteCIDRValve</strong>.</p> + </attribute> + + <attribute name="allow" required="false"> + <p>A comma-separated list of IPv4 or IPv6 netmasks or addresses + that the remote client's IP address is matched against. + If this attribute is specified, the remote address MUST match + for this request to be accepted. If this attribute is not specified, + all requests will be accepted UNLESS the remote IP is matched by a + netmask in the <code>deny</code> attribute. + </p> + </attribute> + + <attribute name="deny" required="false"> + <p>A comma-separated list of IPv4 or IPv6 netmasks or addresses + that the remote client's IP address is matched against. + If this attribute is specified, the remote address MUST NOT match + for this request to be accepted. If this attribute is not specified, + request acceptance is governed solely by the <code>accept</code> + attribute. + </p> + </attribute> + + </attributes> + + </subsection> + + <subsection name="Example"> + <p>To allow access only for the clients connecting from localhost:</p> + <pre> + <Valve className="org.apache.catalina.valves.RemoteCIDRValve" + allow="127.0.0.1, ::1"/> + </pre> + </subsection> + +</subsection> + </section> @@ -1014,7 +1108,6 @@ </section> - <section name="Single Sign On Valve"> <subsection name="Introduction"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org