001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.csp;
018
019import java.util.Collections;
020import java.util.EnumMap;
021import java.util.Map;
022import java.util.function.Predicate;
023import java.util.function.Supplier;
024
025import org.apache.wicket.Application;
026import org.apache.wicket.MetaDataKey;
027import org.apache.wicket.Page;
028import org.apache.wicket.core.request.handler.IPageRequestHandler;
029import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
030import org.apache.wicket.protocol.http.WebApplication;
031import org.apache.wicket.request.IRequestHandler;
032import org.apache.wicket.request.cycle.RequestCycle;
033import org.apache.wicket.util.lang.Args;
034
035/**
036 * Build the CSP configuration like this:
037 * 
038 * <pre>
039 * {@code
040 *  myApplication.getCspSettings().blocking().clear()
041 *      .add(CSPDirective.DEFAULT_SRC, CSPDirectiveSrcValue.NONE)
042 *      .add(CSPDirective.SCRIPT_SRC, CSPDirectiveSrcValue.SELF)
043 *      .add(CSPDirective.IMG_SRC, CSPDirectiveSrcValue.SELF)
044 *      .add(CSPDirective.FONT_SRC, CSPDirectiveSrcValue.SELF));
045 *
046 *  myApplication.getCspSettings().reporting().strict();
047 *      }
048 * </pre>
049 * 
050 * See {@link CSPHeaderConfiguration} for more details on specifying the configuration.
051 *
052 * @see <a href="https://www.w3.org/TR/CSP2/">https://www.w3.org/TR/CSP2</a>
053 * @see <a href=
054 *      "https://developer.mozilla.org/en-US/docs/Web/Security/CSP">https://developer.mozilla.org/en-US/docs/Web/Security/CSP</a>
055 *
056 * @author Sven Haster
057 * @author Emond Papegaaij
058 */
059public class ContentSecurityPolicySettings
060{
061        // The number of bytes to use for a nonce, 18 will result in a 24 char nonce.
062        private static final int NONCE_LENGTH = 18;
063
064        public static final MetaDataKey<String> NONCE_KEY = new MetaDataKey<>()
065        {
066                private static final long serialVersionUID = 1L;
067        };
068
069        private final Map<CSPHeaderMode, CSPHeaderConfiguration> configs = new EnumMap<>(
070                CSPHeaderMode.class);
071
072        private Predicate<IRequestHandler> protectedFilter = RenderPageRequestHandler.class::isInstance;
073
074        private Supplier<String> nonceCreator;
075        
076        public ContentSecurityPolicySettings(Application application)
077        {
078                Args.notNull(application, "application");
079                
080                nonceCreator = () -> {
081                                return application.getSecuritySettings().getRandomSupplier().getRandomBase64(NONCE_LENGTH);
082                        };
083        }
084
085        public CSPHeaderConfiguration blocking()
086        {
087                return configs.computeIfAbsent(CSPHeaderMode.BLOCKING, x -> new CSPHeaderConfiguration());
088        }
089
090        public CSPHeaderConfiguration reporting()
091        {
092                return configs.computeIfAbsent(CSPHeaderMode.REPORT_ONLY,
093                        x -> new CSPHeaderConfiguration());
094        }
095
096        /**
097         * Sets the creator of nonces.
098         * 
099         * @param nonceCreator
100         *            The new creator, must not be null.
101         * @return {@code this} for chaining.
102         */
103        public ContentSecurityPolicySettings setNonceCreator(Supplier<String> nonceCreator)
104        {
105                Args.notNull(nonceCreator, "nonceCreator");
106                this.nonceCreator = nonceCreator;
107                return this;
108        }
109        
110        /**
111         * Sets the predicate that determines which requests must be protected by the CSP. When the
112         * predicate evaluates to false, the request will not be protected.
113         * 
114         * @param protectedFilter
115         *            The new filter, must not be null.
116         * @return {@code this} for chaining.
117         */
118        public ContentSecurityPolicySettings setProtectedFilter(
119                Predicate<IRequestHandler> protectedFilter)
120        {
121                Args.notNull(protectedFilter, "protectedFilter");
122                this.protectedFilter = protectedFilter;
123                return this;
124        }
125
126        /**
127         * Should any request be protected by CSP.
128         *
129         * @param handler
130         * @return <code>true</code> by default for all {@link RenderPageRequestHandler}s
131         * 
132         * @see #setProtectedFilter(Predicate)
133         */
134        protected boolean mustProtectRequest(IRequestHandler handler)
135        {
136                return protectedFilter.test(handler);
137        }
138
139        /**
140         * Returns true if any of the headers includes a directive with a nonce.
141         * 
142         * @return If a nonce is used in the CSP.
143         */
144        public final boolean isNonceEnabled()
145        {
146                return configs.values().stream().anyMatch(CSPHeaderConfiguration::isNonceEnabled);
147        }
148
149        public String getNonce(RequestCycle cycle)
150        {
151                IRequestHandler handler = cycle.getActiveRequestHandler();
152                
153                Page currentPage = IPageRequestHandler.getPage(handler);
154
155                String nonce = cycle.getMetaData(NONCE_KEY);
156                if (nonce == null)
157                {
158                        if (currentPage != null)
159                        {
160                                nonce = currentPage.getMetaData(NONCE_KEY);
161                        }
162                        if (nonce == null)
163                        {
164                                nonce = createNonce();
165                        }
166                        cycle.setMetaData(NONCE_KEY, nonce);
167                }
168
169                if (currentPage != null)
170                {
171                        currentPage.setMetaData(NONCE_KEY, nonce);
172                }
173
174                return nonce;
175        }
176
177        /**
178         * Create a new nonce.
179         *
180         * @return nonce
181         * 
182         * @see #setNonceCreator(Supplier)
183         */
184        protected String createNonce()
185        {
186                return nonceCreator.get();
187        }
188
189        /**
190         * Returns the CSP configuration per {@link CSPHeaderMode}.
191         * 
192         * @return the CSP configuration per {@link CSPHeaderMode}.
193         */
194        public Map<CSPHeaderMode, CSPHeaderConfiguration> getConfiguration()
195        {
196                return Collections.unmodifiableMap(configs);
197        }
198
199        /**
200         * Enforce CSP settings on an application.
201         * 
202         * @param application
203         *            application
204         */
205        public void enforce(WebApplication application)
206        {
207                application.getRequestCycleListeners().add(new CSPRequestCycleListener(this));
208                application.getHeaderResponseDecorators()
209                        .addPreResourceAggregationDecorator(response -> new CSPNonceHeaderResponseDecorator(response, this));
210                application.mount(new ReportCSPViolationMapper(this));
211        }
212
213        /**
214         * Is CSP enabled.
215         */
216        public boolean isEnabled()
217        {
218                return configs.values().stream().anyMatch(CSPHeaderConfiguration::isSet);
219        }
220}