Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SHIRO-764] Add IpFilter for restricting access IP ranges #219

Merged
merged 1 commit into from May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions NOTICE
Expand Up @@ -9,7 +9,7 @@ on initial ideas from Dr. Heinz Kabutz's publicly posted version
available at http://www.javaspecialists.eu/archive/Issue015.html,
with continued modifications.

Certain parts (StringUtils etc.) of the source code for this
product was copied for simplicity and to reduce dependencies
from the source code developed by the Spring Framework Project
(http://www.springframework.org).
Certain parts (StringUtils, IpAddressMatcher, etc.) of the source
code for this product was copied for simplicity and to reduce
dependencies from the source code developed by the Spring Framework
Project (http://www.springframework.org).
Expand Up @@ -38,6 +38,7 @@
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter;
import org.apache.shiro.web.filter.authz.IpFilter;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.filter.authz.PortFilter;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
Expand Down Expand Up @@ -86,6 +87,8 @@ public abstract class ShiroWebModule extends ShiroModule {
@SuppressWarnings({"UnusedDeclaration"})
public static final Key<SslFilter> SSL = Key.get(SslFilter.class);
@SuppressWarnings({"UnusedDeclaration"})
public static final Key<IpFilter> IP = Key.get(IpFilter.class);
@SuppressWarnings({"UnusedDeclaration"})
public static final Key<UserFilter> USER = Key.get(UserFilter.class);


Expand Down
@@ -0,0 +1,97 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed 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.shiro.web.filter.authz;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;

/**
* Matches a request based on IP Address or subnet mask matching against the remote
* address.
* <p>
* Both IPv6 and IPv4 addresses are supported, but a matcher which is configured with an
* IPv4 address will never match a request which returns an IPv6 address, and vice-versa.
*
* @see <a href="https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/util/matcher/IpAddressMatcher.java">Original Spring Security version</a>
* @since 2.0
*/
public final class IpAddressMatcher {
private final int nMaskBits;
private final InetAddress requiredAddress;

/**
* Takes a specific IP address or a range specified using the IP/Netmask (e.g.
* 192.168.1.0/24 or 202.24.0.0/14).
*
* @param ipAddress the address or range of addresses from which the request must
* come.
*/
public IpAddressMatcher(String ipAddress) {
int i = ipAddress.indexOf('/');
if (i > 0) {
nMaskBits = Integer.parseInt(ipAddress.substring(i + 1));
ipAddress = ipAddress.substring(0, i);
} else {
nMaskBits = -1;
}
requiredAddress = parseAddress(ipAddress);
}

public boolean matches(String address) {
InetAddress remoteAddress = parseAddress(address);

if (!requiredAddress.getClass().equals(remoteAddress.getClass())) {
return false;
}

if (nMaskBits < 0) {
return remoteAddress.equals(requiredAddress);
}

byte[] remAddr = remoteAddress.getAddress();
byte[] reqAddr = requiredAddress.getAddress();

int oddBits = nMaskBits % 8;
int nMaskBytes = nMaskBits / 8 + (oddBits == 0 ? 0 : 1);
byte[] mask = new byte[nMaskBytes];

Arrays.fill(mask, 0, oddBits == 0 ? mask.length : mask.length - 1, (byte) 0xFF);

if (oddBits != 0) {
int finalByte = (1 << oddBits) - 1;
finalByte <<= 8 - oddBits;
mask[mask.length - 1] = (byte) finalByte;
}

for (int i = 0; i < mask.length; i++) {
if ((remAddr[i] & mask[i]) != (reqAddr[i] & mask[i])) {
return false;
}
}

return true;
}

private InetAddress parseAddress(String address) {
try {
return InetAddress.getByName(address);
}
catch (UnknownHostException e) {
throw new IllegalArgumentException("Failed to parse address" + address, e);
}
}
}
142 changes: 142 additions & 0 deletions web/src/main/java/org/apache/shiro/web/filter/authz/IpFilter.java
@@ -0,0 +1,142 @@
/*
* 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.shiro.web.filter.authz;

import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Collection;

/**
* A Filter that requires the request to be from within a specific set of IP
* address ranges and / or not from with a specific (denied) set.
* <p/>
* Example config:
* <pre>
* [main]
* localLan = org.apache.shiro.web.filter.authz.IpFilter
* localLan.authorizedIps = 192.168.10.0/24
* localLan.deniedIps = 192.168.10.10/32
* <p/>
* [urls]
* /some/path/** = localLan
* # override for just this path:
* /another/path/** = localLan
* </pre>
*
* @since 2.0
*/
public class IpFilter extends AuthorizationFilter {

private static IpSource DEFAULT_IP_SOURCE = new IpSource() {
public Collection<String> getAuthorizedIps() {
return Collections.emptySet();
}
public Collection<String> getDeniedIps() {
return Collections.emptySet();
}
};

private IpSource ipSource = DEFAULT_IP_SOURCE;

private List<IpAddressMatcher> authorizedIpMatchers = Collections.emptyList();
private List<IpAddressMatcher> deniedIpMatchers = Collections.emptyList();

/**
* Specifies a set of (comma, tab or space-separated) strings representing
* IP address representing IPv4 or IPv6 ranges / CIDRs from which access
* should be allowed (if the IP is not included in either the list of
* statically defined denied IPs or the dynamic list of IPs obtained from
* the IP source.
*/
public void setAuthorizedIps(String authorizedIps) {
String[] ips = StringUtils.tokenizeToStringArray(authorizedIps, ", \t");
if (ips != null && ips.length > 0) {
authorizedIpMatchers = new ArrayList<IpAddressMatcher>();
for (String ip : ips) {
authorizedIpMatchers.add(new IpAddressMatcher(ip));
}
}
}

/**
* Specified a set of (comma, tab or space-separated) strings representing
* IP address representing IPv4 or IPv6 ranges / CIDRs from which access
* should be blocked.
*/
public void setDeniedIps(String deniedIps) {
String[] ips = StringUtils.tokenizeToStringArray(deniedIps, ", \t");
if (ips != null && ips.length > 0) {
deniedIpMatchers = new ArrayList<IpAddressMatcher>();
for (String ip : ips) {
deniedIpMatchers.add(new IpAddressMatcher(ip));
}
}
}

public void setIpSource(IpSource source) {
this.ipSource = source;
}

/**
* Returns the remote host for a given HTTP request. By default uses the
* remote method ServletRequest.getRemoteAddr(). May be overriden by
* subclasses to obtain address information from specific headers (e.g. XFF
* or Forwarded) in situations with reverse proxies.
*/
public String getHostFromRequest(ServletRequest request) {
return request.getRemoteAddr();
}

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
String remoteIp = getHostFromRequest(request);
for (IpAddressMatcher matcher : deniedIpMatchers) {
if (matcher.matches(remoteIp)) {
return false;
}
}
for (String ip : ipSource.getDeniedIps()) {
IpAddressMatcher matcher = new IpAddressMatcher(ip);
if (matcher.matches(remoteIp)) {
return false;
}
}
for (IpAddressMatcher matcher : authorizedIpMatchers) {
if (matcher.matches(remoteIp)) {
return true;
}
}
for (String ip : ipSource.getAuthorizedIps()) {
IpAddressMatcher matcher = new IpAddressMatcher(ip);
if (matcher.matches(remoteIp)) {
return true;
}
}
return false;
}
}
43 changes: 43 additions & 0 deletions web/src/main/java/org/apache/shiro/web/filter/authz/IpSource.java
@@ -0,0 +1,43 @@
/*
* 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.shiro.web.filter.authz;

import java.util.Collection;

/**
* Represents a source of information for IP restrictions (see IpFilter)
* @since 2.0
*/
public interface IpSource {

/**
* Returns a set of strings representing IP address representing
* IPv4 or IPv6 ranges / CIDRs. e.g. 192.168.0.0/16 from which
* access should be allowed (if and only if the IP is not included
* in the list of denied IPs)
*/
public Collection<String> getAuthorizedIps();

/**
* Returns a set of strings representing IP address representing
* IPv4 or IPv6 ranges / CIDRs. e.g. 192.168.0.0/16 from which
* access should be denied.
*/
public Collection<String> getDeniedIps();
}
Expand Up @@ -41,6 +41,7 @@ public enum DefaultFilter {
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
authcBearer(BearerHttpAuthenticationFilter.class),
ip(IpFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
Expand Down
@@ -0,0 +1,77 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed 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.shiro.web.filter.authz;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

import org.junit.Test;

/**
* @since 2.0
*/
public class IpAddressMatcherTests {
final IpAddressMatcher v6matcher = new IpAddressMatcher("fe80::21f:5bff:fe33:bd68");
final IpAddressMatcher v4matcher = new IpAddressMatcher("192.168.1.104");
final String ipv6Address = "fe80::21f:5bff:fe33:bd68";
final String ipv4Address = "192.168.1.104";

@Test
public void ipv6MatcherMatchesIpv6Address() {
assertTrue(v6matcher.matches(ipv6Address));
}

@Test
public void ipv6MatcherDoesntMatchIpv4Address() {
assertFalse(v6matcher.matches(ipv4Address));
}

@Test
public void ipv4MatcherMatchesIpv4Address() {
assertTrue(v4matcher.matches(ipv4Address));
}

@Test
public void ipv4SubnetMatchesCorrectly() throws Exception {
IpAddressMatcher matcher = new IpAddressMatcher("192.168.1.0/24");
assertTrue(matcher.matches(ipv4Address));
matcher = new IpAddressMatcher("192.168.1.128/25");
assertFalse(matcher.matches(ipv4Address));
assertTrue(matcher.matches("192.168.1.159"));
}

@Test
public void ipv6RangeMatches() throws Exception {
IpAddressMatcher matcher = new IpAddressMatcher("2001:DB8::/48");
assertTrue(matcher.matches("2001:DB8:0:0:0:0:0:0"));
assertTrue(matcher.matches("2001:DB8:0:0:0:0:0:1"));
assertTrue(matcher.matches("2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF"));
assertFalse(matcher.matches("2001:DB8:1:0:0:0:0:0"));
}

// https://github.com/spring-projects/spring-security/issues/1970q
@Test
public void zeroMaskMatchesAnything() throws Exception {
IpAddressMatcher matcher = new IpAddressMatcher("0.0.0.0/0");

assertTrue(matcher.matches("123.4.5.6"));
assertTrue(matcher.matches("192.168.0.159"));

matcher = new IpAddressMatcher("192.168.0.159/0");
assertTrue(matcher.matches("123.4.5.6"));
assertTrue(matcher.matches("192.168.0.159"));
}
}