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.camel.util; 018 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Locale; 024import java.util.Map; 025import java.util.Set; 026import java.util.function.BiPredicate; 027import java.util.function.UnaryOperator; 028 029/** 030 * Utility for detecting insecure configuration options. 031 * <p> 032 * The security options map is generated by camel build tools from {@code @Metadata(security=...)} and 033 * {@code @UriParam(security=...)} annotations. 034 * 035 * @since 4.19.0 036 */ 037public final class SecurityUtils { 038 039 public static final String INSECURE_SSL = "insecure:ssl"; 040 public static final String INSECURE_SERIALIZATION = "insecure:serialization"; 041 public static final String INSECURE_DEV = "insecure:dev"; 042 043 private static final String VALUE_FALSE = "false"; 044 045 /** 046 * Information about a security-sensitive configuration option. 047 * 048 * @param category the security category (e.g., "insecure:ssl", "insecure:serialization", "insecure:dev") 049 * @param insecureValue the value that makes this option insecure (e.g., "true" for boolean flags) 050 */ 051 public record SecurityOption(String category, String insecureValue) { 052 } 053 054 private static final Map<String, SecurityOption> SECURITY_OPTIONS; 055 056 static { 057 Map<String, SecurityOption> map = new HashMap<>(); 058 // Generated by camel build tools - do NOT edit this map! 059 // SECURITY-OPTIONS: START 060 map.put("allowjavaserializedobject", new SecurityOption(INSECURE_SERIALIZATION, "true")); 061 map.put("allowlocalwebhookurls", new SecurityOption(INSECURE_DEV, "true")); 062 map.put("allowserializedheaders", new SecurityOption(INSECURE_SERIALIZATION, "true")); 063 map.put("devconsoleenabled", new SecurityOption(INSECURE_DEV, "true")); 064 map.put("downloadenabled", new SecurityOption(INSECURE_DEV, "true")); 065 map.put("hostnameverification", new SecurityOption(INSECURE_SSL, VALUE_FALSE)); 066 map.put("httpshostnameverificationenabled", new SecurityOption(INSECURE_SSL, VALUE_FALSE)); 067 map.put("ignoresslverification", new SecurityOption(INSECURE_SSL, "true")); 068 map.put("objectmessageenabled", new SecurityOption(INSECURE_SERIALIZATION, "true")); 069 map.put("sendenabled", new SecurityOption(INSECURE_DEV, "true")); 070 map.put("skiptlsverify", new SecurityOption(INSECURE_SSL, "true")); 071 map.put("stricthostkeychecking", new SecurityOption(INSECURE_SSL, "")); 072 map.put("transferexception", new SecurityOption(INSECURE_SERIALIZATION, "true")); 073 map.put("transferexchange", new SecurityOption(INSECURE_SERIALIZATION, "true")); 074 map.put("trustallcertificates", new SecurityOption(INSECURE_SSL, "true")); 075 map.put("trustallpackages", new SecurityOption(INSECURE_SERIALIZATION, "true")); 076 map.put("uploadenabled", new SecurityOption(INSECURE_DEV, "true")); 077 map.put("usejavamailsessionpropertiesfromheaders", new SecurityOption(INSECURE_SSL, "true")); 078 map.put("validateauth", new SecurityOption(INSECURE_DEV, VALUE_FALSE)); 079 map.put("validatecertificates", new SecurityOption(INSECURE_SSL, VALUE_FALSE)); 080 map.put("x509hostnameverifier", new SecurityOption(INSECURE_SSL, "")); 081 // SECURITY-OPTIONS: END 082 SECURITY_OPTIONS = Collections.unmodifiableMap(map); 083 } 084 085 private SecurityUtils() { 086 } 087 088 /** 089 * All the security options (unmodifiable) 090 */ 091 public static Map<String, SecurityOption> getSecurityOptions() { 092 return SECURITY_OPTIONS; 093 } 094 095 /** 096 * Get security information for a configuration property. 097 * 098 * @param text the configuration property key (e.g., "camel.component.http.trustAllCertificates") 099 * @return the security option info, or null if the property has no security category 100 */ 101 public static SecurityOption getSecurityOption(String text) { 102 int lastPeriod = text.lastIndexOf('.'); 103 if (lastPeriod >= 0) { 104 text = text.substring(lastPeriod + 1); 105 } 106 text = text.toLowerCase(Locale.ENGLISH); 107 text = text.replace("-", ""); 108 return SECURITY_OPTIONS.get(text); 109 } 110 111 /** 112 * Whether the given configuration property value is the insecure value for the property. 113 * 114 * @param text the configuration property key 115 * @param value the property value 116 * @return true if the value is the insecure value for this property, false otherwise 117 */ 118 public static boolean isInsecureValue(String text, Object value) { 119 SecurityOption option = getSecurityOption(text); 120 if (option == null) { 121 return false; 122 } 123 return String.valueOf(value).equalsIgnoreCase(option.insecureValue()); 124 } 125 126 /** 127 * Whether the given property value appears to be a plain-text secret (not using vault, env var, or system property 128 * placeholders). 129 * 130 * @param value the property value to check 131 * @return true if the value is plain text (not secured via a placeholder mechanism) 132 */ 133 public static boolean isPlainTextSecret(Object value) { 134 if (value == null) { 135 return false; 136 } 137 String v = value.toString(); 138 if (v.isEmpty()) { 139 return false; 140 } 141 // check for known secure value patterns 142 // note: RAW() is a URI encoding wrapper, not a security mechanism — RAW(password) is still plain text 143 if (v.startsWith("{{") && v.contains("vault:")) { 144 return false; 145 } 146 if (v.startsWith("${env:") || v.startsWith("${ENV:")) { 147 return false; 148 } 149 if (v.startsWith("${sys:") || v.startsWith("${SYS:")) { 150 return false; 151 } 152 // property placeholder - could be pointing to a secure source 153 return !(v.startsWith("{{") && v.endsWith("}}")); 154 } 155 156 /** 157 * Detect security policy violations in a set of configuration properties. 158 * <p> 159 * This method checks for both plain-text secrets and insecure configuration options based on the security options 160 * map and the provided policy resolver. 161 * 162 * @param properties map of property key → value pairs to check 163 * @param secretDetector predicate that returns true if a property key+value pair represents a sensitive secret 164 * @param policyResolver function that resolves the effective policy ("allow", "warn", "fail") for a given security 165 * category 166 * @param allowedKeys set of property keys to skip (allowed explicitly by the user) 167 * @return list of violations found (may be empty) 168 */ 169 public static List<SecurityViolation> detectViolations( 170 Map<String, Object> properties, 171 BiPredicate<String, Object> secretDetector, 172 UnaryOperator<String> policyResolver, 173 Set<String> allowedKeys) { 174 175 List<SecurityViolation> violations = new ArrayList<>(); 176 177 for (Map.Entry<String, Object> entry : properties.entrySet()) { 178 String k = entry.getKey(); 179 Object v = entry.getValue(); 180 181 if (shouldSkip(k, allowedKeys)) { 182 continue; 183 } 184 185 checkPlainTextSecret(k, v, secretDetector, policyResolver, violations); 186 checkInsecureOption(k, v, policyResolver, violations); 187 } 188 189 return violations; 190 } 191 192 private static boolean shouldSkip(String key, Set<String> allowedKeys) { 193 return key.startsWith("camel.security.") 194 || (allowedKeys != null && allowedKeys.contains(key)); 195 } 196 197 private static void checkPlainTextSecret( 198 String key, Object value, 199 BiPredicate<String, Object> secretDetector, 200 UnaryOperator<String> policyResolver, 201 List<SecurityViolation> violations) { 202 203 if (secretDetector.test(key, value) && isPlainTextSecret(value)) { 204 String effectivePolicy = policyResolver.apply("secret"); 205 if (!"allow".equals(effectivePolicy)) { 206 violations.add(new SecurityViolation( 207 "secret", key, 208 "Secret property is configured with plain-text value." 209 + " Consider using {{vault:...}}, ${env:...}, or {{...}} property placeholders", 210 effectivePolicy)); 211 } 212 } 213 } 214 215 private static void checkInsecureOption( 216 String key, Object value, 217 UnaryOperator<String> policyResolver, 218 List<SecurityViolation> violations) { 219 220 SecurityOption secOption = getSecurityOption(key); 221 if (secOption != null && isInsecureValue(key, value)) { 222 String effectivePolicy = policyResolver.apply(secOption.category()); 223 if (!"allow".equals(effectivePolicy)) { 224 violations.add(new SecurityViolation( 225 secOption.category(), key, 226 "Insecure configuration detected (category: " + secOption.category() + ")", 227 effectivePolicy)); 228 } 229 } 230 } 231}