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.juneau.http.header; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.security.*; 022import java.util.*; 023import java.util.function.*; 024 025/** 026 * Represents an HTTP <c>Content-Security-Policy</c> (or <c>Content-Security-Policy-Report-Only</c>) response header. 027 * 028 * <p> 029 * A Content Security Policy lets a server declare which content sources a browser may load and execute for a page, 030 * mitigating cross-site-scripting (XSS) and data-injection attacks. This bean carries an already-rendered policy 031 * value; use {@link Builder} to assemble the value from typed directives. 032 * 033 * <h5 class='section'>Example:</h5> 034 * <p class='bjava'> 035 * <jc>// Build a static policy and emit it as a response header.</jc> 036 * ContentSecurityPolicy <jv>csp</jv> = ContentSecurityPolicy.<jsm>create</jsm>() 037 * .defaultSrc(ContentSecurityPolicy.Builder.<jsf>SELF</jsf>) 038 * .objectSrc(ContentSecurityPolicy.Builder.<jsf>NONE</jsf>) 039 * .toHeader(); 040 * 041 * <jc>// => "Content-Security-Policy: default-src 'self'; object-src 'none'"</jc> 042 * </p> 043 * 044 * <p> 045 * Because this is an {@link HttpStringHeader}, an instance can be added directly to the REST server's 046 * <c>defaultResponseHeaders</c> {@link HttpHeaderList} (e.g. via a <c><ja>@Bean</ja>(name=<js>"defaultResponseHeaders"</js>)</c> 047 * factory method) and is emitted through the normal response-header path. 048 * 049 * <h5 class='section'>See Also:</h5><ul> 050 * <li class='jc'>{@link Builder} 051 * <li class='extlink'><a class='doclink' href='https://www.w3.org/TR/CSP3/'>Content Security Policy Level 3 (W3C WD)</a> 052 * <li class='extlink'><a class='doclink' href='https://www.w3.org/TR/CSP2/'>Content Security Policy Level 2 (W3C Rec)</a> 053 * </ul> 054 * 055 * @since 10.0.0 056 */ 057@SuppressWarnings({ 058 "java:S115" // NAME / REPORT_ONLY_NAME mirror the external HTTP header literal exactly. 059}) 060public class ContentSecurityPolicy extends HttpStringHeader { 061 062 /** The enforcing header name: {@value}. */ 063 public static final String NAME = "Content-Security-Policy"; 064 065 /** The observe-only header name: {@value}. */ 066 public static final String REPORT_ONLY_NAME = "Content-Security-Policy-Report-Only"; 067 068 /** 069 * Constructor with an eager value (enforcing {@value #NAME} header). 070 * 071 * @param value The header value. May be <jk>null</jk>. 072 */ 073 public ContentSecurityPolicy(String value) { 074 super(NAME, value); 075 } 076 077 /** 078 * Constructor with a lazy value supplier (enforcing {@value #NAME} header). 079 * 080 * @param valueSupplier Supplier for the header value. Must not be <jk>null</jk>. 081 */ 082 public ContentSecurityPolicy(Supplier<String> valueSupplier) { 083 super(NAME, valueSupplier); 084 } 085 086 /** 087 * Constructor allowing the header name to be chosen (enforcing vs. report-only). 088 * 089 * @param name The header name. Must not be <jk>null</jk>. 090 * @param value The header value. May be <jk>null</jk>. 091 */ 092 protected ContentSecurityPolicy(String name, String value) { 093 super(name, value); 094 } 095 096 /** 097 * Constructor allowing the header name to be chosen, with a lazy value supplier. 098 * 099 * @param name The header name. Must not be <jk>null</jk>. 100 * @param valueSupplier Supplier for the header value. Must not be <jk>null</jk>. 101 */ 102 protected ContentSecurityPolicy(String name, Supplier<String> valueSupplier) { 103 super(name, valueSupplier); 104 } 105 106 /** 107 * Static factory method for an enforcing ({@value #NAME}) header with an eager value. 108 * 109 * @param value The header value. May be <jk>null</jk>. 110 * @return A new instance. Never <jk>null</jk>. 111 */ 112 public static ContentSecurityPolicy of(String value) { 113 return new ContentSecurityPolicy(value); 114 } 115 116 /** 117 * Static factory method for an enforcing ({@value #NAME}) header with a lazy value supplier. 118 * 119 * @param valueSupplier Supplier for the header value. Must not be <jk>null</jk>. 120 * @return A new instance. Never <jk>null</jk>. 121 */ 122 public static ContentSecurityPolicy of(Supplier<String> valueSupplier) { 123 return new ContentSecurityPolicy(valueSupplier); 124 } 125 126 /** 127 * Static factory method for an observe-only ({@value #REPORT_ONLY_NAME}) header with an eager value. 128 * 129 * <p> 130 * Browsers report violations against this header but do not block anything, letting operators tune a policy 131 * before enforcing it. 132 * 133 * @param value The header value. May be <jk>null</jk>. 134 * @return A new instance. Never <jk>null</jk>. 135 */ 136 public static ContentSecurityPolicy ofReportOnly(String value) { 137 return new ContentSecurityPolicy(REPORT_ONLY_NAME, value); 138 } 139 140 /** 141 * Static factory method for an observe-only ({@value #REPORT_ONLY_NAME}) header with a lazy value supplier. 142 * 143 * @param valueSupplier Supplier for the header value. Must not be <jk>null</jk>. 144 * @return A new instance. Never <jk>null</jk>. 145 */ 146 public static ContentSecurityPolicy ofReportOnly(Supplier<String> valueSupplier) { 147 return new ContentSecurityPolicy(REPORT_ONLY_NAME, valueSupplier); 148 } 149 150 /** 151 * Creates a new empty {@link Builder}. 152 * 153 * @return A new builder. Never <jk>null</jk>. 154 */ 155 public static Builder create() { 156 return Builder.create(); 157 } 158 159 /** 160 * Creates a {@link Builder} pre-populated with the nonce-based <i>strict starter</i> preset. 161 * 162 * @return A new builder pre-populated with the strict-starter directives. Never <jk>null</jk>. 163 * @see Builder#strictStarter() 164 */ 165 public static Builder strictStarter() { 166 return Builder.strictStarter(); 167 } 168 169 //------------------------------------------------------------------------------------------------------------------ 170 // Inner types 171 //------------------------------------------------------------------------------------------------------------------ 172 173 /** 174 * Fluent builder for assembling a <c>Content-Security-Policy</c> header value. 175 * 176 * <p> 177 * Composes the directives defined by 178 * <a class='doclink' href='https://www.w3.org/TR/CSP3/'>CSP Level 3</a> into the {@code directive source source; ...} 179 * wire format expected by the {@code Content-Security-Policy} response header. Typed setters cover the common 180 * directives; {@link #directive(String, String...)} is a generic escape hatch so new directives never require an API 181 * change. Setting the same directive more than once <b>replaces</b> the previous source list. 182 * 183 * <h5 class='section'>Example:</h5> 184 * <p class='bjava'> 185 * String <jv>value</jv> = ContentSecurityPolicy.<jsm>create</jsm>() 186 * .defaultSrc(<jsf>SELF</jsf>) 187 * .imgSrc(<jsf>SELF</jsf>, <js>"data:"</js>) 188 * .objectSrc(<jsf>NONE</jsf>) 189 * .build(); 190 * <jc>// => "default-src 'self'; img-src 'self' data:; object-src 'none'"</jc> 191 * </p> 192 * 193 * <h5 class='section'>Nonces:</h5> 194 * <p> 195 * Source helper {@link #nonce(String)} renders a <c>'nonce-<token>'</c> source expression. For per-request 196 * nonces (where the actual token is not known until the response is rendered), use {@link #NONCE_PLACEHOLDER} as the 197 * token and have the REST layer rewrite it per-response via {@link #resolveNonce(String, String)}; the same nonce is 198 * then stamped onto the inline <c><script></c>/<c><style></c> tags the serializer emits. 199 * 200 * <h5 class='section'>See Also:</h5><ul> 201 * <li class='jc'>{@link ContentSecurityPolicy} 202 * <li class='extlink'><a class='doclink' href='https://www.w3.org/TR/CSP3/'>Content Security Policy Level 3 (W3C WD)</a> 203 * </ul> 204 * 205 * @since 10.0.0 206 */ 207 public static class Builder { 208 209 //-------------------------------------------------------------------------------------------------------------- 210 // Source-expression keyword helpers 211 //-------------------------------------------------------------------------------------------------------------- 212 213 /** The <c>'self'</c> source-expression keyword. */ 214 public static final String SELF = "'self'"; 215 216 /** The <c>'none'</c> source-expression keyword. */ 217 public static final String NONE = "'none'"; 218 219 /** The <c>'unsafe-inline'</c> source-expression keyword. */ 220 public static final String UNSAFE_INLINE = "'unsafe-inline'"; 221 222 /** The <c>'unsafe-eval'</c> source-expression keyword. */ 223 public static final String UNSAFE_EVAL = "'unsafe-eval'"; 224 225 /** The <c>'strict-dynamic'</c> source-expression keyword. */ 226 public static final String STRICT_DYNAMIC = "'strict-dynamic'"; 227 228 /** 229 * The placeholder token used in nonce sources when the actual per-request nonce is not yet known. 230 * 231 * <p> 232 * {@link #strictStarter()} emits {@code 'nonce-{nonce}'}; the REST layer substitutes the real nonce per-response 233 * via {@link #resolveNonce(String, String)}. 234 */ 235 public static final String NONCE_PLACEHOLDER = "{nonce}"; 236 237 /** 238 * Renders a <c>'nonce-<token>'</c> source expression. 239 * 240 * @param token The base64 nonce value (or {@link #NONCE_PLACEHOLDER} for deferred substitution). Must not be <jk>null</jk> or blank. 241 * @return The source expression. Never <jk>null</jk>. 242 * @throws IllegalArgumentException If {@code token} is <jk>null</jk> or blank. 243 */ 244 public static String nonce(String token) { 245 reqnn("token", token); 246 if (token.trim().isEmpty()) 247 throw iaex("nonce token must not be blank"); 248 return "'nonce-" + token + "'"; 249 } 250 251 /** 252 * Renders a scheme source expression, ensuring a single trailing colon (e.g. <js>"https"</js> => <c>https:</c>). 253 * 254 * @param scheme The scheme (with or without a trailing colon). Must not be <jk>null</jk> or blank. 255 * @return The scheme source expression. Never <jk>null</jk>. 256 * @throws IllegalArgumentException If {@code scheme} is <jk>null</jk> or blank. 257 */ 258 public static String scheme(String scheme) { 259 reqnn("scheme", scheme); 260 var s = scheme.trim(); 261 if (s.isEmpty()) 262 throw iaex("scheme must not be blank"); 263 return s.endsWith(":") ? s : s + ":"; 264 } 265 266 /** 267 * Renders a <c>'sha256-<hash>'</c> (or sha384/sha512) source expression. 268 * 269 * @param algorithm One of <js>"sha256"</js>, <js>"sha384"</js>, or <js>"sha512"</js>. Must not be <jk>null</jk> or blank. 270 * @param base64Hash The base64-encoded hash of the inline block. Must not be <jk>null</jk> or blank. 271 * @return The hash source expression. Never <jk>null</jk>. 272 * @throws IllegalArgumentException If either argument is <jk>null</jk> or blank. 273 */ 274 public static String hash(String algorithm, String base64Hash) { 275 reqnn("algorithm", algorithm); 276 reqnn("base64Hash", base64Hash); 277 if (algorithm.trim().isEmpty()) 278 throw iaex("hash algorithm must not be blank"); 279 if (base64Hash.trim().isEmpty()) 280 throw iaex("hash value must not be blank"); 281 return "'" + algorithm.trim() + "-" + base64Hash.trim() + "'"; 282 } 283 284 private static final SecureRandom SECURE_RANDOM = new SecureRandom(); 285 286 /** 287 * Mints a fresh, cryptographically-random CSP nonce. 288 * 289 * <p> 290 * Generates 128 bits (16 bytes) from {@link SecureRandom} encoded as an unpadded base64url string. Each call 291 * returns a new value; a nonce must never be reused across responses. 292 * 293 * @return A new nonce token. Never <jk>null</jk>. 294 */ 295 public static String generateNonce() { 296 var bytes = new byte[16]; 297 SECURE_RANDOM.nextBytes(bytes); 298 return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); 299 } 300 301 /** 302 * Replaces every occurrence of {@link #NONCE_PLACEHOLDER} in a rendered policy value with the supplied nonce. 303 * 304 * @param policyValue The rendered policy value (may contain {@link #NONCE_PLACEHOLDER}). May be <jk>null</jk>. 305 * @param nonce The per-request nonce token. Must not be <jk>null</jk>. 306 * @return The policy value with placeholders substituted, or <jk>null</jk> if {@code policyValue} was <jk>null</jk>. 307 * @throws IllegalArgumentException If {@code nonce} is <jk>null</jk>. 308 */ 309 public static String resolveNonce(String policyValue, String nonce) { 310 reqnn("nonce", nonce); 311 if (policyValue == null) 312 return null; 313 return policyValue.replace(NONCE_PLACEHOLDER, nonce); 314 } 315 316 //-------------------------------------------------------------------------------------------------------------- 317 // Instance 318 //-------------------------------------------------------------------------------------------------------------- 319 320 private final Map<String,List<String>> directives = m(); 321 private boolean reportOnly; 322 323 /** 324 * Creates a new empty builder. 325 * 326 * @return A new builder. Never <jk>null</jk>. 327 */ 328 public static Builder create() { 329 return new Builder(); 330 } 331 332 /** 333 * Creates a builder pre-populated with the nonce-based <i>strict starter</i> preset. 334 * 335 * <p> 336 * The preset is: 337 * <p class='bcode'> 338 * default-src 'self'; 339 * script-src 'self' 'nonce-{nonce}'; 340 * style-src 'self' 'nonce-{nonce}'; 341 * img-src 'self' data:; 342 * connect-src 'self'; 343 * object-src 'none'; 344 * base-uri 'self'; 345 * frame-ancestors 'self' 346 * </p> 347 * 348 * <p> 349 * The {@code {nonce}} tokens are {@link #NONCE_PLACEHOLDER}s; for the policy to function on Juneau-served HTML 350 * (which emits inline {@code <script>}/{@code <style>}) the REST layer must mint a per-request nonce, substitute 351 * it via {@link #resolveNonce(String, String)}, and stamp the same value onto the emitted inline tags. 352 * 353 * @return A new pre-populated builder. Never <jk>null</jk>. 354 */ 355 public static Builder strictStarter() { 356 return create() 357 .defaultSrc(SELF) 358 .scriptSrc(SELF, nonce(NONCE_PLACEHOLDER)) 359 .styleSrc(SELF, nonce(NONCE_PLACEHOLDER)) 360 .imgSrc(SELF, "data:") 361 .connectSrc(SELF) 362 .objectSrc(NONE) 363 .baseUri(SELF) 364 .frameAncestors(SELF); 365 } 366 367 /** 368 * Sets the <c>default-src</c> directive. 369 * 370 * @param sources The source expressions. May be empty. 371 * @return This object. 372 */ 373 public Builder defaultSrc(String...sources) { 374 return directive("default-src", sources); 375 } 376 377 /** 378 * Sets the <c>script-src</c> directive. 379 * 380 * @param sources The source expressions. May be empty. 381 * @return This object. 382 */ 383 public Builder scriptSrc(String...sources) { 384 return directive("script-src", sources); 385 } 386 387 /** 388 * Sets the <c>style-src</c> directive. 389 * 390 * @param sources The source expressions. May be empty. 391 * @return This object. 392 */ 393 public Builder styleSrc(String...sources) { 394 return directive("style-src", sources); 395 } 396 397 /** 398 * Sets the <c>img-src</c> directive. 399 * 400 * @param sources The source expressions. May be empty. 401 * @return This object. 402 */ 403 public Builder imgSrc(String...sources) { 404 return directive("img-src", sources); 405 } 406 407 /** 408 * Sets the <c>connect-src</c> directive. 409 * 410 * @param sources The source expressions. May be empty. 411 * @return This object. 412 */ 413 public Builder connectSrc(String...sources) { 414 return directive("connect-src", sources); 415 } 416 417 /** 418 * Sets the <c>font-src</c> directive. 419 * 420 * @param sources The source expressions. May be empty. 421 * @return This object. 422 */ 423 public Builder fontSrc(String...sources) { 424 return directive("font-src", sources); 425 } 426 427 /** 428 * Sets the <c>object-src</c> directive. 429 * 430 * @param sources The source expressions. May be empty. 431 * @return This object. 432 */ 433 public Builder objectSrc(String...sources) { 434 return directive("object-src", sources); 435 } 436 437 /** 438 * Sets the <c>base-uri</c> directive. 439 * 440 * @param sources The source expressions. May be empty. 441 * @return This object. 442 */ 443 public Builder baseUri(String...sources) { 444 return directive("base-uri", sources); 445 } 446 447 /** 448 * Sets the <c>frame-ancestors</c> directive. 449 * 450 * @param sources The source expressions. May be empty. 451 * @return This object. 452 */ 453 public Builder frameAncestors(String...sources) { 454 return directive("frame-ancestors", sources); 455 } 456 457 /** 458 * Sets the <c>report-uri</c> directive (deprecated in CSP3 but still widely supported). 459 * 460 * @param uris The collector URIs. May be empty. 461 * @return This object. 462 */ 463 public Builder reportUri(String...uris) { 464 return directive("report-uri", uris); 465 } 466 467 /** 468 * Sets the <c>report-to</c> directive (the CSP3 reporting-group mechanism). 469 * 470 * @param groups The reporting-group names. May be empty. 471 * @return This object. 472 */ 473 public Builder reportTo(String...groups) { 474 return directive("report-to", groups); 475 } 476 477 /** 478 * Generic escape hatch for setting any directive by name. 479 * 480 * <p> 481 * Replaces any previously-registered source list for the same directive while preserving its original position. 482 * 483 * @param name The directive name (e.g. <js>"worker-src"</js>). Must not be <jk>null</jk> or blank. 484 * @param sources The source expressions. May be empty or <jk>null</jk> (renders the bare directive name). 485 * @return This object. 486 * @throws IllegalArgumentException If {@code name} is <jk>null</jk> or blank. 487 */ 488 public Builder directive(String name, String...sources) { 489 reqnn("name", name); 490 var n = name.trim(); 491 if (n.isEmpty()) 492 throw iaex("directive name must not be blank"); 493 directives.put(n, sources == null ? l() : l(sources)); 494 return this; 495 } 496 497 /** 498 * Flips the rendered header to the observe-only {@link ContentSecurityPolicy#REPORT_ONLY_NAME} variant. 499 * 500 * @return This object. 501 */ 502 public Builder reportOnly() { 503 return reportOnly(true); 504 } 505 506 /** 507 * Sets whether the rendered header uses the observe-only {@link ContentSecurityPolicy#REPORT_ONLY_NAME} variant. 508 * 509 * @param value <jk>true</jk> to emit {@code Content-Security-Policy-Report-Only} instead of {@code Content-Security-Policy}. 510 * @return This object. 511 */ 512 public Builder reportOnly(boolean value) { 513 reportOnly = value; 514 return this; 515 } 516 517 /** 518 * Returns <jk>true</jk> if this builder is configured to emit the report-only header variant. 519 * 520 * @return A boolean flag. 521 */ 522 public boolean isReportOnly() { 523 return reportOnly; 524 } 525 526 /** 527 * Builds the rendered policy value (the header value only, not including the header name). 528 * 529 * <p> 530 * Returns an empty string when no directives have been registered. 531 * 532 * @return The policy value. Never <jk>null</jk>. 533 */ 534 public String build() { 535 var parts = new ArrayList<String>(directives.size()); 536 directives.forEach((name, sources) -> { 537 if (sources.isEmpty()) 538 parts.add(name); 539 else 540 parts.add(name + " " + String.join(" ", sources)); 541 }); 542 return String.join("; ", parts); 543 } 544 545 /** 546 * Builds a {@link ContentSecurityPolicy} header bean from this builder. 547 * 548 * <p> 549 * The header name is {@link ContentSecurityPolicy#REPORT_ONLY_NAME} when {@link #isReportOnly()} is <jk>true</jk>, 550 * otherwise {@link ContentSecurityPolicy#NAME}. 551 * 552 * @return A new header bean carrying {@link #build()}. Never <jk>null</jk>. 553 */ 554 public ContentSecurityPolicy toHeader() { 555 return reportOnly ? ContentSecurityPolicy.ofReportOnly(build()) : ContentSecurityPolicy.of(build()); 556 } 557 558 @Override 559 public String toString() { 560 return build(); 561 } 562 } 563}