This is an automated email from the ASF dual-hosted git repository. dklco pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-security.git
The following commit(s) were added to refs/heads/master by this push: new ed34399 SLING-9061 - Adding support for evaluating the origin as well as the referrer and pluggable referrer configurations (#9) ed34399 is described below commit ed343999a43e709000b239d57065d1fb89e7c832 Author: Dan Klco <klcod...@users.noreply.github.com> AuthorDate: Thu May 18 21:19:35 2023 -0400 SLING-9061 - Adding support for evaluating the origin as well as the referrer and pluggable referrer configurations (#9) * SLING-11871 - Adding support for bypassing the ReferrerFilter for requests with an Origin header * SLING-9061 - Adding support for evaluating the origin as well as the referrer and pluggable referrer configurations * Changing mergeValues to return a collection rather an an array --- .../apache/sling/security/impl/ReferrerFilter.java | 41 ++++++++-- .../security/impl/ReferrerFilterAmendment.java | 43 +++++++++++ .../security/impl/ReferrerFilterAmendmentImpl.java | 90 ++++++++++++++++++++++ .../sling/security/impl/ReferrerFilterTest.java | 87 +++++++++++++++++---- 4 files changed, 237 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/apache/sling/security/impl/ReferrerFilter.java b/src/main/java/org/apache/sling/security/impl/ReferrerFilter.java index 5125e57..f563234 100644 --- a/src/main/java/org/apache/sling/security/impl/ReferrerFilter.java +++ b/src/main/java/org/apache/sling/security/impl/ReferrerFilter.java @@ -27,11 +27,14 @@ import java.net.SocketException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Function; import java.util.regex.Pattern; + import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; @@ -42,6 +45,9 @@ import javax.servlet.http.HttpServletResponse; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; import org.osgi.service.http.whiteboard.Preprocessor; import org.osgi.service.metatype.annotations.AttributeDefinition; @@ -245,7 +251,7 @@ public class ReferrerFilter implements Preprocessor { /** * Create Patterns out of the regular expression referrer list */ - private Pattern[] createRegexPatterns(final String[] regexps) { + private Pattern[] createRegexPatterns(final Collection<String> regexps) { final List<Pattern> patterns = new ArrayList<>(); if (regexps != null) { for (final String regexp : regexps) { @@ -260,16 +266,32 @@ public class ReferrerFilter implements Preprocessor { return patterns.toArray(new Pattern[0]); } + private Collection<String> mergeValues(String[] primary, List<ReferrerFilterAmendment> amendments, + Function<ReferrerFilterAmendment, String[]> extractor) { + Set<String> consolidated = new HashSet<>(); + if (primary != null) { + Arrays.stream(primary).forEach(consolidated::add); + } + if (amendments != null) { + amendments.stream().map(extractor::apply).forEach(v -> Arrays.stream(v).forEach(consolidated::add)); + } + return consolidated; + } + @Activate - public ReferrerFilter(final Config config) { + public ReferrerFilter(final Config config, + @Reference(policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MULTIPLE, service=ReferrerFilterAmendment.class) List<ReferrerFilterAmendment> amendments) { this.allowEmpty = config.allow_empty(); - this.allowedRegexReferrers = createRegexPatterns(config.allow_hosts_regexp()); - this.excludedRegexUserAgents = createRegexPatterns(config.exclude_agents_regexp()); - this.excludedPaths = config.exclude_paths(); + this.allowedRegexReferrers = createRegexPatterns( + mergeValues(config.allow_hosts_regexp(), amendments, a -> a.allowHostsRegex())); + this.excludedRegexUserAgents = createRegexPatterns( + mergeValues(config.exclude_agents_regexp(), amendments, a -> a.excludeAgentsRegex())); + this.excludedPaths = mergeValues(config.exclude_paths(), amendments, a -> a.excludePaths()).toArray(new String[0]); final Set<String> allowUriReferrers = getDefaultAllowedReferrers(); if (config.allow_hosts() != null) { - allowUriReferrers.addAll(Arrays.asList(config.allow_hosts())); + allowUriReferrers.addAll( + mergeValues(config.allow_hosts(), amendments, a -> a.allowHosts())); } this.allowedUriReferrers = createReferrerUrls(allowUriReferrers); @@ -368,7 +390,12 @@ public class ReferrerFilter implements Preprocessor { return true; } - final String referrer = request.getHeader("referer"); + String referrer = request.getHeader("referer"); + // use the origin if the referrer is not set + if (referrer == null || referrer.trim().length() == 0) { + referrer = request.getHeader("origin"); + } + // check for missing/empty referrer if (referrer == null || referrer.trim().length() == 0) { if (!this.allowEmpty) { diff --git a/src/main/java/org/apache/sling/security/impl/ReferrerFilterAmendment.java b/src/main/java/org/apache/sling/security/impl/ReferrerFilterAmendment.java new file mode 100644 index 0000000..09f6fcb --- /dev/null +++ b/src/main/java/org/apache/sling/security/impl/ReferrerFilterAmendment.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.sling.security.impl; + +/** + * Amends the primary configuration of Referrer Filter + */ +public interface ReferrerFilterAmendment { + /** + * @return List of allowed hosts for the referrer which are added to the list of + * default hosts. + */ + String[] allowHosts(); + + /** + * @return List of allowed regular expression for the referrer. + */ + String[] allowHostsRegex(); + + /** + * @return List of regexp for user agents not to check the referrer + */ + String[] excludeAgentsRegex(); + + /** + * @return List of paths for which not to check the referrer + */ + String[] excludePaths(); +} diff --git a/src/main/java/org/apache/sling/security/impl/ReferrerFilterAmendmentImpl.java b/src/main/java/org/apache/sling/security/impl/ReferrerFilterAmendmentImpl.java new file mode 100644 index 0000000..4bdcb43 --- /dev/null +++ b/src/main/java/org/apache/sling/security/impl/ReferrerFilterAmendmentImpl.java @@ -0,0 +1,90 @@ +/* + * 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.sling.security.impl; + +import java.util.Optional; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@Component(service = ReferrerFilterAmendment.class) +@Designate(factory = true, ocd = ReferrerFilterAmendmentImpl.Config.class) +public class ReferrerFilterAmendmentImpl implements ReferrerFilterAmendment { + + private final Config config; + + @Activate + public ReferrerFilterAmendmentImpl(Config config) { + this.config = config; + } + + @Override + public String[] allowHosts() { + return Optional.ofNullable(config.allow_hosts()).orElse(new String[0]); + } + + @Override + public String[] allowHostsRegex() { + return Optional.ofNullable(config.allow_hosts_regexp()).orElse(new String[0]); + } + + @Override + public String[] excludeAgentsRegex() { + return Optional.ofNullable(config.exclude_agents_regexp()).orElse(new String[0]); + } + + @Override + public String[] excludePaths() { + return Optional.ofNullable(config.exclude_paths()).orElse(new String[0]); + } + + @ObjectClassDefinition(name = "Apache Sling Referrer Filter Amendment", description = "Amend the primary list of Referrer Filter allow hosts with additional hosts") + public @interface Config { + + @AttributeDefinition(name = "Allow Hosts", description = "List of allowed hosts for the referrer which are added to the list of default hosts. " + + "It is matched against the full referrer URL in the format \"<scheme>://<host>:<port>\". " + + "If port is 0, it is not taken into consideration. The default list contains all host names " + + "and IPs bound to all NICs found in the system plus \"localhost\", \"127.0.0.1\", \"[::1]\" for protocols \"http\" and \"https\". " + + "If given value does not have a \":\" entries for both http and https are transparently generated.") + String[] allow_hosts() default {}; + + /** + * Allow referrer regex hosts property + */ + @AttributeDefinition(name = "Allow Regexp Host", description = "List of allowed regular expression for the referrer. " + + "It is matched against the full referrer URL in the format \"<scheme>://<host>:<port>\". " + + "Evaluated in addition to the default list and the given allowed hosts (see above)!") + String[] allow_hosts_regexp() default {}; + + /** + * Excluded regexp user agents property + */ + @AttributeDefinition(name = "Exclude Regexp User Agent", description = "List of regexp for user agents not to check the referrer") + String[] exclude_agents_regexp() default {}; + + /** + * Excluded the configured paths from the referrer check + */ + @AttributeDefinition(name = "Exclude Paths", description = "List of paths for which not to check the referrer") + String[] exclude_paths() default {}; + + } + +} \ No newline at end of file diff --git a/src/test/java/org/apache/sling/security/impl/ReferrerFilterTest.java b/src/test/java/org/apache/sling/security/impl/ReferrerFilterTest.java index 53b1f29..0a68898 100644 --- a/src/test/java/org/apache/sling/security/impl/ReferrerFilterTest.java +++ b/src/test/java/org/apache/sling/security/impl/ReferrerFilterTest.java @@ -22,9 +22,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.lang.annotation.Annotation; +import java.util.Collections; import javax.servlet.http.HttpServletRequest; +import org.apache.sling.security.impl.ReferrerFilterAmendmentImpl.Config; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -35,15 +37,16 @@ public class ReferrerFilterTest { @Before public void setup() { - ReferrerFilter.Config config = createConfiguration(false, new String[]{"relhost"}, - new String[]{"http://([^.]*.)?abshost:80", "^app://.+"}, - new String[]{"[a-zA-Z]*\\/[0-9]*\\.[0-9]*;Some-Agent\\s.*"}, - new String[] {null, "/test_path"}); - filter = new ReferrerFilter(config); + ReferrerFilter.Config config = createConfiguration(false, new String[] { "relhost" }, + new String[] { "http://([^.]*.)?abshost:80", "^app://.+" }, + new String[] { "[a-zA-Z]*\\/[0-9]*\\.[0-9]*;Some-Agent\\s.*" }, + new String[] { null, "/test_path" }); + filter = new ReferrerFilter(config, Collections.emptyList()); } - private static ReferrerFilter.Config createConfiguration(boolean allowEmpty, String[] allowHosts, String[] allowHostsRexexp, - String[] excludeAgentsRegexp, String[] excludePaths) { + private static ReferrerFilter.Config createConfiguration(boolean allowEmpty, String[] allowHosts, + String[] allowHostsRexexp, + String[] excludeAgentsRegexp, String[] excludePaths) { return new ReferrerFilter.Config() { @Override public Class<? extends Annotation> annotationType() { @@ -79,7 +82,7 @@ public class ReferrerFilterTest { public String[] exclude_paths() { return excludePaths; } - }; + }; } @Test @@ -105,18 +108,18 @@ public class ReferrerFilterTest { final HttpServletRequest request = mock(HttpServletRequest.class); when(request.getMethod()).thenReturn("POST"); if (pathInfo != null) { - when(request.getRequestURI()).thenReturn("http://somehost/somewhere"+pathInfo); + when(request.getRequestURI()).thenReturn("http://somehost/somewhere" + pathInfo); when(request.getPathInfo()).thenReturn(pathInfo); } else { when(request.getRequestURI()).thenReturn("http://somehost/somewhere"); } when(request.getHeader("referer")).thenReturn(referrer); - if ( userAgent != null && userAgent.length() > 0 ) { + if (userAgent != null && userAgent.length() > 0) { when(request.getHeader("User-Agent")).thenReturn(userAgent); } return request; } - + private static HttpServletRequest getRequest(final String referrer, final String userAgent) { return getRequest(referrer, userAgent, null); } @@ -146,21 +149,22 @@ public class ReferrerFilterTest { assertTrue(filter.isValidRequest(getRequest("app://yet.another.abshost:80"))); assertFalse(filter.isValidRequest(getRequest("?://"))); } - + @Test public void testExcludedPath() { assertTrue(filter.isValidRequest(getRequest(null, null, "/test_path"))); assertFalse(filter.isValidRequest(getRequest(null, null, "/test_path/subtree"))); assertFalse(filter.isValidRequest(getRequest(null, null, "/test_path_sibling"))); - + assertTrue(filter.isValidRequest(getRequest("relative", null, "/test_path"))); assertTrue(filter.isValidRequest(getRequest("http://yet.another.abshost:80", null, "/test_path"))); } @Test public void testExcludedPathNull() { - ReferrerFilter rf = new ReferrerFilter(createConfiguration(false, null, null, null, null)); - + ReferrerFilter rf = new ReferrerFilter(createConfiguration(false, null, null, null, null), + Collections.emptyList()); + assertFalse(rf.isValidRequest(getRequest(null, null, "/test_path"))); assertFalse(rf.isValidRequest(getRequest(null, null, "/test_path/subtree"))); assertFalse(rf.isValidRequest(getRequest(null, null, "/test_path_sibling"))); @@ -168,10 +172,59 @@ public class ReferrerFilterTest { assertTrue(rf.isValidRequest(getRequest("relative", null, "/test_path"))); assertFalse(rf.isValidRequest(getRequest("http://yet.another.abshost:80", null, "/test_path"))); } - + + @Test + public void testWithAmendments() { + ReferrerFilterAmendment amendment = new ReferrerFilterAmendmentImpl(new Config() { + + @Override + public Class<? extends Annotation> annotationType() { + throw new UnsupportedOperationException("Unimplemented method 'annotationType'"); + } + + @Override + public String[] allow_hosts() { + return new String[]{"test.com"}; + } + + @Override + public String[] allow_hosts_regexp() { + return new String[]{".*test2.com.*"}; + } + + @Override + public String[] exclude_agents_regexp() { + return null; + } + + @Override + public String[] exclude_paths() { + return new String[]{"/testpath2"}; + } + + }); + ReferrerFilter rf = new ReferrerFilter(createConfiguration(false, null, new String[]{".*test1.com.*"}, null, null), Collections.singletonList(amendment)); + + assertTrue(rf.isValidRequest(getRequest(null, null, "/testpath2"))); + assertFalse(rf.isValidRequest(getRequest(null, null, "/test1path"))); + + assertFalse(rf.isValidRequest(getRequest("http://testnotvalid.com:80", null, "/test_path"))); + assertTrue(rf.isValidRequest(getRequest("http://test1.com:80", null, "/test_path"))); + assertTrue(rf.isValidRequest(getRequest("http://test2.com:80", null, "/test_path"))); + } + + + @Test + public void testAllowsWithOrigin(){ + HttpServletRequest request = getRequest(null); + when(request.getHeader("origin")).thenReturn("http://abshost"); + Assert.assertEquals(true, filter.isValidRequest(request)); + } + @Test public void testAllowEmpty() { - ReferrerFilter rf = new ReferrerFilter(createConfiguration(true, null, null, null, null)); + ReferrerFilter rf = new ReferrerFilter(createConfiguration(true, null, null, null, null), + Collections.emptyList()); assertTrue(rf.isValidRequest(getRequest(null, null, "/test_path"))); assertTrue(rf.isValidRequest(getRequest("", null, null)));