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}