| /* |
| * 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.io.IOException; |
| import java.io.PrintWriter; |
| import java.net.Inet4Address; |
| import java.net.Inet6Address; |
| import java.net.InetAddress; |
| import java.net.MalformedURLException; |
| import java.net.NetworkInterface; |
| import java.net.SocketException; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| import javax.servlet.FilterChain; |
| import javax.servlet.FilterConfig; |
| import javax.servlet.ServletException; |
| import javax.servlet.ServletRequest; |
| import javax.servlet.ServletResponse; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import org.osgi.service.component.annotations.Activate; |
| import org.osgi.service.component.annotations.Component; |
| import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; |
| import org.osgi.service.http.whiteboard.Preprocessor; |
| import org.osgi.service.metatype.annotations.AttributeDefinition; |
| import org.osgi.service.metatype.annotations.Designate; |
| import org.osgi.service.metatype.annotations.ObjectClassDefinition; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| @Component( |
| service = Preprocessor.class, |
| property = { |
| HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT + "=(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)", |
| "felix.webconsole.label=slingreferrerfilter", |
| "felix.webconsole.title=Sling Referrer Filter", |
| "felix.webconsole.configprinter.modes=always" |
| } |
| ) |
| @Designate(ocd = ReferrerFilter.Config.class) |
| public class ReferrerFilter implements Preprocessor { |
| |
| /** |
| * Request header providing the clients user agent information used |
| * by {@link #isBrowserRequest(HttpServletRequest)} to decide whether |
| * a request is probably sent by a browser or not. |
| */ |
| private static final String USER_AGENT = "User-Agent"; |
| |
| /** |
| * String contained in a {@link #USER_AGENT} header indicating a Mozilla |
| * class browser. Examples of such browsers are Firefox (generally Gecko |
| * based browsers), Safari, Chrome (probably generally WebKit based |
| * browsers), and Microsoft IE. |
| */ |
| private static final String BROWSER_CLASS_MOZILLA = "Mozilla"; |
| |
| /** |
| * String contained in a {@link #USER_AGENT} header indicating a Opera class |
| * browser. The only known browser in this class is the Opera browser. |
| */ |
| private static final String BROWSER_CLASS_OPERA = "Opera"; |
| |
| /** |
| * Logger. |
| */ |
| private final Logger logger = LoggerFactory.getLogger(this.getClass()); |
| |
| @ObjectClassDefinition( |
| name = "Apache Sling Referrer Filter", |
| description = "Request filter checking the referrer of modification requests and denying request with a 403 in case the referrer is not allowed" |
| ) |
| public @interface Config { |
| |
| /** |
| * Allow empty property. |
| */ |
| @AttributeDefinition( |
| name = "Allow Empty", |
| description = "Allow an empty or missing referrer" |
| ) |
| boolean allow_empty() default false; |
| |
| /** |
| * Allow referrer uri hosts property. |
| */ |
| @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 {}; |
| |
| /** |
| * Filtered methods property |
| */ |
| @AttributeDefinition( |
| name = "Filter Methods", |
| description = "These methods are filtered by the filter" |
| ) |
| String[] filter_methods() default {"POST", "PUT", "DELETE", "COPY", "MOVE"}; |
| |
| /** |
| * 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 {}; |
| } |
| |
| |
| /** |
| * Do we allow empty referrer? |
| */ |
| private final boolean allowEmpty; |
| |
| /** Allowed uri referrers */ |
| private final URL[] allowedUriReferrers; |
| |
| /** Allowed regexp referrers */ |
| private final Pattern[] allowedRegexReferrers; |
| |
| /** Methods to be filtered. */ |
| private final String[] filterMethods; |
| |
| /** Paths to be excluded */ |
| private final Pattern[] excludedRegexUserAgents; |
| |
| /** |
| * Create a default list of referrers |
| */ |
| private Set<String> getDefaultAllowedReferrers() { |
| final Set<String> referrers = new HashSet<>(); |
| try { |
| final Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); |
| |
| while(ifaces.hasMoreElements()){ |
| final NetworkInterface iface = ifaces.nextElement(); |
| logger.info("Adding Allowed referers for Interface:" + iface.getDisplayName()); |
| final Enumeration<InetAddress> ias = iface.getInetAddresses(); |
| while(ias.hasMoreElements()){ |
| final InetAddress ia = ias.nextElement(); |
| final String address = ia.getHostAddress().trim().toLowerCase(); |
| if ( ia instanceof Inet4Address ) { |
| referrers.add("http://" + address + ":0"); |
| referrers.add("https://" + address + ":0"); |
| } |
| if ( ia instanceof Inet6Address ) { |
| referrers.add("http://[" + address + "]" + ":0"); |
| referrers.add("https://[" + address + "]" + ":0"); |
| } |
| } |
| } |
| } catch ( final SocketException se) { |
| logger.error("Unable to detect network interfaces", se); |
| } |
| referrers.add("http://localhost" + ":0"); |
| referrers.add("http://127.0.0.1" + ":0"); |
| referrers.add("http://[::1]" + ":0"); |
| referrers.add("https://localhost" + ":0"); |
| referrers.add("https://127.0.0.1" + ":0"); |
| referrers.add("https://[::1]" + ":0"); |
| |
| return referrers; |
| } |
| |
| private void add(final List<URL> urls, final String ref) { |
| try { |
| final URL u = new URL(ref); |
| urls.add(u); |
| } catch (final MalformedURLException mue) { |
| logger.warn("Unable to create URL from " + ref + " : " + mue.getMessage()); |
| } |
| } |
| |
| /** |
| * Create URLs out of the uri referrer set |
| */ |
| private URL[] createReferrerUrls(final Set<String> referrers) { |
| final List<URL> urls = new ArrayList<>(); |
| |
| for(final String ref : referrers) { |
| final int pos = ref.indexOf("://"); |
| // valid url? |
| if ( pos != -1 ) { |
| this.add(urls, ref); |
| } else { |
| this.add(urls, "http://" + ref + ":0"); |
| this.add(urls, "https://" + ref + ":0"); |
| } |
| } |
| return urls.toArray(new URL[urls.size()]); |
| } |
| |
| /** |
| * Create Patterns out of the regular expression referrer list |
| */ |
| private Pattern[] createRegexPatterns(final String[] regexps) { |
| final List<Pattern> patterns = new ArrayList<>(); |
| if ( regexps != null ) { |
| for(final String regexp : regexps) { |
| try { |
| final Pattern pattern = Pattern.compile(regexp); |
| patterns.add(pattern); |
| } catch (final Exception e) { |
| logger.warn("Unable to create Pattern from {} : {}", new Object[]{regexp, e.getMessage()}); |
| } |
| } |
| } |
| return patterns.toArray(new Pattern[patterns.size()]); |
| } |
| |
| @Activate |
| public ReferrerFilter(final Config config) { |
| this.allowEmpty = config.allow_empty(); |
| this.allowedRegexReferrers = createRegexPatterns(config.allow_hosts_regexp()); |
| this.excludedRegexUserAgents = createRegexPatterns(config.exclude_agents_regexp()); |
| |
| final Set<String> allowUriReferrers = getDefaultAllowedReferrers(); |
| if ( config.allow_hosts() != null ) { |
| allowUriReferrers.addAll(Arrays.asList(config.allow_hosts())); |
| } |
| this.allowedUriReferrers = createReferrerUrls(allowUriReferrers); |
| |
| String[] methods = config.filter_methods(); |
| if ( methods != null ) { |
| final List<String> values = new ArrayList<>(); |
| for(final String m : methods) { |
| if ( m != null && m.trim().length() > 0 ) { |
| values.add(m.trim().toUpperCase()); |
| } |
| } |
| if ( values.isEmpty() ) { |
| methods = null; |
| } else { |
| methods = values.toArray(new String[values.size()]); |
| } |
| } |
| this.filterMethods = methods; |
| } |
| |
| private boolean isModification(final HttpServletRequest req) { |
| final String method = req.getMethod(); |
| if ( filterMethods != null ) { |
| for(final String m : filterMethods) { |
| if ( m.equals(method) ) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void doFilter(final ServletRequest req, |
| final ServletResponse res, |
| final FilterChain chain) |
| throws IOException, ServletException { |
| if ( req instanceof HttpServletRequest && res instanceof HttpServletResponse ) { |
| final HttpServletRequest request = (HttpServletRequest)req; |
| |
| // is this a modification request from a browser |
| if ( this.isBrowserRequest(request) && this.isModification(request) ) { |
| if ( !this.isValidRequest(request) ) { |
| final HttpServletResponse response = (HttpServletResponse)res; |
| // we use 403 |
| response.sendError(403); |
| return; |
| } |
| } |
| } |
| chain.doFilter(req, res); |
| } |
| |
| final static class HostInfo { |
| public String host; |
| public String scheme; |
| public int port; |
| public String toURI() { |
| return scheme + "://" + host + ":" + port; |
| } |
| } |
| |
| HostInfo getHost(final String referrer) { |
| final int startPos = referrer.indexOf("://") + 3; |
| if ( startPos == 2 || startPos == referrer.length() ) { |
| // we consider this illegal |
| return null; |
| } |
| final HostInfo info = new HostInfo(); |
| info.scheme = referrer.substring(0, startPos - 3); |
| |
| final int paramStart = referrer.indexOf('?'); |
| final String hostAndPath = (paramStart == -1 ? referrer : referrer.substring(0, paramStart)); |
| final int endPos = hostAndPath.indexOf('/', startPos); |
| final String hostPart = (endPos == -1 ? hostAndPath.substring(startPos) : hostAndPath.substring(startPos, endPos)); |
| final int hostNameStart = hostPart.indexOf('@') + 1; |
| final int hostNameEnd = hostPart.lastIndexOf(':'); |
| if (hostNameEnd < hostNameStart ) { |
| info.host = hostPart.substring(hostNameStart); |
| if ( info.scheme.equals("http") ) { |
| info.port = 80; |
| } else if ( info.scheme.equals("https") ) { |
| info.port = 443; |
| } |
| } else { |
| info.host = hostPart.substring(hostNameStart, hostNameEnd); |
| info.port = Integer.valueOf(hostPart.substring(hostNameEnd + 1)); |
| } |
| return info; |
| } |
| |
| boolean isValidRequest(final HttpServletRequest request) { |
| final String referrer = request.getHeader("referer"); |
| // check for missing/empty referrer |
| if ( referrer == null || referrer.trim().length() == 0 ) { |
| if ( !this.allowEmpty ) { |
| this.logger.info("Rejected empty referrer header for {} request to {}", request.getMethod(), request.getRequestURI()); |
| } |
| return this.allowEmpty; |
| } |
| // check for relative referrer - which is always allowed |
| if ( referrer.indexOf(":/") == - 1 ) { |
| return true; |
| } |
| |
| final HostInfo info = getHost(referrer); |
| if ( info == null ) { |
| // if this is invalid we just return invalid |
| this.logger.info("Rejected illegal referrer header for {} request to {} : {}", |
| new Object[] {request.getMethod(), request.getRequestURI(), referrer}); |
| return false; |
| } |
| |
| // allow the request if the host name of the referrer is |
| // the same as the request's host name |
| if ( info.host.equals(request.getServerName()) ) { |
| return true; |
| } |
| |
| // allow the request if the referrer matches any of the allowed referrers |
| boolean valid = isValidUriReferrer(info) || isValidRegexReferrer(info); |
| |
| if ( !valid) { |
| this.logger.info("Rejected referrer header for {} request to {} : {}", |
| new Object[] {request.getMethod(), request.getRequestURI(), referrer}); |
| } |
| return valid; |
| } |
| |
| /** |
| * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) |
| */ |
| @Override |
| public void init(final FilterConfig config) throws ServletException { |
| // nothing to do |
| } |
| |
| /** |
| * @see javax.servlet.Filter#destroy() |
| */ |
| @Override |
| public void destroy() { |
| // nothing to do |
| } |
| |
| /** |
| * @param hostInfo The hostInfo to check for validity |
| * @return <code>true</code> if the hostInfo matches any of the allowed URI referrer. |
| */ |
| private boolean isValidUriReferrer(HostInfo hostInfo) { |
| for(final URL ref : this.allowedUriReferrers) { |
| if ( hostInfo.host.equals(ref.getHost()) && hostInfo.scheme.equals(ref.getProtocol()) ) { |
| if ( ref.getPort() == 0 || hostInfo.port == ref.getPort() ) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * @param hostInfo The hostInfo to check for validity |
| * @return <code>true</code> if the hostInfo matches any of the allowed regexp referrer. |
| */ |
| private boolean isValidRegexReferrer(HostInfo hostInfo) { |
| for(final Pattern ref : this.allowedRegexReferrers) { |
| String url = hostInfo.toURI(); |
| if (ref.matcher(url).matches()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns <code>true</code> if the provided user agent matches any present exclusion regexp pattern. |
| * @param userAgent The user agent string to check |
| * @return <code>true</code> if the user agent matches any exclusion pattern. |
| */ |
| private boolean isExcludedRegexUserAgent(String userAgent) { |
| for(final Pattern pattern : this.excludedRegexUserAgents) { |
| if (pattern.matcher(userAgent).matches()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns <code>true</code> if the given request can be assumed to be sent |
| * by a client browser such as Firefix, Internet Explorer, etc. |
| * <p> |
| * This method inspects the <code>User-Agent</code> header and returns |
| * <code>true</code> if the header contains the string <i>Mozilla</i> (known |
| * to be contained in Firefox, Internet Explorer, WebKit-based browsers |
| * User-Agent) or <i>Opera</i> (known to be contained in the Opera |
| * User-Agent). |
| * |
| * @param request The request to inspect |
| * @return <code>true</code> if the request is assumed to be sent by a |
| * browser. |
| */ |
| protected boolean isBrowserRequest(final HttpServletRequest request) { |
| final String userAgent = request.getHeader(USER_AGENT); |
| return userAgent != null |
| && (userAgent.contains(BROWSER_CLASS_MOZILLA) || userAgent.contains(BROWSER_CLASS_OPERA)) |
| && !isExcludedRegexUserAgent(userAgent); |
| } |
| |
| /** |
| * Print out the allowedReferrers |
| * @see org.apache.felix.webconsole.ConfigurationPrinter#printConfiguration(java.io.PrintWriter) |
| * @param pw the PrintWriter object |
| */ |
| public void printConfiguration(final PrintWriter pw) { |
| pw.println("Current Apache Sling Referrer Filter Allowed Referrers:"); |
| pw.println(); |
| for (final URL url : allowedUriReferrers) { |
| pw.println(url.toString()); |
| } |
| for (final Pattern pattern : allowedRegexReferrers) { |
| pw.println(pattern.toString()); |
| } |
| } |
| } |