Browse Source

Add InetAddressMatcher

Co-authored-by: Gábor Vaspöri <gabor.vaspori@gmail.com>
Co-authored-by: Kian Jamali <kianjamali123@gmail.com>
Co-authored-by: Rossen Stoyanchev <rstoyanchev@users.noreply.github.com>
pull/18593/head
Robert Winch 2 months ago committed by Rob Winch
parent
commit
cc6a005aa5
  1. 2
      docs/modules/ROOT/pages/whats-new.adoc
  2. 9
      web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java
  3. 49
      web/src/main/java/org/springframework/security/web/util/matcher/InetAddressMatcher.java
  4. 379
      web/src/main/java/org/springframework/security/web/util/matcher/InetAddressMatchers.java
  5. 76
      web/src/main/java/org/springframework/security/web/util/matcher/InetAddressParser.java
  6. 98
      web/src/main/java/org/springframework/security/web/util/matcher/IpAddressMatcher.java
  7. 117
      web/src/main/java/org/springframework/security/web/util/matcher/IpInetAddressMatcher.java
  8. 69
      web/src/test/java/org/springframework/security/web/util/matcher/InetAddressMatcherTests.java
  9. 499
      web/src/test/java/org/springframework/security/web/util/matcher/InetAddressMatchersTests.java
  10. 6
      web/src/test/java/org/springframework/security/web/util/matcher/IpAddressMatcherTests.java
  11. 138
      web/src/test/java/org/springframework/security/web/util/matcher/IpInetAddressMatcherTests.java

2
docs/modules/ROOT/pages/whats-new.adoc

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
[[new]]
= What's New in Spring Security 7.1
This is a placeholder for updates to Spring Security 7.1
* https://github.com/spring-projects/spring-security/pull/18634[gh-18634] - Added javadoc:org.springframework.security.web.util.matcher.InetAddressMatcher[]

9
web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java

@ -16,9 +16,12 @@ @@ -16,9 +16,12 @@
package org.springframework.security.web.server.util.matcher;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.security.web.util.matcher.IpAddressMatcher;
import org.springframework.security.web.util.matcher.InetAddressMatcher;
import org.springframework.security.web.util.matcher.InetAddressMatchers;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
@ -31,7 +34,7 @@ import org.springframework.web.server.ServerWebExchange; @@ -31,7 +34,7 @@ import org.springframework.web.server.ServerWebExchange;
*/
public final class IpAddressServerWebExchangeMatcher implements ServerWebExchangeMatcher {
private final IpAddressMatcher ipAddressMatcher;
private final InetAddressMatcher ipAddressMatcher;
/**
* Takes a specific IP address or a range specified using the IP/Netmask (e.g.
@ -41,7 +44,7 @@ public final class IpAddressServerWebExchangeMatcher implements ServerWebExchang @@ -41,7 +44,7 @@ public final class IpAddressServerWebExchangeMatcher implements ServerWebExchang
*/
public IpAddressServerWebExchangeMatcher(String ipAddress) {
Assert.hasText(ipAddress, "IP address cannot be empty");
this.ipAddressMatcher = new IpAddressMatcher(ipAddress);
this.ipAddressMatcher = InetAddressMatchers.builder().includeAddresses(List.of(ipAddress)).build();
}
@Override

49
web/src/main/java/org/springframework/security/web/util/matcher/InetAddressMatcher.java

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
/*
* Copyright 2004-present 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
*
* https://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.springframework.security.web.util.matcher;
import java.net.InetAddress;
import org.jspecify.annotations.Nullable;
/**
* Matches an {@link InetAddress}.
*
* @author Rossen Stoyanchev
* @author Rob Winch
* @since 7.1
*/
@FunctionalInterface
public interface InetAddressMatcher {
/**
* Whether the given address matches.
* @param address the {@link InetAddress} to check (may be {@code null})
* @return {@code true} if the address matches, {@code false} otherwise
*/
boolean matches(@Nullable InetAddress address);
/**
* Whether the given address string matches.
* @param address the IP address string to check (may be {@code null})
* @return {@code true} if the address matches, {@code false} otherwise
*/
default boolean matches(@Nullable String address) {
return (address != null) ? matches(InetAddressParser.parseAddress(address)) : false;
}
}

379
web/src/main/java/org/springframework/security/web/util/matcher/InetAddressMatchers.java

@ -0,0 +1,379 @@ @@ -0,0 +1,379 @@
/*
* Copyright 2004-present 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
*
* https://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.springframework.security.web.util.matcher;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* Factory for creating {@link InetAddressMatcher} instances with various matching
* strategies for IP addresses.
*
* @author Rob Winch
* @since 7.1
*/
public final class InetAddressMatchers {
private InetAddressMatchers() {
}
/**
* Creates a new builder for configuring an {@link InetAddressMatcher}.
* @return a new {@link Builder} instance
*/
public static Builder builder() {
return new Builder();
}
/**
* Creates a new builder configured to match external (non-private) IP addresses.
* @return a {@link Builder} configured to match external addresses
*/
public static Builder matchExternal() {
return builder().matchAll(ExternalInetAddressMatcher.getInstance());
}
/**
* Creates a new builder configured to match internal (private) IP addresses.
* <p>
* Internal addresses include loopback addresses (127.0.0.0/8 for IPv4, ::1 for IPv6),
* private IPv4 address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and IPv6
* Unique Local Addresses (fc00::/7).
* @return a {@link Builder} configured to match internal addresses
*/
public static Builder matchInternal() {
return builder().matchAll(InternalInetAddressMatcher.getInstance());
}
/**
* A builder for constructing {@link InetAddressMatcher} instances with various
* matching rules.
*
* @author Kian Jamali
* @author Gábor Vaspöri
* @author Rossen Stoyanchev
* @author Rob Winch
*/
public static final class Builder {
private final List<InetAddressMatcher> matchers = new ArrayList<>();
private boolean reportOnly;
/**
* Adds an include list matcher that permits only the specified addresses.
* @param addresses the list of IP address patterns to include (cannot be null or
* empty)
* @return this builder for method chaining
* @throws IllegalArgumentException if addresses is null or empty
*/
public Builder includeAddresses(List<String> addresses) {
Assert.notEmpty(addresses, "addresses cannot be empty");
List<InetAddressMatcher> matchers = addresses.stream()
.<InetAddressMatcher>map(IpInetAddressMatcher::new)
.toList();
this.matchers.add(new IncludeListInetAddressMatcher(matchers));
return this;
}
/**
* Adds an exclude list matcher that blocks the specified addresses.
* @param addresses the list of IP address patterns to exclude (cannot be null or
* empty)
* @return this builder for method chaining
* @throws IllegalArgumentException if addresses is null or empty
*/
public Builder excludeAddresses(List<String> addresses) {
Assert.notEmpty(addresses, "addresses cannot be empty");
List<InetAddressMatcher> matchers = addresses.stream()
.<InetAddressMatcher>map(IpInetAddressMatcher::new)
.toList();
this.matchers.add(new ExcludeListInetAddressMatcher(matchers));
return this;
}
/**
* Adds custom matchers to the matcher chain. All matchers must match for an
* address to be permitted.
* @param matchers the custom {@link InetAddressMatcher} instances to add (cannot
* be null or empty)
* @return this builder for method chaining
* @throws IllegalArgumentException if matchers is null or empty
*/
public Builder matchAll(InetAddressMatcher... matchers) {
Assert.notEmpty(matchers, "matchers cannot be empty");
for (InetAddressMatcher matcher : matchers) {
this.matchers.add(matcher);
}
return this;
}
/**
* Configures the matcher to operate in report-only mode. In this mode, matching
* logic is evaluated and logged, but all addresses are allowed regardless of
* match results.
* @return this builder for method chaining
*/
public Builder reportOnly() {
this.reportOnly = true;
return this;
}
/**
* Builds the configured {@link InetAddressMatcher}.
* @return the constructed {@link InetAddressMatcher}
*/
public InetAddressMatcher build() {
return new CompositeInetAddressMatcher(this.matchers, this.reportOnly);
}
}
/**
* An {@link InetAddressMatcher} that matches addresses against an include list. Only
* addresses that match an entry in the include list are permitted.
*
* @author Rossen Stoyanchev
* @author Rob Winch
*/
static final class IncludeListInetAddressMatcher implements InetAddressMatcher {
private final List<InetAddressMatcher> includeList;
IncludeListInetAddressMatcher(List<InetAddressMatcher> includeList) {
Assert.notEmpty(includeList, "includeList cannot be null or empty");
this.includeList = new ArrayList<>(includeList);
}
@Override
public boolean matches(@Nullable InetAddress address) {
for (InetAddressMatcher matcher : this.includeList) {
if (matcher.matches(address)) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "IncludeListInetAddressMatcher[\"" + this.includeList + "\"]";
}
}
/**
* An {@link InetAddressMatcher} that matches addresses against an exclude list.
* Addresses that match an entry in the exclude list are rejected.
*
* @author Rossen Stoyanchev
* @author Rob Winch
*/
static final class ExcludeListInetAddressMatcher implements InetAddressMatcher {
private final List<InetAddressMatcher> excludeList;
ExcludeListInetAddressMatcher(List<InetAddressMatcher> excludeList) {
Assert.notEmpty(excludeList, "excludeList cannot be null or empty");
this.excludeList = new ArrayList<>(excludeList);
}
@Override
public boolean matches(@Nullable InetAddress address) {
for (InetAddressMatcher matcher : this.excludeList) {
if (matcher.matches(address)) {
return false;
}
}
return true;
}
@Override
public String toString() {
return "ExcludeListInetAddressMatcher[\"" + this.excludeList + "\"]";
}
}
/**
* An {@link InetAddressMatcher} that matches internal (private) addresses.
* <p>
* Internal addresses include loopback addresses (127.0.0.0/8 for IPv4, ::1 for IPv6),
* private IPv4 address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and IPv6
* Unique Local Addresses (fc00::/7).
*
* @author Gábor Vaspöri
* @author Kian Jamali
* @author Rossen Stoyanchev
* @author Rob Winch
*/
static final class InternalInetAddressMatcher implements InetAddressMatcher {
private static final InternalInetAddressMatcher INSTANCE = new InternalInetAddressMatcher();
static InternalInetAddressMatcher getInstance() {
return INSTANCE;
}
private InternalInetAddressMatcher() {
}
@Override
public boolean matches(@Nullable InetAddress address) {
if (address == null) {
return false;
}
if (address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress()) {
return true;
}
byte[] rawAddress = address.getAddress();
if (rawAddress.length == 16) {
// Convert signed bytes to unsigned ints for easier matching logic
int[] iAddr = new int[rawAddress.length];
for (int i = 0; i < rawAddress.length; i++) {
iAddr[i] = Byte.toUnsignedInt(rawAddress[i]);
}
/*
* IPv6, check for Unique Local Addresses. We cannot rely on
* Inet6Address.isSiteLocalAddress() here because the JVM implementation
* dictates that fec0::/10 is the only site-local IPv6 address space,
* based on the outdated RFC 2373. That RFC was deprecated by the IETF in
* 2004 in favor of fc00::/7 (RFC 4193). To keep our private network
* checking accurate to modern subnets, we maintain manual parsing.
*/
if (iAddr[0] == 0xfc || iAddr[0] == 0xfd) {
return true;
}
// IPv4/IPv6 translation, 64:ff9b
if (iAddr[0] == 0x00 && iAddr[1] == 0x64 && iAddr[2] == 0xff && iAddr[3] == 0x9b) {
try {
InetAddress ipv4Part = InetAddress.getByAddress(
new byte[] { rawAddress[12], rawAddress[13], rawAddress[14], rawAddress[15] });
if (ipv4Part.isLoopbackAddress() || ipv4Part.isLinkLocalAddress()
|| ipv4Part.isSiteLocalAddress()) {
return true;
}
}
catch (java.net.UnknownHostException ex) {
// Should not happen for 4-byte array
}
}
}
return false;
}
@Override
public String toString() {
return "InternalInetAddressMatcher";
}
}
/**
* An {@link InetAddressMatcher} that matches external (public) addresses.
* <p>
* External addresses are any addresses that are not internal (private) addresses.
* This matcher delegates to {@link InternalInetAddressMatcher} and negates the
* result.
*
* @author Gábor Vaspöri
* @author Kian Jamali
* @author Rossen Stoyanchev
* @author Rob Winch
*/
static final class ExternalInetAddressMatcher implements InetAddressMatcher {
private static final ExternalInetAddressMatcher INSTANCE = new ExternalInetAddressMatcher();
static ExternalInetAddressMatcher getInstance() {
return INSTANCE;
}
private final InternalInetAddressMatcher internalMatcher = InternalInetAddressMatcher.getInstance();
private ExternalInetAddressMatcher() {
}
@Override
public boolean matches(@Nullable InetAddress address) {
return !this.internalMatcher.matches(address);
}
@Override
public String toString() {
return "ExternalInetAddressMatcher";
}
}
/**
* A composite {@link InetAddressMatcher} that chains multiple matchers together. All
* matchers must match for an address to be allowed. If report-only mode is enabled,
* matching results are logged but all addresses are permitted.
*
* @author Gábor Vaspöri
* @author Kian Jamali
* @author Rossen Stoyanchev
* @author Rob Winch
*/
static final class CompositeInetAddressMatcher implements InetAddressMatcher {
private static final Log logger = LogFactory.getLog(InetAddressMatcher.class);
private final List<InetAddressMatcher> matchers;
private final boolean reportOnly;
CompositeInetAddressMatcher(List<InetAddressMatcher> matchers, boolean reportOnly) {
this.matchers = new ArrayList<>(matchers);
this.reportOnly = reportOnly;
}
@Override
public boolean matches(@Nullable InetAddress address) {
boolean result = doMatch(address);
return (this.reportOnly || result);
}
private boolean doMatch(@Nullable InetAddress address) {
for (InetAddressMatcher matcher : this.matchers) {
if (!matcher.matches(address)) {
if (logger.isDebugEnabled()) {
logger.debug("InetAddress " + address + " blocked by " + matcher);
}
return false;
}
}
return true;
}
}
}

76
web/src/main/java/org/springframework/security/web/util/matcher/InetAddressParser.java

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* Copyright 2004-present 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
*
* https://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.springframework.security.web.util.matcher;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.regex.Pattern;
import org.springframework.util.Assert;
/**
* Utility class for parsing IP addresses.
*
* @author Luke Taylor
* @author Steve Riesenberg
* @author Andrey Litvitski
* @author Rob Winch
* @since 7.1
*/
final class InetAddressParser {
private static Pattern IPV4 = Pattern.compile("^\\d{1,3}(?:\\.\\d{1,3}){0,3}(?:/\\d{1,2})?$");
/**
* Parses the given address string into an {@link InetAddress}.
* @param address the IP address string to parse
* @return the parsed {@link InetAddress}
* @throws IllegalArgumentException if the address cannot be parsed or appears to be a
* hostname
*/
static InetAddress parseAddress(String address) {
assertNotHostName(address);
try {
return InetAddress.getByName(address);
}
catch (UnknownHostException ex) {
throw new IllegalArgumentException("Failed to parse address '" + address + "'", ex);
}
}
static void assertNotHostName(String ipAddress) {
Assert.isTrue(isIpAddress(ipAddress),
() -> String.format("ipAddress %s doesn't look like an IP Address. Is it a host name?", ipAddress));
}
private static boolean isIpAddress(String ipAddress) {
if (!org.springframework.util.StringUtils.hasText(ipAddress)) {
return false;
}
// @formatter:off
return IPV4.matcher(ipAddress).matches()
|| ipAddress.charAt(0) == '['
|| ipAddress.charAt(0) == ':'
|| Character.digit(ipAddress.charAt(0), 16) != -1
&& ipAddress.indexOf(':') > 0;
// @formatter:on
}
private InetAddressParser() {
}
}

98
web/src/main/java/org/springframework/security/web/util/matcher/IpAddressMatcher.java

@ -16,15 +16,8 @@ @@ -16,15 +16,8 @@
package org.springframework.security.web.util.matcher;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Objects;
import java.util.regex.Pattern;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.jspecify.annotations.Nullable;
/**
* Matches a request based on IP Address or subnet mask matching against the remote
@ -40,11 +33,7 @@ import org.springframework.util.StringUtils; @@ -40,11 +33,7 @@ import org.springframework.util.StringUtils;
*/
public final class IpAddressMatcher implements RequestMatcher {
private static Pattern IPV4 = Pattern.compile("^\\d{1,3}(?:\\.\\d{1,3}){0,3}(?:/\\d{1,2})?$");
private final InetAddress requiredAddress;
private final int nMaskBits;
private final InetAddressMatcher matcher;
/**
* Takes a specific IP address or a range specified using the IP/Netmask (e.g.
@ -53,89 +42,26 @@ public final class IpAddressMatcher implements RequestMatcher { @@ -53,89 +42,26 @@ public final class IpAddressMatcher implements RequestMatcher {
* come.
*/
public IpAddressMatcher(String ipAddress) {
Assert.hasText(ipAddress, "ipAddress cannot be empty");
assertNotHostName(ipAddress);
String requiredAddress;
int nMaskBits;
if (ipAddress.indexOf('/') > 0) {
String[] parts = Objects.requireNonNull(StringUtils.split(ipAddress, "/"));
requiredAddress = parts[0];
nMaskBits = Integer.parseInt(parts[1]);
}
else {
requiredAddress = ipAddress;
nMaskBits = -1;
}
this.requiredAddress = parseAddress(requiredAddress);
this.nMaskBits = nMaskBits;
Assert.isTrue(this.requiredAddress.getAddress().length * 8 >= this.nMaskBits, () -> String
.format("IP address %s is too short for bitmask of length %d", requiredAddress, this.nMaskBits));
this.matcher = new IpInetAddressMatcher(ipAddress);
}
@Override
public boolean matches(HttpServletRequest request) {
return matches(request.getRemoteAddr());
}
public boolean matches(String ipAddress) {
// Do not match null or blank address
if (!StringUtils.hasText(ipAddress)) {
return false;
}
assertNotHostName(ipAddress);
InetAddress remoteAddress = parseAddress(ipAddress);
if (!this.requiredAddress.getClass().equals(remoteAddress.getClass())) {
return false;
}
if (this.nMaskBits < 0) {
return remoteAddress.equals(this.requiredAddress);
}
byte[] remAddr = remoteAddress.getAddress();
byte[] reqAddr = this.requiredAddress.getAddress();
int nMaskFullBytes = this.nMaskBits / 8;
for (int i = 0; i < nMaskFullBytes; i++) {
if (remAddr[i] != reqAddr[i]) {
return false;
}
}
byte finalByte = (byte) (0xFF00 >> (this.nMaskBits & 0x07));
if (finalByte != 0) {
return (remAddr[nMaskFullBytes] & finalByte) == (reqAddr[nMaskFullBytes] & finalByte);
}
return true;
}
private static void assertNotHostName(String ipAddress) {
Assert.isTrue(isIpAddress(ipAddress),
() -> String.format("ipAddress %s doesn't look like an IP Address. Is it a host name?", ipAddress));
return this.matcher.matches(request.getRemoteAddr());
}
private static boolean isIpAddress(String ipAddress) {
// @formatter:off
return IPV4.matcher(ipAddress).matches()
|| ipAddress.charAt(0) == '['
|| ipAddress.charAt(0) == ':'
|| Character.digit(ipAddress.charAt(0), 16) != -1
&& ipAddress.indexOf(':') > 0;
// @formatter:on
}
private InetAddress parseAddress(String address) {
try {
return InetAddress.getByName(address);
}
catch (UnknownHostException ex) {
throw new IllegalArgumentException("Failed to parse address '" + address + "'", ex);
}
/**
* Checks if the given IP address string matches the configured address pattern.
* @param ipAddress the IP address string to check (may be {@code null})
* @return {@code true} if the address matches, {@code false} otherwise
*/
public boolean matches(@Nullable String ipAddress) {
return this.matcher.matches(ipAddress);
}
@Override
public String toString() {
String hostAddress = this.requiredAddress.getHostAddress();
return (this.nMaskBits < 0) ? "IpAddress [" + hostAddress + "]"
: "IpAddress [" + hostAddress + "/" + this.nMaskBits + "]";
return this.matcher.toString();
}
}

117
web/src/main/java/org/springframework/security/web/util/matcher/IpInetAddressMatcher.java

@ -0,0 +1,117 @@ @@ -0,0 +1,117 @@
/*
* Copyright 2004-present 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
*
* https://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.springframework.security.web.util.matcher;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Objects;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link InetAddressMatcher} that matches IP addresses with support for
* CIDR notation (e.g., 192.168.1.0/24).
* <p>
* Both IPv4 and IPv6 addresses are supported. The matcher can be configured with either a
* specific IP address or a subnet using CIDR notation.
* <p>
* The logic from this class was migrated from {@link IpAddressMatcher} to provide a more
* general API that did not depend on the servlet APIs (e.g. HttpServletRequest).
*
* @author Luke Taylor
* @author Steve Riesenberg
* @author Andrey Litvitski
* @since 7.1
* @see IpAddressMatcher
*/
final class IpInetAddressMatcher implements InetAddressMatcher {
private static final Log logger = LogFactory.getLog(IpAddressMatcher.class);
private final InetAddress requiredAddress;
private final int nMaskBits;
IpInetAddressMatcher(String ipAddress) {
Assert.hasText(ipAddress, "ipAddress cannot be empty");
String requiredAddress;
int nMaskBits;
if (ipAddress.indexOf('/') > 0) {
String[] parts = Objects.requireNonNull(StringUtils.split(ipAddress, "/"));
requiredAddress = parts[0];
nMaskBits = Integer.parseInt(parts[1]);
}
else {
requiredAddress = ipAddress;
nMaskBits = -1;
}
this.requiredAddress = InetAddressParser.parseAddress(requiredAddress);
this.nMaskBits = nMaskBits;
Assert.isTrue(this.requiredAddress.getAddress().length * 8 >= this.nMaskBits, () -> String
.format("IP address %s is too short for bitmask of length %d", requiredAddress, this.nMaskBits));
}
private static InetAddress parse(String address) {
try {
InetAddress result = InetAddress.getByName(address);
if (address.matches(".*[a-zA-Z\\-].*$") && !address.contains(":")) {
logger.warn("Hostname '" + address + "' resolved to " + result.toString()
+ " will be used on IP address matching");
}
return result;
}
catch (UnknownHostException ex) {
throw new IllegalArgumentException(String.format("Failed to parse address '%s'", address), ex);
}
}
@Override
public boolean matches(@Nullable InetAddress toCheck) {
if (toCheck == null) {
return false;
}
if (this.nMaskBits < 0) {
return toCheck.equals(this.requiredAddress);
}
byte[] remAddr = toCheck.getAddress();
byte[] reqAddr = this.requiredAddress.getAddress();
int nMaskFullBytes = this.nMaskBits / 8;
byte finalByte = (byte) (0xFF00 >> (this.nMaskBits & 0x07));
for (int i = 0; i < nMaskFullBytes; i++) {
if (remAddr[i] != reqAddr[i]) {
return false;
}
}
if (finalByte != 0) {
return (remAddr[nMaskFullBytes] & finalByte) == (reqAddr[nMaskFullBytes] & finalByte);
}
return true;
}
@Override
public String toString() {
String hostAddress = this.requiredAddress.getHostAddress();
return (this.nMaskBits < 0) ? "IpAddress [" + hostAddress + "]"
: "IpAddress [" + hostAddress + "/" + this.nMaskBits + "]";
}
}

69
web/src/test/java/org/springframework/security/web/util/matcher/InetAddressMatcherTests.java

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
/*
* Copyright 2004-present 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
*
* https://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.springframework.security.web.util.matcher;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link InetAddressMatcher}.
*
* @author Rob Winch
*/
class InetAddressMatcherTests {
@Test
void matchesWhenStringValidIpv4ThenReturnsTrue() {
InetAddressMatcher matcher = (address) -> address.getHostAddress().equals("192.168.1.1");
assertThat(matcher.matches("192.168.1.1")).isTrue();
}
@Test
void matchesWhenStringValidIpv6ThenReturnsTrue() {
InetAddressMatcher matcher = (address) -> address.getHostAddress().equals("fe80:0:0:0:21f:5bff:fe33:bd68");
assertThat(matcher.matches("fe80::21f:5bff:fe33:bd68")).isTrue();
}
@Test
void matchesWhenStringNullThenReturnsFalse() {
InetAddressMatcher matcher = (address) -> true;
assertThat(matcher.matches((String) null)).isFalse();
}
@Test
void matchesWhenStringInvalidThenThrowsIllegalArgumentException() {
InetAddressMatcher matcher = (address) -> true;
assertThat(matcher.matches("192.168.1.1")).isTrue();
assertThatIllegalArgumentException().isThrownBy(() -> matcher.matches("not.an.ip.address"));
}
@Test
void matchesWhenStringMatchesPredicateThenReturnsTrue() {
InetAddressMatcher matcher = (address) -> address.getHostAddress().startsWith("192.168");
assertThat(matcher.matches("192.168.1.1")).isTrue();
assertThat(matcher.matches("192.168.100.200")).isTrue();
}
@Test
void matchesWhenStringDoesNotMatchPredicateThenReturnsFalse() {
InetAddressMatcher matcher = (address) -> address.getHostAddress().startsWith("192.168");
assertThat(matcher.matches("10.0.0.1")).isFalse();
}
}

499
web/src/test/java/org/springframework/security/web/util/matcher/InetAddressMatchersTests.java

@ -0,0 +1,499 @@ @@ -0,0 +1,499 @@
/*
* Copyright 2004-present 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
*
* https://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.springframework.security.web.util.matcher;
import java.net.InetAddress;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link InetAddressMatchers}.
*
* @author Rob Winch
*/
class InetAddressMatchersTests {
@Test
void builderWhenInvokedThenReturnsBuilder() {
assertThat(InetAddressMatchers.builder()).isNotNull();
}
@Test
void matchExternalWhenInvokedThenReturnsBuilder() {
InetAddressMatchers.Builder builder = InetAddressMatchers.matchExternal();
assertThat(builder).isNotNull();
}
@Test
void matchInternalWhenInvokedThenReturnsBuilder() {
InetAddressMatchers.Builder builder = InetAddressMatchers.matchInternal();
assertThat(builder).isNotNull();
}
@Nested
class BuilderTests {
@Test
void includeAddressesWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> InetAddressMatchers.builder().includeAddresses(null))
.withMessage("addresses cannot be empty");
}
@Test
void includeAddressesWhenEmptyListThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> InetAddressMatchers.builder().includeAddresses(List.of()))
.withMessage("addresses cannot be empty");
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "192.168.1.2" })
void includeAddressesWhenSingleAddressThenMatchesOnlyThatAddress(String testAddress) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder().includeAddresses(List.of("192.168.1.1")).build();
InetAddress address = InetAddress.getByName(testAddress);
boolean expected = testAddress.equals("192.168.1.1");
assertThat(matcher.matches(address)).isEqualTo(expected);
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "10.0.0.1", "8.8.8.8" })
void includeAddressesWhenMultipleAddressesThenMatchesAny(String testAddress) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder()
.includeAddresses(List.of("192.168.1.1", "10.0.0.1"))
.build();
InetAddress address = InetAddress.getByName(testAddress);
boolean expected = testAddress.equals("192.168.1.1") || testAddress.equals("10.0.0.1");
assertThat(matcher.matches(address)).isEqualTo(expected);
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "192.168.1.255", "192.168.2.1" })
void includeAddressesWhenCidrNotationThenMatchesSubnet(String testAddress) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder()
.includeAddresses(List.of("192.168.1.0/24"))
.build();
InetAddress address = InetAddress.getByName(testAddress);
boolean expected = testAddress.startsWith("192.168.1.");
assertThat(matcher.matches(address)).isEqualTo(expected);
}
@Test
void excludeAddressesWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> InetAddressMatchers.builder().excludeAddresses(null))
.withMessage("addresses cannot be empty");
}
@Test
void excludeAddressesWhenEmptyListThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> InetAddressMatchers.builder().excludeAddresses(List.of()))
.withMessage("addresses cannot be empty");
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "192.168.1.2" })
void excludeAddressesWhenSingleAddressThenBlocksOnlyThatAddress(String testAddress) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder().excludeAddresses(List.of("192.168.1.1")).build();
InetAddress address = InetAddress.getByName(testAddress);
boolean expected = !testAddress.equals("192.168.1.1");
assertThat(matcher.matches(address)).isEqualTo(expected);
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "10.0.0.1", "8.8.8.8" })
void excludeAddressesWhenMultipleAddressesThenBlocksAll(String testAddress) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder()
.excludeAddresses(List.of("192.168.1.1", "10.0.0.1"))
.build();
InetAddress address = InetAddress.getByName(testAddress);
boolean expected = !testAddress.equals("192.168.1.1") && !testAddress.equals("10.0.0.1");
assertThat(matcher.matches(address)).isEqualTo(expected);
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "192.168.1.255", "192.168.2.1" })
void excludeAddressesWhenCidrNotationThenBlocksSubnet(String testAddress) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder()
.excludeAddresses(List.of("192.168.1.0/24"))
.build();
InetAddress address = InetAddress.getByName(testAddress);
boolean expected = !testAddress.startsWith("192.168.1.");
assertThat(matcher.matches(address)).isEqualTo(expected);
}
@ParameterizedTest
@ValueSource(strings = { "10.0.0.1", "192.168.1.1" })
void matchAllWhenVarargsThenAddsMatchersToChain(String testAddress) throws Exception {
InetAddressMatcher customMatcher = (address) -> address.getHostAddress().startsWith("10.");
InetAddressMatcher matcher = InetAddressMatchers.builder().matchAll(customMatcher).build();
InetAddress address = InetAddress.getByName(testAddress);
boolean expected = testAddress.startsWith("10.");
assertThat(matcher.matches(address)).isEqualTo(expected);
}
@Test
void matchAllWhenNullVarargsThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> InetAddressMatchers.builder().matchAll((InetAddressMatcher[]) null))
.withMessage("matchers cannot be empty");
}
@Test
void matchAllWhenEmptyVarargsThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> InetAddressMatchers.builder().matchAll(new InetAddressMatcher[0]))
.withMessage("matchers cannot be empty");
}
@ParameterizedTest
@ValueSource(strings = { "10.0.0.1", "10.0.0.2", "192.168.1.1" })
void matchAllWhenMultipleMatchersThenAppliesAndLogic(String testAddress) throws Exception {
InetAddressMatcher startsWithTen = (address) -> address.getHostAddress().startsWith("10.");
InetAddressMatcher endsWithOne = (address) -> address.getHostAddress().endsWith(".1");
InetAddressMatcher matcher = InetAddressMatchers.builder().matchAll(startsWithTen, endsWithOne).build();
InetAddress address = InetAddress.getByName(testAddress);
boolean expected = testAddress.startsWith("10.") && testAddress.endsWith(".1");
assertThat(matcher.matches(address)).isEqualTo(expected);
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "8.8.8.8" })
void reportOnlyWhenSetThenAllowsAllAddresses(String testAddress) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder()
.excludeAddresses(List.of("192.168.1.1"))
.reportOnly()
.build();
InetAddress address = InetAddress.getByName(testAddress);
assertThat(matcher.matches(address)).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "192.168.1.100", "192.168.2.1" })
void buildWhenMultipleMatchersThenAppliesAndLogic(String testAddress) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder()
.includeAddresses(List.of("192.168.1.0/24"))
.excludeAddresses(List.of("192.168.1.100"))
.build();
InetAddress address = InetAddress.getByName(testAddress);
boolean expected = testAddress.startsWith("192.168.1.") && !testAddress.equals("192.168.1.100");
assertThat(matcher.matches(address)).isEqualTo(expected);
}
}
@Nested
class IncludeListInetAddressMatcherTests {
@Test
void constructorWhenNullListThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new InetAddressMatchers.IncludeListInetAddressMatcher(null))
.withMessage("includeList cannot be null or empty");
}
@Test
void constructorWhenEmptyListThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new InetAddressMatchers.IncludeListInetAddressMatcher(List.of()))
.withMessage("includeList cannot be null or empty");
}
@Test
void matchesWhenAddressInListThenReturnsTrue() throws Exception {
String addressString = "192.168.1.1";
InetAddressMatcher matcher = InetAddressMatchers.builder().includeAddresses(List.of(addressString)).build();
assertThat(matcher.matches(InetAddress.getByName(addressString))).isTrue();
}
@Test
void matchesWhenAddressNotInListThenReturnsFalse() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder().includeAddresses(List.of("192.168.1.1")).build();
assertThat(matcher.matches(InetAddress.getByName("192.168.1.2"))).isFalse();
}
}
@Nested
class ExcludeListInetAddressMatcherTests {
@Test
void constructorWhenNullListThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new InetAddressMatchers.ExcludeListInetAddressMatcher(null))
.withMessage("excludeList cannot be null or empty");
}
@Test
void constructorWhenEmptyListThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new InetAddressMatchers.ExcludeListInetAddressMatcher(List.of()))
.withMessage("excludeList cannot be null or empty");
}
@Test
void matchesWhenAddressInListThenReturnsFalse() throws Exception {
String addressString = "192.168.1.1";
InetAddressMatcher matcher = InetAddressMatchers.builder().excludeAddresses(List.of(addressString)).build();
assertThat(matcher.matches(InetAddress.getByName(addressString))).isFalse();
}
@Test
void matchesWhenAddressNotInListThenReturnsTrue() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder().excludeAddresses(List.of("192.168.1.1")).build();
assertThat(matcher.matches(InetAddress.getByName("192.168.1.2"))).isTrue();
}
}
@Nested
class InternalInetAddressMatcherTests {
@Test
void matchesWhenInetAddressNullThenReturnsFalse() {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches((InetAddress) null)).isFalse();
}
@ParameterizedTest
@ValueSource(strings = { "127.0.0.1", "127.0.0.255" })
void matchesWhenIpv4LoopbackThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@Test
void matchesWhenIpv6LoopbackThenReturnsTrue() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName("::1"))).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "10.0.0.1", "10.255.255.255" })
void matchesWhenIpv4PrivateClass10ThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "192.168.0.1", "192.168.255.255" })
void matchesWhenIpv4PrivateClass192ThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "169.254.0.0", "169.254.169.254", "169.254.255.255" })
void matchesWhenIpv4LinkLocalThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "172.16.0.1", "172.16.255.255", "172.17.1.1", "172.31.255.255" })
void matchesWhenIpv4PrivateClass172ThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "::ffff:192.168.1.1", "::ffff:169.254.169.254", "::ffff:10.0.0.1" })
void matchesWhenIpv4MappedIpv6InternalThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "fc00::1", "fd00::1" })
void matchesWhenIpv6UniqueLocalThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@ParameterizedTest
@ValueSource(
strings = { "64:ff9b::10.0.0.1", "64:ff9b::127.0.0.1", "64:ff9b::192.168.1.1", "64:ff9b::172.16.0.1" })
void matchesWhenIpv6TranslationWithInternalIpv4ThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "64:ff9b::192.0.2.1", "64:ff9b::192.167.1.1" })
void matchesWhenIpv6TranslationWithIpv4StartsWith192ButNot168ThenReturnsFalse(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
}
@ParameterizedTest
@ValueSource(strings = { "64:ff9b::172.16.0.1", "64:ff9b::172.16.255.255" })
void matchesWhenIpv6TranslationWithIpv4StartsWith172And16ThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "64:ff9b::8.8.8.8", "64:ff9b::1.1.1.1" })
void matchesWhenIpv6TranslationWithExternalIpv4ThenReturnsFalse(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
}
@Test
void matchesWhenIpv6NonTranslationPrefixByte0ThenReturnsFalse() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName("65:ff9b::10.0.0.1"))).isFalse();
}
@Test
void matchesWhenIpv6NonTranslationPrefixByte1ThenReturnsFalse() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName("64:fe9b::10.0.0.1"))).isFalse();
}
@Test
void matchesWhenIpv6NonTranslationPrefixByte2ThenReturnsFalse() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName("64:ff9a::10.0.0.1"))).isFalse();
}
@Test
void matchesWhenIpv6NonTranslationPrefixByte3ThenReturnsFalse() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName("64:ff9c::10.0.0.1"))).isFalse();
}
@ParameterizedTest
@ValueSource(strings = { "8.8.8.8", "1.1.1.1" })
void matchesWhenIpv4PublicThenReturnsFalse(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
}
@ParameterizedTest
@ValueSource(strings = { "192.0.2.1", "192.167.1.1", "192.169.1.1" })
void matchesWhenIpv4StartsWith192ButNot168ThenReturnsFalse(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
}
@ParameterizedTest
@ValueSource(strings = { "172.15.1.1", "172.32.1.1" })
void matchesWhenIpv4StartsWith172ButNotPrivate16To31ThenReturnsFalse(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
}
@Test
void matchesWhenIpv6PublicThenReturnsFalse() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
assertThat(matcher.matches(InetAddress.getByName("2001:4860:4860::8888"))).isFalse();
}
}
@Nested
class ExternalInetAddressMatcherTests {
@ParameterizedTest
@ValueSource(strings = { "8.8.8.8", "1.1.1.1" })
void matchesWhenIpv4PublicThenReturnsTrue(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
}
@Test
void matchesWhenIpv6PublicThenReturnsTrue() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
assertThat(matcher.matches(InetAddress.getByName("2001:4860:4860::8888"))).isTrue();
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "10.0.0.1", "172.16.0.1" })
void matchesWhenIpv4PrivateThenReturnsFalse(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
}
@Test
void matchesWhenIpv4LoopbackThenReturnsFalse() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
assertThat(matcher.matches(InetAddress.getByName("127.0.0.1"))).isFalse();
}
@ParameterizedTest
@ValueSource(strings = { "169.254.0.0", "169.254.169.254", "169.254.255.255" })
void matchesWhenIpv4LinkLocalThenReturnsFalse(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
}
@Test
void matchesWhenIpv6LoopbackThenReturnsFalse() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
assertThat(matcher.matches(InetAddress.getByName("::1"))).isFalse();
}
@ParameterizedTest
@ValueSource(strings = { "fc00::1", "fd00::1" })
void matchesWhenIpv6UniqueLocalThenReturnsFalse(String address) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
}
}
@Nested
class CompositeInetAddressMatcherTests {
@Test
void matchesWhenAllMatchersTrueThenReturnsTrue() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder()
.includeAddresses(List.of("192.168.1.0/24"))
.matchAll((address) -> address.getHostAddress().endsWith(".1"))
.build();
assertThat(matcher.matches(InetAddress.getByName("192.168.1.1"))).isTrue();
}
@Test
void matchesWhenOneMatcherFalseThenReturnsFalse() throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder()
.includeAddresses(List.of("192.168.1.0/24"))
.matchAll((address) -> address.getHostAddress().endsWith(".1"))
.build();
assertThat(matcher.matches(InetAddress.getByName("192.168.1.2"))).isFalse();
}
@ParameterizedTest
@ValueSource(strings = { "192.168.1.1", "8.8.8.8" })
void matchesWhenReportOnlyThenAlwaysReturnsTrue(String testAddress) throws Exception {
InetAddressMatcher matcher = InetAddressMatchers.builder()
.excludeAddresses(List.of("192.168.1.1"))
.reportOnly()
.build();
assertThat(matcher.matches(InetAddress.getByName(testAddress))).isTrue();
}
}
}

6
web/src/test/java/org/springframework/security/web/util/matcher/IpAddressMatcherTests.java

@ -154,6 +154,12 @@ public class IpAddressMatcherTests { @@ -154,6 +154,12 @@ public class IpAddressMatcherTests {
.withMessage("ipAddress cannot be empty");
}
@Test
public void isIpAddressWhenEmptyOrBlankThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new IpAddressMatcher(" "))
.withMessage("ipAddress cannot be empty");
}
// gh-16795
@Test
public void toStringWhenCidrIsProvidedThenReturnsIpAddressWithCidr() {

138
web/src/test/java/org/springframework/security/web/util/matcher/IpInetAddressMatcherTests.java

@ -0,0 +1,138 @@ @@ -0,0 +1,138 @@
/*
* Copyright 2004-present 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
*
* https://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.springframework.security.web.util.matcher;
import java.net.InetAddress;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link IpInetAddressMatcher}.
*
* @author Rob Winch
*/
class IpInetAddressMatcherTests {
@Test
void constructorWhenNullIpAddressThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new IpInetAddressMatcher(null))
.withMessage("ipAddress cannot be empty");
}
@Test
void constructorWhenEmptyIpAddressThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new IpInetAddressMatcher(""))
.withMessage("ipAddress cannot be empty");
}
@Test
void constructorWhenHostnameThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new IpInetAddressMatcher("example.com"))
.withMessageContaining("doesn't look like an IP Address");
}
@Test
void matchesWhenIpv4ExactMatchThenReturnsTrue() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
assertThat(matcher.matches(InetAddress.getByName("192.168.1.1"))).isTrue();
}
@Test
void matchesWhenIpv4NoMatchThenReturnsFalse() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
assertThat(matcher.matches(InetAddress.getByName("192.168.1.2"))).isFalse();
}
@Test
void matchesWhenIpv6ExactMatchThenReturnsTrue() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("fe80::21f:5bff:fe33:bd68");
assertThat(matcher.matches(InetAddress.getByName("fe80::21f:5bff:fe33:bd68"))).isTrue();
}
@Test
void matchesWhenIpv6NoMatchThenReturnsFalse() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("fe80::21f:5bff:fe33:bd68");
assertThat(matcher.matches(InetAddress.getByName("fe80::21f:5bff:fe33:bd69"))).isFalse();
}
@Test
void matchesWhenIpv4WithCidrMatchesSubnetThenReturnsTrue() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.0/24");
assertThat(matcher.matches(InetAddress.getByName("192.168.1.1"))).isTrue();
assertThat(matcher.matches(InetAddress.getByName("192.168.1.255"))).isTrue();
}
@Test
void matchesWhenIpv4WithCidrOutsideSubnetThenReturnsFalse() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.0/24");
assertThat(matcher.matches(InetAddress.getByName("192.168.2.1"))).isFalse();
assertThat(matcher.matches(InetAddress.getByName("192.168.0.255"))).isFalse();
}
@Test
void matchesWhenIpv6WithCidrMatchesSubnetThenReturnsTrue() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("2001:db8::/48");
assertThat(matcher.matches(InetAddress.getByName("2001:db8:0:0:0:0:0:0"))).isTrue();
assertThat(matcher.matches(InetAddress.getByName("2001:db8:0:ffff:ffff:ffff:ffff:ffff"))).isTrue();
}
@Test
void matchesWhenIpv6WithCidrOutsideSubnetThenReturnsFalse() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("2001:db8::/48");
assertThat(matcher.matches(InetAddress.getByName("2001:db8:1:0:0:0:0:0"))).isFalse();
}
@Test
void matchesWhenIpv4AndIpv6AddressThenReturnsFalse() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
assertThat(matcher.matches(InetAddress.getByName("fe80::21f:5bff:fe33:bd68"))).isFalse();
}
@Test
void matchesWhenIpv6AndIpv4AddressThenReturnsFalse() throws Exception {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("fe80::21f:5bff:fe33:bd68");
assertThat(matcher.matches(InetAddress.getByName("192.168.1.1"))).isFalse();
}
@Test
void matchesWhenStringIpv4MatchThenReturnsTrue() {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
assertThat(matcher.matches("192.168.1.1")).isTrue();
}
@Test
void matchesWhenStringIpv4NoMatchThenReturnsFalse() {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
assertThat(matcher.matches("192.168.1.2")).isFalse();
}
@Test
void matchesWhenStringNullThenReturnsFalse() {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
assertThat(matcher.matches((String) null)).isFalse();
}
@Test
void matchesWhenInetAddressNullThenReturnsFalse() {
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
assertThat(matcher.matches((InetAddress) null)).isFalse();
}
}
Loading…
Cancel
Save