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 static org.apache.wicket.csp.CSPDirective.BASE_URI;
020import static org.apache.wicket.csp.CSPDirective.CHILD_SRC;
021import static org.apache.wicket.csp.CSPDirective.CONNECT_SRC;
022import static org.apache.wicket.csp.CSPDirective.DEFAULT_SRC;
023import static org.apache.wicket.csp.CSPDirective.FONT_SRC;
024import static org.apache.wicket.csp.CSPDirective.IMG_SRC;
025import static org.apache.wicket.csp.CSPDirective.MANIFEST_SRC;
026import static org.apache.wicket.csp.CSPDirective.REPORT_URI;
027import static org.apache.wicket.csp.CSPDirective.SCRIPT_SRC;
028import static org.apache.wicket.csp.CSPDirective.STYLE_SRC;
029import static org.apache.wicket.csp.CSPDirectiveSrcValue.NONCE;
030import static org.apache.wicket.csp.CSPDirectiveSrcValue.NONE;
031import static org.apache.wicket.csp.CSPDirectiveSrcValue.SELF;
032import static org.apache.wicket.csp.CSPDirectiveSrcValue.STRICT_DYNAMIC;
033import static org.apache.wicket.csp.CSPDirectiveSrcValue.UNSAFE_EVAL;
034import static org.apache.wicket.csp.CSPDirectiveSrcValue.UNSAFE_INLINE;
035
036import java.util.ArrayList;
037import java.util.Collections;
038import java.util.EnumMap;
039import java.util.List;
040import java.util.Map;
041import java.util.stream.Collectors;
042
043import org.apache.wicket.request.cycle.RequestCycle;
044
045/**
046 * {@code CSPHeaderConfiguration} contains the configuration for a Content-Security-Policy header.
047 * This configuration is constructed using the available {@link CSPDirective}s. An number of default
048 * profiles is provided. These profiles can be used as a basis for a specific CSP. Extra directives
049 * can be added or existing directives modified.
050 *
051 * @author papegaaij
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 */
056public class CSPHeaderConfiguration
057{
058        public static final String CSP_VIOLATION_REPORTING_URI = "cspviolation";
059
060        private final Map<CSPDirective, List<CSPRenderable>> directives = new EnumMap<>(CSPDirective.class);
061
062        private boolean addLegacyHeaders = false;
063
064        private boolean nonceEnabled = false;
065
066        private String reportUriMountPath = null;
067
068        /**
069         * Removes all directives from the CSP, returning an empty configuration.
070         *
071         * @return {@code this} for chaining.
072         */
073        public CSPHeaderConfiguration disabled()
074        {
075                return clear();
076        }
077
078        /**
079         * Builds a CSP configuration with the following directives: {@code default-src 'none';}
080         * {@code script-src 'self' 'unsafe-inline' 'unsafe-eval';}
081         * {@code style-src 'self' 'unsafe-inline';} {@code img-src 'self';} {@code connect-src 'self';}
082         * {@code font-src 'self';} {@code manifest-src 'self';} {@code child-src 'self';}
083         * {@code frame-src 'self'} {@code base-uri 'self'}. This will allow resources to be loaded
084         * from {@code 'self'} (the current host). In addition, unsafe inline Javascript,
085         * {@code eval()} and inline CSS is allowed.
086         *
087         * It is recommended to not allow {@code unsafe-inline} or {@code unsafe-eval}, because those
088         * can be used to trigger XSS attacks in your application (often in combination with another
089         * bug). Because older application often rely on inline scripting and styling, this CSP can be
090         * used as a stepping stone for older Wicket applications, before switching to {@link #strict}.
091         * Using a CSP with unsafe directives is still more secure than using no CSP at all.
092         *
093         * @return {@code this} for chaining.
094         */
095        public CSPHeaderConfiguration unsafeInline()
096        {
097                return clear().add(DEFAULT_SRC, NONE)
098                        .add(SCRIPT_SRC, SELF, UNSAFE_INLINE, UNSAFE_EVAL)
099                        .add(STYLE_SRC, SELF, UNSAFE_INLINE)
100                        .add(IMG_SRC, SELF)
101                        .add(CONNECT_SRC, SELF)
102                        .add(FONT_SRC, SELF)
103                        .add(MANIFEST_SRC, SELF)
104                        .add(CHILD_SRC, SELF)
105                        .add(BASE_URI, SELF);
106        }
107
108        /**
109         * Builds a strict, very secure CSP configuration with the following directives:
110         * {@code default-src 'none';} {@code script-src 'strict-dynamic' 'nonce-XYZ';}
111         * {@code style-src 'nonce-XYZ';} {@code img-src 'self';} {@code connect-src 'self';}
112         * {@code font-src 'self';} {@code manifest-src 'self';} {@code child-src 'self';}
113         * {@code frame-src 'self'} {@code base-uri 'self'}. This will allow most resources to be loaded
114         * from {@code 'self'} (the current host). Scripts and styles are only allowed when rendered with
115         * the correct nonce.
116         * Wicket will automatically add the nonces to the {@code script} and {@code link} (CSS)
117         * elements and to the headers.
118         *
119         * @return {@code this} for chaining.
120         */
121        public CSPHeaderConfiguration strict()
122        {
123                return clear().add(DEFAULT_SRC, NONE)
124                        .add(SCRIPT_SRC, STRICT_DYNAMIC, NONCE)
125                        .add(STYLE_SRC, NONCE)
126                        .add(IMG_SRC, SELF)
127                        .add(CONNECT_SRC, SELF)
128                        .add(FONT_SRC, SELF)
129                        .add(MANIFEST_SRC, SELF)
130                        .add(CHILD_SRC, SELF)
131                        .add(BASE_URI, SELF);
132        }
133
134        /**
135         * Configures the CSP to report violations back at the application.
136         *
137         * WARNING: CSP reporting can generate a lot of traffic. A single page load can trigger multiple
138         * violations and flood your logs or even DDoS your server. In addition, it is an open endpoint
139         * for your application and can be used by an attacker to flood your application logs. Do not
140         * enable this feature on a production application unless you take the needed precautions to
141         * prevent this.
142         *
143         * @return {@code this} for chaining
144         * @see <a href=
145         *      "https://scotthelme.co.uk/just-how-much-traffic-can-you-generate-using-csp">https://scotthelme.co.uk/just-how-much-traffic-can-you-generate-using-csp</a>
146         */
147        public CSPHeaderConfiguration reportBack()
148        {
149                return reportBackAt(CSP_VIOLATION_REPORTING_URI);
150        }
151
152        /**
153         * Configures the CSP to report violations at the specified relative URI.
154         *
155         * WARNING: CSP reporting can generate a lot of traffic. A single page load can trigger multiple
156         * violations and flood your logs or even DDoS your server. In addition, it is an open endpoint
157         * for your application and can be used by an attacker to flood your application logs. Do not
158         * enable this feature on a production application unless you take the needed precautions to
159         * prevent this.
160         *
161         * @param mountPath
162         *            The path to report the violations at.
163         * @return {@code this} for chaining
164         * @see <a href=
165         *      "https://scotthelme.co.uk/just-how-much-traffic-can-you-generate-using-csp">https://scotthelme.co.uk/just-how-much-traffic-can-you-generate-using-csp</a>
166         */
167        public CSPHeaderConfiguration reportBackAt(String mountPath)
168        {
169                return add(REPORT_URI, new RelativeURICSPValue(mountPath));
170        }
171
172        /**
173         * Returns the report URI mount path.
174         *
175         * @return the report URI mount path.
176         */
177        String getReportUriMountPath()
178        {
179                return reportUriMountPath;
180        }
181
182        /**
183         * True when the {@link CSPDirectiveSrcValue#NONCE} is used in one of the directives.
184         *
185         * @return When any of the directives contains a nonce.
186         */
187        public boolean isNonceEnabled()
188        {
189                return nonceEnabled;
190        }
191
192        /**
193         * True when legacy headers should be added.
194         *
195         * @return True when legacy headers should be added.
196         */
197        public boolean isAddLegacyHeaders()
198        {
199                return addLegacyHeaders;
200        }
201
202        /**
203         * Enable legacy {@code X-Content-Security-Policy} headers for older browsers, such as IE.
204         *
205         * @param addLegacyHeaders
206         *            True when the legacy headers should be added.
207         * @return {@code this} for chaining
208         */
209        public CSPHeaderConfiguration setAddLegacyHeaders(boolean addLegacyHeaders)
210        {
211                this.addLegacyHeaders = addLegacyHeaders;
212                return this;
213        }
214
215        /**
216         * Removes the given directive from the configuration.
217         *
218         * @param directive
219         *            The directive to remove.
220         * @return {@code this} for chaining
221         */
222        public CSPHeaderConfiguration remove(CSPDirective directive)
223        {
224                directives.remove(directive);
225                return recalculateState();
226        }
227
228        /**
229         * Adds the given values to the CSP directive on this configuraiton.
230         *
231         * @param directive
232         *            The directive to add the values to.
233         * @param values
234         *            The values to add.
235         * @return {@code this} for chaining
236         */
237        public CSPHeaderConfiguration add(CSPDirective directive, CSPRenderable... values)
238        {
239                for (CSPRenderable value : values)
240                {
241                        doAddDirective(directive, value);
242                }
243                return recalculateState();
244        }
245
246        /**
247         * Adds a free-form value to a directive for the CSP header. This is primarily meant to used for
248         * URIs.
249         *
250         * @param directive
251         *            The directive to add the values to.
252         * @param values
253         *            The values to add.
254         * @return {@code this} for chaining
255         */
256        public CSPHeaderConfiguration add(CSPDirective directive, String... values)
257        {
258                for (String value : values)
259                {
260                        doAddDirective(directive, new FixedCSPValue(value));
261                }
262                return recalculateState();
263        }
264
265        /**
266         * Returns an unmodifiable map of the directives set for this header.
267         * 
268         * @return The directives set for this header.
269         */
270        public Map<CSPDirective, List<CSPRenderable>> getDirectives()
271        {
272                return Collections.unmodifiableMap(directives);
273        }
274
275        /**
276         * @return true if this {@code CSPHeaderConfiguration} has any directives configured.
277         */
278        public boolean isSet()
279        {
280                return !directives.isEmpty();
281        }
282
283        /**
284         * Removes all CSP directives from the configuration.
285         *
286         * @return {@code this} for chaining.
287         */
288        public CSPHeaderConfiguration clear()
289        {
290                directives.clear();
291                return recalculateState();
292        }
293
294        private CSPHeaderConfiguration recalculateState()
295        {
296                nonceEnabled = directives.values()
297                        .stream()
298                        .flatMap(List::stream)
299                        .anyMatch(value -> value == CSPDirectiveSrcValue.NONCE);
300
301                reportUriMountPath = null;
302                List<CSPRenderable> reportValues = directives.get(CSPDirective.REPORT_URI);
303                if (reportValues != null && !reportValues.isEmpty())
304                {
305                        CSPRenderable reportUri = reportValues.get(0);
306                        if (reportUri instanceof RelativeURICSPValue)
307                        {
308                                reportUriMountPath = reportUri.toString();
309                        }
310                }
311                return this;
312        }
313
314        private void doAddDirective(CSPDirective directive, CSPRenderable value)
315        {
316                // Add backwards compatible frame-src
317                // see http://caniuse.com/#feat=contentsecuritypolicy2
318                if (CSPDirective.CHILD_SRC.equals(directive)
319                        && !directives.containsKey(CSPDirective.FRAME_SRC))
320                {
321                        doAddDirective(CSPDirective.FRAME_SRC,
322                                new ClonedCSPValue(this, CSPDirective.CHILD_SRC));
323                }
324                List<CSPRenderable> values = directives.computeIfAbsent(directive, x -> new ArrayList<>());
325                directive.checkValueForDirective(value, values);
326                values.add(value);
327        }
328
329        /**
330         * Renders this {@code CSPHeaderConfiguration} into an HTTP header. The returned String will be
331         * in the form {@code "key1 value1a value1b; key2 value2a; key3 value3a value3b value3c"}.
332         *
333         * @param settings
334         *            The {@link ContentSecurityPolicySettings} that renders the header.
335         * @param cycle
336         *            The current {@link RequestCycle}.
337         * @return the rendered header.
338         */
339        public String renderHeaderValue(ContentSecurityPolicySettings settings, RequestCycle cycle)
340        {
341                return directives.entrySet()
342                        .stream()
343                        .map(e -> e.getKey().getValue() + " "
344                                + e.getValue()
345                                        .stream()
346                                        .map(r -> r.render(settings, cycle))
347                                        .collect(Collectors.joining(" ")))
348                        .collect(Collectors.joining("; "));
349        }
350}