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}