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                                application.getSecuritySettings().getRandomSupplier().getRandomBase64(NONCE_LENGTH);
082        }
083
084        public CSPHeaderConfiguration blocking()
085        {
086                return configs.computeIfAbsent(CSPHeaderMode.BLOCKING, x -> new CSPHeaderConfiguration());
087        }
088
089        public CSPHeaderConfiguration reporting()
090        {
091                return configs.computeIfAbsent(CSPHeaderMode.REPORT_ONLY,
092                        x -> new CSPHeaderConfiguration());
093        }
094
095        /**
096         * Sets the creator of nonces.
097         * 
098         * @param nonceCreator
099         *            The new creator, must not be null.
100         * @return {@code this} for chaining.
101         */
102        public ContentSecurityPolicySettings setNonceCreator(Supplier<String> nonceCreator)
103        {
104                Args.notNull(nonceCreator, "nonceCreator");
105                this.nonceCreator = nonceCreator;
106                return this;
107        }
108        
109        /**
110         * Sets the predicate that determines which requests must be protected by the CSP. When the
111         * predicate evaluates to false, the request will not be protected.
112         * 
113         * @param protectedFilter
114         *            The new filter, must not be null.
115         * @return {@code this} for chaining.
116         */
117        public ContentSecurityPolicySettings setProtectedFilter(
118                Predicate<IRequestHandler> protectedFilter)
119        {
120                Args.notNull(protectedFilter, "protectedFilter");
121                this.protectedFilter = protectedFilter;
122                return this;
123        }
124
125        /**
126         * Should any request be protected by CSP.
127         *
128         * @param handler
129         * @return <code>true</code> by default for all {@link RenderPageRequestHandler}s
130         * 
131         * @see #setProtectedFilter(Predicate)
132         */
133        protected boolean mustProtectRequest(IRequestHandler handler)
134        {
135                return protectedFilter.test(handler);
136        }
137
138        /**
139         * Returns true if any of the headers includes a directive with a nonce.
140         * 
141         * @return If a nonce is used in the CSP.
142         */
143        public final boolean isNonceEnabled()
144        {
145                return configs.values().stream().anyMatch(CSPHeaderConfiguration::isNonceEnabled);
146        }
147
148        public String getNonce(RequestCycle cycle)
149        {
150                IRequestHandler handler = cycle.getActiveRequestHandler();
151                
152                Page currentPage = IPageRequestHandler.getPage(handler);
153
154                String nonce = cycle.getMetaData(NONCE_KEY);
155                if (nonce == null)
156                {
157                        if (currentPage != null)
158                        {
159                                nonce = currentPage.getMetaData(NONCE_KEY);
160                        }
161                        if (nonce == null)
162                        {
163                                nonce = createNonce();
164                        }
165                        cycle.setMetaData(NONCE_KEY, nonce);
166                }
167
168                if (currentPage != null)
169                {
170                        currentPage.setMetaData(NONCE_KEY, nonce);
171                }
172
173                return nonce;
174        }
175
176        /**
177         * Create a new nonce.
178         *
179         * @return nonce
180         * 
181         * @see #setNonceCreator(Supplier)
182         */
183        protected String createNonce()
184        {
185                return nonceCreator.get();
186        }
187
188        /**
189         * Returns the CSP configuration per {@link CSPHeaderMode}.
190         * 
191         * @return the CSP configuration per {@link CSPHeaderMode}.
192         */
193        public Map<CSPHeaderMode, CSPHeaderConfiguration> getConfiguration()
194        {
195                return Collections.unmodifiableMap(configs);
196        }
197
198        /**
199         * Enforce CSP settings on an application.
200         * 
201         * @param application
202         *            application
203         */
204        public void enforce(WebApplication application)
205        {
206                application.getRequestCycleListeners().add(new CSPRequestCycleListener(this));
207                application.getHeaderResponseDecorators()
208                        .addPreResourceAggregationDecorator(response -> new CSPNonceHeaderResponseDecorator(response, this));
209                application.mount(new ReportCSPViolationMapper(this));
210        }
211
212        /**
213         * Is CSP enabled.
214         */
215        public boolean isEnabled()
216        {
217                return configs.values().stream().anyMatch(CSPHeaderConfiguration::isSet);
218        }
219}