001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.http.security.config; 017 018import org.jspecify.annotations.Nullable; 019 020import java.util.HashSet; 021import java.util.Objects; 022import java.util.Set; 023 024/** 025 * Builder class for constructing {@link SecurityConfiguration} instances with fluent API. 026 * 027 * <p>This builder provides a convenient way to construct SecurityConfiguration objects 028 * with sensible defaults while allowing fine-grained control over all security settings. 029 * The builder follows the standard builder pattern with method chaining.</p> 030 * 031 * <h3>Design Principles</h3> 032 * <ul> 033 * <li><strong>Fluent API</strong> - All setter methods return the builder for chaining</li> 034 * <li><strong>Sensible Defaults</strong> - Pre-configured with reasonable security defaults</li> 035 * <li><strong>Validation</strong> - Input validation on all parameters</li> 036 * <li><strong>Immutability</strong> - Produces immutable SecurityConfiguration instances</li> 037 * </ul> 038 * 039 * <h3>Usage Examples</h3> 040 * <pre> 041 * // Basic configuration with defaults 042 * SecurityConfiguration config = SecurityConfiguration.builder().build(); 043 * 044 * // Custom configuration 045 * SecurityConfiguration custom = SecurityConfiguration.builder() 046 * .maxPathLength(2048) 047 * .allowPathTraversal(false) 048 * .maxParameterCount(50) 049 * .requireSecureCookies(true) 050 * .addBlockedHeaderName("X-Debug") 051 * .addAllowedContentType("application/json") 052 * .addAllowedContentType("text/plain") 053 * .build(); 054 * 055 * // Chain multiple settings 056 * SecurityConfiguration strict = SecurityConfiguration.builder() 057 * .pathSecurity(1024, false) 058 * .cookieSecurity(true, true, 10, 64, 512) 059 * .bodySecurity(1024 * 1024, Set.of("application/json")) 060 * .encoding(false, false, false, true) 061 * .policies(true, true, true) 062 * .build(); 063 * </pre> 064 * 065 * <h3>Default Values</h3> 066 * <p>The builder is initialized with balanced default values that provide reasonable 067 * security without being overly restrictive:</p> 068 * <ul> 069 * <li>Path length: 4096 characters</li> 070 * <li>Parameter count: 100</li> 071 * <li>Header count: 50</li> 072 * <li>Cookie count: 20</li> 073 * <li>Body size: 5MB</li> 074 * <li>Path traversal: disabled</li> 075 * <li>Cookie security flags: recommended but not required</li> 076 * </ul> 077 * 078 * Implements: Task C2 from HTTP verification specification 079 * 080 * @since 1.0 081 * @see SecurityConfiguration 082 */ 083public class SecurityConfigurationBuilder { 084 085 // Path Security defaults 086 private int maxPathLength = 4096; 087 private boolean allowPathTraversal = false; 088 private boolean allowDoubleEncoding = false; 089 090 // Parameter Security defaults 091 private int maxParameterCount = 100; 092 private int maxParameterNameLength = 128; 093 private int maxParameterValueLength = 2048; 094 095 // Header Security defaults 096 private int maxHeaderCount = 50; 097 private int maxHeaderNameLength = 128; 098 private int maxHeaderValueLength = 2048; 099 private @Nullable Set<String> allowedHeaderNames = null; 100 private Set<String> blockedHeaderNames = new HashSet<>(); 101 102 // Cookie Security defaults 103 private int maxCookieCount = 20; 104 private int maxCookieNameLength = 128; 105 private int maxCookieValueLength = 2048; 106 private boolean requireSecureCookies = false; 107 private boolean requireHttpOnlyCookies = false; 108 109 // Body Security defaults 110 private long maxBodySize = 5L * 1024 * 1024; // 5MB 111 private @Nullable Set<String> allowedContentTypes = null; 112 private Set<String> blockedContentTypes = new HashSet<>(); 113 114 // Encoding Security defaults 115 private boolean allowNullBytes = false; 116 private boolean allowControlCharacters = false; 117 private boolean allowExtendedAscii = true; 118 private boolean normalizeUnicode = false; 119 120 // General Policy defaults 121 private boolean caseSensitiveComparison = false; 122 private boolean failOnSuspiciousPatterns = false; 123 private boolean logSecurityViolations = true; 124 125 /** 126 * Package-private constructor for internal use. 127 */ 128 SecurityConfigurationBuilder() { 129 // Initialize with default values already set above 130 } 131 132 // === Path Security Methods === 133 134 /** 135 * Sets the maximum allowed path length. 136 * 137 * @param maxLength Maximum path length in characters (must be positive) 138 * @return This builder for method chaining 139 * @throws IllegalArgumentException if maxLength is not positive 140 */ 141 public SecurityConfigurationBuilder maxPathLength(int maxLength) { 142 if (maxLength <= 0) { 143 throw new IllegalArgumentException("maxPathLength must be positive, got: " + maxLength); 144 } 145 this.maxPathLength = maxLength; 146 return this; 147 } 148 149 /** 150 * Sets whether path traversal patterns (../) are allowed. 151 * 152 * @param allow true to allow path traversal, false to block it 153 * @return This builder for method chaining 154 */ 155 public SecurityConfigurationBuilder allowPathTraversal(boolean allow) { 156 this.allowPathTraversal = allow; 157 return this; 158 } 159 160 /** 161 * Sets whether double URL encoding is allowed. 162 * 163 * @param allow true to allow double encoding, false to block it 164 * @return This builder for method chaining 165 */ 166 public SecurityConfigurationBuilder allowDoubleEncoding(boolean allow) { 167 this.allowDoubleEncoding = allow; 168 return this; 169 } 170 171 /** 172 * Configures path security settings in one call. 173 * 174 * @param maxLength Maximum path length 175 * @param allowTraversal Whether to allow path traversal 176 * @return This builder for method chaining 177 */ 178 public SecurityConfigurationBuilder pathSecurity(int maxLength, boolean allowTraversal) { 179 return maxPathLength(maxLength).allowPathTraversal(allowTraversal); 180 } 181 182 // === Parameter Security Methods === 183 184 /** 185 * Sets the maximum number of query parameters allowed. 186 * 187 * @param maxCount Maximum parameter count (must be non-negative) 188 * @return This builder for method chaining 189 * @throws IllegalArgumentException if maxCount is negative 190 */ 191 public SecurityConfigurationBuilder maxParameterCount(int maxCount) { 192 if (maxCount < 0) { 193 throw new IllegalArgumentException("maxParameterCount must be non-negative, got: " + maxCount); 194 } 195 this.maxParameterCount = maxCount; 196 return this; 197 } 198 199 /** 200 * Sets the maximum length for parameter names. 201 * 202 * @param maxLength Maximum name length (must be positive) 203 * @return This builder for method chaining 204 * @throws IllegalArgumentException if maxLength is not positive 205 */ 206 public SecurityConfigurationBuilder maxParameterNameLength(int maxLength) { 207 if (maxLength <= 0) { 208 throw new IllegalArgumentException("maxParameterNameLength must be positive, got: " + maxLength); 209 } 210 this.maxParameterNameLength = maxLength; 211 return this; 212 } 213 214 /** 215 * Sets the maximum length for parameter values. 216 * 217 * @param maxLength Maximum value length (must be positive) 218 * @return This builder for method chaining 219 * @throws IllegalArgumentException if maxLength is not positive 220 */ 221 public SecurityConfigurationBuilder maxParameterValueLength(int maxLength) { 222 if (maxLength <= 0) { 223 throw new IllegalArgumentException("maxParameterValueLength must be positive, got: " + maxLength); 224 } 225 this.maxParameterValueLength = maxLength; 226 return this; 227 } 228 229 /** 230 * Configures parameter security settings in one call. 231 * 232 * @param maxCount Maximum parameter count 233 * @param maxNameLength Maximum parameter name length 234 * @param maxValueLength Maximum parameter value length 235 * @return This builder for method chaining 236 */ 237 public SecurityConfigurationBuilder parameterSecurity(int maxCount, int maxNameLength, int maxValueLength) { 238 return maxParameterCount(maxCount) 239 .maxParameterNameLength(maxNameLength) 240 .maxParameterValueLength(maxValueLength); 241 } 242 243 // === Header Security Methods === 244 245 /** 246 * Sets the maximum number of HTTP headers allowed. 247 * 248 * @param maxCount Maximum header count (must be non-negative) 249 * @return This builder for method chaining 250 * @throws IllegalArgumentException if maxCount is negative 251 */ 252 public SecurityConfigurationBuilder maxHeaderCount(int maxCount) { 253 if (maxCount < 0) { 254 throw new IllegalArgumentException("maxHeaderCount must be non-negative, got: " + maxCount); 255 } 256 this.maxHeaderCount = maxCount; 257 return this; 258 } 259 260 /** 261 * Sets the maximum length for header names. 262 * 263 * @param maxLength Maximum name length (must be positive) 264 * @return This builder for method chaining 265 * @throws IllegalArgumentException if maxLength is not positive 266 */ 267 public SecurityConfigurationBuilder maxHeaderNameLength(int maxLength) { 268 if (maxLength <= 0) { 269 throw new IllegalArgumentException("maxHeaderNameLength must be positive, got: " + maxLength); 270 } 271 this.maxHeaderNameLength = maxLength; 272 return this; 273 } 274 275 /** 276 * Sets the maximum length for header values. 277 * 278 * @param maxLength Maximum value length (must be positive) 279 * @return This builder for method chaining 280 * @throws IllegalArgumentException if maxLength is not positive 281 */ 282 public SecurityConfigurationBuilder maxHeaderValueLength(int maxLength) { 283 if (maxLength <= 0) { 284 throw new IllegalArgumentException("maxHeaderValueLength must be positive, got: " + maxLength); 285 } 286 this.maxHeaderValueLength = maxLength; 287 return this; 288 } 289 290 /** 291 * Adds a header name to the allowed list. If the allowed list is null, 292 * this method initializes it with the given header name. 293 * 294 * @param headerName Header name to allow (must not be null) 295 * @return This builder for method chaining 296 * @throws NullPointerException if headerName is null 297 */ 298 public SecurityConfigurationBuilder addAllowedHeaderName(String headerName) { 299 Objects.requireNonNull(headerName, "headerName must not be null"); 300 if (allowedHeaderNames == null) { 301 allowedHeaderNames = new HashSet<>(); 302 } 303 allowedHeaderNames.add(headerName); 304 return this; 305 } 306 307 /** 308 * Sets the complete list of allowed header names. 309 * 310 * @param headerNames Set of allowed header names (null means all allowed) 311 * @return This builder for method chaining 312 */ 313 public SecurityConfigurationBuilder allowedHeaderNames(@Nullable Set<String> headerNames) { 314 this.allowedHeaderNames = headerNames != null ? new HashSet<>(headerNames) : null; 315 return this; 316 } 317 318 /** 319 * Adds a header name to the blocked list. 320 * 321 * @param headerName Header name to block (must not be null) 322 * @return This builder for method chaining 323 * @throws NullPointerException if headerName is null 324 */ 325 public SecurityConfigurationBuilder addBlockedHeaderName(String headerName) { 326 Objects.requireNonNull(headerName, "headerName must not be null"); 327 blockedHeaderNames.add(headerName); 328 return this; 329 } 330 331 /** 332 * Sets the complete list of blocked header names. 333 * 334 * @param headerNames Set of blocked header names (must not be null) 335 * @return This builder for method chaining 336 * @throws NullPointerException if headerNames is null 337 */ 338 public SecurityConfigurationBuilder blockedHeaderNames(Set<String> headerNames) { 339 Objects.requireNonNull(headerNames, "headerNames must not be null"); 340 this.blockedHeaderNames = new HashSet<>(headerNames); 341 return this; 342 } 343 344 /** 345 * Configures header security settings in one call. 346 * 347 * @param maxCount Maximum header count 348 * @param maxNameLength Maximum header name length 349 * @param maxValueLength Maximum header value length 350 * @return This builder for method chaining 351 */ 352 public SecurityConfigurationBuilder headerSecurity(int maxCount, int maxNameLength, int maxValueLength) { 353 return maxHeaderCount(maxCount) 354 .maxHeaderNameLength(maxNameLength) 355 .maxHeaderValueLength(maxValueLength); 356 } 357 358 // === Cookie Security Methods === 359 360 /** 361 * Sets the maximum number of cookies allowed. 362 * 363 * @param maxCount Maximum cookie count (must be non-negative) 364 * @return This builder for method chaining 365 * @throws IllegalArgumentException if maxCount is negative 366 */ 367 public SecurityConfigurationBuilder maxCookieCount(int maxCount) { 368 if (maxCount < 0) { 369 throw new IllegalArgumentException("maxCookieCount must be non-negative, got: " + maxCount); 370 } 371 this.maxCookieCount = maxCount; 372 return this; 373 } 374 375 /** 376 * Sets the maximum length for cookie names. 377 * 378 * @param maxLength Maximum name length (must be positive) 379 * @return This builder for method chaining 380 * @throws IllegalArgumentException if maxLength is not positive 381 */ 382 public SecurityConfigurationBuilder maxCookieNameLength(int maxLength) { 383 if (maxLength <= 0) { 384 throw new IllegalArgumentException("maxCookieNameLength must be positive, got: " + maxLength); 385 } 386 this.maxCookieNameLength = maxLength; 387 return this; 388 } 389 390 /** 391 * Sets the maximum length for cookie values. 392 * 393 * @param maxLength Maximum value length (must be positive) 394 * @return This builder for method chaining 395 * @throws IllegalArgumentException if maxLength is not positive 396 */ 397 public SecurityConfigurationBuilder maxCookieValueLength(int maxLength) { 398 if (maxLength <= 0) { 399 throw new IllegalArgumentException("maxCookieValueLength must be positive, got: " + maxLength); 400 } 401 this.maxCookieValueLength = maxLength; 402 return this; 403 } 404 405 /** 406 * Sets whether all cookies must have the Secure flag. 407 * 408 * @param require true to require Secure flag on all cookies 409 * @return This builder for method chaining 410 */ 411 public SecurityConfigurationBuilder requireSecureCookies(boolean require) { 412 this.requireSecureCookies = require; 413 return this; 414 } 415 416 /** 417 * Sets whether all cookies must have the HttpOnly flag. 418 * 419 * @param require true to require HttpOnly flag on all cookies 420 * @return This builder for method chaining 421 */ 422 public SecurityConfigurationBuilder requireHttpOnlyCookies(boolean require) { 423 this.requireHttpOnlyCookies = require; 424 return this; 425 } 426 427 /** 428 * Configures cookie security settings in one call. 429 * 430 * @param requireSecure Whether to require Secure flag 431 * @param requireHttpOnly Whether to require HttpOnly flag 432 * @param maxCount Maximum cookie count 433 * @param maxNameLength Maximum cookie name length 434 * @param maxValueLength Maximum cookie value length 435 * @return This builder for method chaining 436 */ 437 public SecurityConfigurationBuilder cookieSecurity(boolean requireSecure, boolean requireHttpOnly, 438 int maxCount, int maxNameLength, int maxValueLength) { 439 return requireSecureCookies(requireSecure) 440 .requireHttpOnlyCookies(requireHttpOnly) 441 .maxCookieCount(maxCount) 442 .maxCookieNameLength(maxNameLength) 443 .maxCookieValueLength(maxValueLength); 444 } 445 446 // === Body Security Methods === 447 448 /** 449 * Sets the maximum body size in bytes. 450 * 451 * @param maxSize Maximum body size (must be non-negative) 452 * @return This builder for method chaining 453 * @throws IllegalArgumentException if maxSize is negative 454 */ 455 public SecurityConfigurationBuilder maxBodySize(long maxSize) { 456 if (maxSize < 0) { 457 throw new IllegalArgumentException("maxBodySize must be non-negative, got: " + maxSize); 458 } 459 this.maxBodySize = maxSize; 460 return this; 461 } 462 463 /** 464 * Adds a content type to the allowed list. If the allowed list is null, 465 * this method initializes it with the given content type. 466 * 467 * @param contentType Content type to allow (must not be null) 468 * @return This builder for method chaining 469 * @throws NullPointerException if contentType is null 470 */ 471 public SecurityConfigurationBuilder addAllowedContentType(String contentType) { 472 Objects.requireNonNull(contentType, "contentType must not be null"); 473 if (allowedContentTypes == null) { 474 allowedContentTypes = new HashSet<>(); 475 } 476 allowedContentTypes.add(contentType); 477 return this; 478 } 479 480 /** 481 * Sets the complete list of allowed content types. 482 * 483 * @param contentTypes Set of allowed content types (null means all allowed) 484 * @return This builder for method chaining 485 */ 486 public SecurityConfigurationBuilder allowedContentTypes(@Nullable Set<String> contentTypes) { 487 this.allowedContentTypes = contentTypes != null ? new HashSet<>(contentTypes) : null; 488 return this; 489 } 490 491 /** 492 * Adds a content type to the blocked list. 493 * 494 * @param contentType Content type to block (must not be null) 495 * @return This builder for method chaining 496 * @throws NullPointerException if contentType is null 497 */ 498 public SecurityConfigurationBuilder addBlockedContentType(String contentType) { 499 Objects.requireNonNull(contentType, "contentType must not be null"); 500 blockedContentTypes.add(contentType); 501 return this; 502 } 503 504 /** 505 * Sets the complete list of blocked content types. 506 * 507 * @param contentTypes Set of blocked content types (must not be null) 508 * @return This builder for method chaining 509 * @throws NullPointerException if contentTypes is null 510 */ 511 public SecurityConfigurationBuilder blockedContentTypes(Set<String> contentTypes) { 512 Objects.requireNonNull(contentTypes, "contentTypes must not be null"); 513 this.blockedContentTypes = new HashSet<>(contentTypes); 514 return this; 515 } 516 517 /** 518 * Configures body security settings in one call. 519 * 520 * @param maxSize Maximum body size 521 * @param allowedTypes Set of allowed content types (null = all allowed) 522 * @return This builder for method chaining 523 */ 524 public SecurityConfigurationBuilder bodySecurity(long maxSize, @Nullable Set<String> allowedTypes) { 525 return maxBodySize(maxSize).allowedContentTypes(allowedTypes); 526 } 527 528 // === Encoding Security Methods === 529 530 /** 531 * Sets whether null bytes are allowed in content. 532 * 533 * @param allow true to allow null bytes, false to block them 534 * @return This builder for method chaining 535 */ 536 public SecurityConfigurationBuilder allowNullBytes(boolean allow) { 537 this.allowNullBytes = allow; 538 return this; 539 } 540 541 /** 542 * Sets whether control characters are allowed in content. 543 * 544 * @param allow true to allow control characters, false to block them 545 * @return This builder for method chaining 546 */ 547 public SecurityConfigurationBuilder allowControlCharacters(boolean allow) { 548 this.allowControlCharacters = allow; 549 return this; 550 } 551 552 /** 553 * Sets whether extended ASCII characters (128-255) are allowed in content. 554 * For URL paths and parameters, this only affects characters 128-255. 555 * For headers and body content, this also enables Unicode support. 556 * 557 * @param allow true to allow extended ASCII and applicable Unicode characters, false to block them 558 * @return This builder for method chaining 559 */ 560 public SecurityConfigurationBuilder allowExtendedAscii(boolean allow) { 561 this.allowExtendedAscii = allow; 562 return this; 563 } 564 565 566 /** 567 * Sets whether Unicode normalization should be performed. 568 * 569 * @param normalize true to normalize Unicode, false to leave as-is 570 * @return This builder for method chaining 571 */ 572 public SecurityConfigurationBuilder normalizeUnicode(boolean normalize) { 573 this.normalizeUnicode = normalize; 574 return this; 575 } 576 577 /** 578 * Configures encoding security settings in one call. 579 * 580 * @param allowNulls Whether to allow null bytes 581 * @param allowControls Whether to allow control characters 582 * @param allowHighBit Whether to allow high-bit characters 583 * @param normalizeUni Whether to normalize Unicode 584 * @return This builder for method chaining 585 */ 586 public SecurityConfigurationBuilder encoding(boolean allowNulls, boolean allowControls, 587 boolean allowHighBit, boolean normalizeUni) { 588 return allowNullBytes(allowNulls) 589 .allowControlCharacters(allowControls) 590 .allowExtendedAscii(allowHighBit) 591 .normalizeUnicode(normalizeUni); 592 } 593 594 // === General Policy Methods === 595 596 /** 597 * Sets whether string comparisons should be case-sensitive. 598 * 599 * @param caseSensitive true for case-sensitive comparisons 600 * @return This builder for method chaining 601 */ 602 public SecurityConfigurationBuilder caseSensitiveComparison(boolean caseSensitive) { 603 this.caseSensitiveComparison = caseSensitive; 604 return this; 605 } 606 607 /** 608 * Sets whether to fail on detection of suspicious patterns. 609 * 610 * @param fail true to fail on suspicious patterns, false to log only 611 * @return This builder for method chaining 612 */ 613 public SecurityConfigurationBuilder failOnSuspiciousPatterns(boolean fail) { 614 this.failOnSuspiciousPatterns = fail; 615 return this; 616 } 617 618 /** 619 * Sets whether to log security violations. 620 * 621 * @param log true to enable logging, false to disable 622 * @return This builder for method chaining 623 */ 624 public SecurityConfigurationBuilder logSecurityViolations(boolean log) { 625 this.logSecurityViolations = log; 626 return this; 627 } 628 629 /** 630 * Configures general policy settings in one call. 631 * 632 * @param caseSensitive Whether comparisons are case-sensitive 633 * @param failOnSuspicious Whether to fail on suspicious patterns 634 * @param logViolations Whether to log security violations 635 * @return This builder for method chaining 636 */ 637 public SecurityConfigurationBuilder policies(boolean caseSensitive, boolean failOnSuspicious, boolean logViolations) { 638 return caseSensitiveComparison(caseSensitive) 639 .failOnSuspiciousPatterns(failOnSuspicious) 640 .logSecurityViolations(logViolations); 641 } 642 643 /** 644 * Builds the SecurityConfiguration with the current settings. 645 * 646 * @return A new immutable SecurityConfiguration instance 647 * @throws IllegalArgumentException if any configuration values are invalid 648 */ 649 public SecurityConfiguration build() { 650 return new SecurityConfiguration( 651 maxPathLength, allowPathTraversal, allowDoubleEncoding, 652 maxParameterCount, maxParameterNameLength, maxParameterValueLength, 653 maxHeaderCount, maxHeaderNameLength, maxHeaderValueLength, allowedHeaderNames, blockedHeaderNames, 654 maxCookieCount, maxCookieNameLength, maxCookieValueLength, requireSecureCookies, requireHttpOnlyCookies, 655 maxBodySize, allowedContentTypes, blockedContentTypes, 656 allowNullBytes, allowControlCharacters, allowExtendedAscii, normalizeUnicode, 657 caseSensitiveComparison, failOnSuspiciousPatterns, logSecurityViolations 658 ); 659 } 660}