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.core; 017 018import de.cuioss.http.security.exceptions.UrlSecurityException; 019import org.jspecify.annotations.Nullable; 020 021import java.util.Optional; 022import java.util.function.Predicate; 023 024/** 025 * Core functional interface for HTTP security validation. 026 * 027 * <p>This interface defines the contract for validating HTTP components against security threats. 028 * It follows the "String in, String out, throws on violation" pattern consistently across all 029 * implementations, enabling clean functional programming patterns and easy composition.</p> 030 * 031 * <h3>Design Principles</h3> 032 * <ul> 033 * <li><strong>Functional Interface</strong> - Can be used with lambda expressions and method references</li> 034 * <li><strong>Fail Secure</strong> - Throws UrlSecurityException on any security violation</li> 035 * <li><strong>String/throws Pattern</strong> - Simple contract: input string, output string, throws on failure</li> 036 * <li><strong>Composable</strong> - Multiple validators can be chained or combined</li> 037 * <li><strong>Thread Safe</strong> - Implementations should be thread-safe for concurrent use</li> 038 * </ul> 039 * 040 * <h3>Usage Examples</h3> 041 * <pre> 042 * // Simple validation 043 * HttpSecurityValidator pathValidator = input -> { 044 * if (input.contains("../")) { 045 * throw UrlSecurityException.builder() 046 * .failureType(UrlSecurityFailureType.PATH_TRAVERSAL_DETECTED) 047 * .validationType(ValidationType.URL_PATH) 048 * .originalInput(input) 049 * .build(); 050 * } 051 * return input; 052 * }; 053 * 054 * // Using with streams 055 * List<String> validPaths = paths.stream() 056 * .map(pathValidator::validate) 057 * .collect(Collectors.toList()); 058 * 059 * // Composing validators 060 * HttpSecurityValidator compositeValidator = input -> 061 * secondValidator.validate(firstValidator.validate(input)); 062 * </pre> 063 * 064 * <h3>Implementation Guidelines</h3> 065 * <ul> 066 * <li>Always validate the most dangerous patterns first</li> 067 * <li>Provide clear, actionable error messages in exceptions</li> 068 * <li>Consider performance - validators may be called frequently</li> 069 * <li>Be consistent with null handling (typically reject null inputs)</li> 070 * <li>Log security violations appropriately without exposing sensitive data</li> 071 * </ul> 072 * 073 * Implements: Task B3 from HTTP verification specification 074 * 075 * @since 1.0 076 * @see UrlSecurityException 077 * @see ValidationType 078 */ 079@FunctionalInterface 080public interface HttpSecurityValidator { 081 082 /** 083 * Validates the input string and returns the sanitized/normalized version. 084 * 085 * <p>This method should examine the input for security violations and either: 086 * <ul> 087 * <li>Return the input unchanged if it's safe</li> 088 * <li>Return a sanitized/normalized version if safe transformations are possible</li> 089 * <li>Return Optional.empty() if the input was null</li> 090 * <li>Throw UrlSecurityException if the input represents a security threat</li> 091 * </ul> 092 * 093 * <p>The decision between sanitization and rejection depends on the specific validator 094 * and security requirements. Critical security validators should prefer rejection 095 * over sanitization to avoid bypasses.</p> 096 * 097 * @param value The input to validate. May be null. 098 * @return The validated, potentially sanitized or normalized value wrapped in Optional. 099 * Returns Optional.empty() if the input was null. 100 * @throws UrlSecurityException If the input represents a security violation that cannot 101 * be safely sanitized. The exception should include detailed context about the 102 * failure for logging and debugging purposes. 103 * @throws IllegalArgumentException If the input is malformed in a way that prevents 104 * security analysis (distinct from security violations). 105 */ 106 Optional<String> validate(@Nullable String value) throws UrlSecurityException; 107 108 /** 109 * Creates a composite validator that applies this validator followed by the given validator. 110 * 111 * <p>This is a convenience method for chaining validators. The resulting validator will: 112 * <ol> 113 * <li>Apply this validator to the input</li> 114 * <li>Apply the {@code after} validator to the result</li> 115 * <li>Return the final result</li> 116 * </ol> 117 * 118 * <p>If either validator throws an exception, the composition stops and the exception 119 * is propagated.</p> 120 * 121 * @param after The validator to apply after this validator 122 * @return A composite validator that applies both validators in sequence 123 * @throws NullPointerException if {@code after} is null 124 * @since 1.0 125 */ 126 @SuppressWarnings({"ConstantConditions", "java:S2583"}) 127 default HttpSecurityValidator andThen(HttpSecurityValidator after) { 128 // Explicit null check despite @NullMarked for fail-fast behavior and clear error messages 129 if (after == null) { 130 throw new NullPointerException("after validator must not be null"); 131 } 132 return value -> this.validate(value).flatMap(after::validate); 133 } 134 135 /** 136 * Creates a composite validator that applies the given validator followed by this validator. 137 * 138 * <p>This is a convenience method for chaining validators. The resulting validator will: 139 * <ol> 140 * <li>Apply the {@code before} validator to the input</li> 141 * <li>Apply this validator to the result</li> 142 * <li>Return the final result</li> 143 * </ol> 144 * 145 * @param before The validator to apply before this validator 146 * @return A composite validator that applies both validators in sequence 147 * @throws NullPointerException if {@code before} is null 148 * @since 1.0 149 */ 150 @SuppressWarnings({"ConstantConditions", "java:S2583"}) 151 default HttpSecurityValidator compose(HttpSecurityValidator before) { 152 // Explicit null check despite @NullMarked for fail-fast behavior and clear error messages 153 if (before == null) { 154 throw new NullPointerException("before validator must not be null"); 155 } 156 return value -> before.validate(value).flatMap(this::validate); 157 } 158 159 /** 160 * Creates a validator that applies this validator only if the given predicate is true. 161 * If the predicate is false, the input is returned unchanged. 162 * 163 * <p>This is useful for conditional validation based on input characteristics:</p> 164 * <pre> 165 * // Only validate non-empty strings 166 * HttpSecurityValidator conditionalValidator = validator.when(s -> s != null && !s.isEmpty()); 167 * 168 * // Only validate strings that look like URLs 169 * HttpSecurityValidator urlValidator = validator.when(s -> s.startsWith("http")); 170 * </pre> 171 * 172 * @param predicate The condition under which to apply this validator 173 * @return A conditional validator that only applies this validator when the predicate is true 174 * @throws NullPointerException if {@code predicate} is null 175 * @since 1.0 176 */ 177 @SuppressWarnings({"ConstantConditions", "java:S2583"}) 178 default HttpSecurityValidator when(Predicate<String> predicate) { 179 // Explicit null check despite @NullMarked for fail-fast behavior and clear error messages 180 if (predicate == null) { 181 throw new NullPointerException("predicate must not be null"); 182 } 183 return value -> { 184 if (value == null || !predicate.test(value)) { 185 return Optional.ofNullable(value); 186 } 187 return this.validate(value); 188 }; 189 } 190 191 /** 192 * Creates an identity validator that always returns the input unchanged. 193 * This is useful as a no-op validator or as a starting point for composition. 194 * 195 * @return An identity validator that performs no validation 196 * @since 1.0 197 */ 198 static HttpSecurityValidator identity() { 199 return Optional::ofNullable; 200 } 201 202 /** 203 * Creates a validator that always rejects input with the specified failure type. 204 * This is useful for creating validators that unconditionally block certain inputs. 205 * 206 * @param failureType The failure type to use in the exception 207 * @param validationType The validation type context 208 * @return A validator that always throws UrlSecurityException 209 * @throws NullPointerException if either parameter is null 210 * @since 1.0 211 */ 212 @SuppressWarnings({"ConstantConditions", "java:S2583"}) 213 static HttpSecurityValidator reject(UrlSecurityFailureType failureType, ValidationType validationType) { 214 // Explicit null checks despite @NullMarked for fail-fast behavior and clear error messages 215 if (failureType == null) { 216 throw new NullPointerException("failureType must not be null"); 217 } 218 if (validationType == null) { 219 throw new NullPointerException("validationType must not be null"); 220 } 221 return value -> { 222 throw UrlSecurityException.builder() 223 .failureType(failureType) 224 .validationType(validationType) 225 .originalInput(value != null ? value : "null") 226 .detail("Input unconditionally rejected") 227 .build(); 228 }; 229 } 230}