From 43e900d4a5c8266721c87ada1e5ea71a7062cd12 Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Thu, 6 Aug 2026 12:55:48 -0400 Subject: [PATCH 1/2] fix(security): obfuscate system config overrides in JVM info endpoint (#36919) getDBOverrides() copied SystemTable.all() into the /api/v1/jvm response unmasked, while the system-properties and environment sections already run values through obfuscateIfNeeded(). Apply the same pattern-based masking (passw|pass|passwd|secret|key|token + OBFUSCATE_SYSTEM_ENVIRONMENTAL_VARIABLES) to config overrides shown in the maintenance portlet. Co-Authored-By: Claude Fable 5 --- .../com/dotcms/rest/api/v1/maintenance/JVMInfoResource.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/maintenance/JVMInfoResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/maintenance/JVMInfoResource.java index 1eb101fa1e66..95f6ab786f60 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/maintenance/JVMInfoResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/maintenance/JVMInfoResource.java @@ -116,10 +116,8 @@ private Map getDBOverrides(){ SystemTable systemTable =APILocator.getSystemAPI().getSystemTable(); - resultMap.putAll(systemTable.all()); - - - + systemTable.all().forEach((key, value) -> + resultMap.put(key, obfuscateIfNeeded(key, value))); return resultMap; } From ea5c5d03e1733d05a6e6c60a472523358f164e27 Mon Sep 17 00:00:00 2001 From: Will Ezell Date: Thu, 6 Aug 2026 13:36:30 -0400 Subject: [PATCH 2/2] refactor(security): centralize config obfuscation rules in ObfuscationUtil (#36923) The masking pattern lived as statics on JVMInfoResource (a JAX-RS resource) and was reached into by SystemTableImpl (business layer) and ConfigurationResource. Move the default pattern, the OBFUSCATE_SYSTEM_ENVIRONMENTAL_VARIABLES custom pattern, and the masking helpers into com.dotcms.util.ObfuscationUtil. JVMInfoResource keeps its public members as @Deprecated delegates for plugin back-compat; in-repo callers now use the utility. No behavior change. Co-Authored-By: Claude Fable 5 --- .../com/dotcms/business/SystemTableImpl.java | 4 +- .../api/v1/maintenance/JVMInfoResource.java | 34 ++++----- .../api/v1/system/ConfigurationResource.java | 4 +- .../java/com/dotcms/util/ObfuscationUtil.java | 74 +++++++++++++++++++ .../v1/system/ConfigurationResourceTest.java | 4 +- 5 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/util/ObfuscationUtil.java diff --git a/dotCMS/src/main/java/com/dotcms/business/SystemTableImpl.java b/dotCMS/src/main/java/com/dotcms/business/SystemTableImpl.java index 0d19174efe69..8bd09dc4ae37 100644 --- a/dotCMS/src/main/java/com/dotcms/business/SystemTableImpl.java +++ b/dotCMS/src/main/java/com/dotcms/business/SystemTableImpl.java @@ -2,7 +2,7 @@ import com.dotcms.api.system.event.Payload; import com.dotcms.api.system.event.SystemEventType; -import com.dotcms.rest.api.v1.maintenance.JVMInfoResource; +import com.dotcms.util.ObfuscationUtil; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.FactoryLocator; import com.dotmarketing.db.HibernateUtil; @@ -69,7 +69,7 @@ public Map all() { @WrapInTransaction public void set(final String key, final String value) { - SecurityLogger.logInfo(this.getClass(), "Saving system table value for key:" + key + "=" + JVMInfoResource.obfuscateIfNeeded( + SecurityLogger.logInfo(this.getClass(), "Saving system table value for key:" + key + "=" + ObfuscationUtil.obfuscateIfNeeded( key,value)); Try.run(()-> this.systemTableFactory.saveOrUpdate(key, value)) diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/maintenance/JVMInfoResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/maintenance/JVMInfoResource.java index 95f6ab786f60..75e31d61ebf5 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/maintenance/JVMInfoResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/maintenance/JVMInfoResource.java @@ -2,6 +2,7 @@ import com.dotcms.business.SystemTable; import com.dotcms.rest.InitDataObject; +import com.dotcms.util.ObfuscationUtil; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; import com.dotmarketing.business.APILocator; @@ -37,14 +38,13 @@ @SuppressWarnings("serial") public class JVMInfoResource implements Serializable { - private static final String DEFAULT_OBFUSCATE_PATTERN = "passw|pass|passwd|secret|key|token"; + /** @deprecated use {@link ObfuscationUtil#BASE_PATTERN} */ + @Deprecated + public static final Pattern obfuscateBasePattern = ObfuscationUtil.BASE_PATTERN; - public static final Pattern obfuscateBasePattern = Pattern.compile(DEFAULT_OBFUSCATE_PATTERN, - Pattern.CASE_INSENSITIVE); - - public static final Pattern obfuscatePattern = Pattern.compile( - Config.getStringProperty("OBFUSCATE_SYSTEM_ENVIRONMENTAL_VARIABLES", DEFAULT_OBFUSCATE_PATTERN), - Pattern.CASE_INSENSITIVE); + /** @deprecated use {@link ObfuscationUtil#CUSTOM_PATTERN} */ + @Deprecated + public static final Pattern obfuscatePattern = ObfuscationUtil.CUSTOM_PATTERN; @Path("/") @GET @@ -117,7 +117,7 @@ private Map getDBOverrides(){ SystemTable systemTable =APILocator.getSystemAPI().getSystemTable(); systemTable.all().forEach((key, value) -> - resultMap.put(key, obfuscateIfNeeded(key, value))); + resultMap.put(key, ObfuscationUtil.obfuscateIfNeeded(key, value))); return resultMap; } @@ -153,7 +153,7 @@ private Map getSystemProps(){ Properties props = System.getProperties(); for(Object keyObject : props.keySet()) { final String key = (String) keyObject; - resultMap.put(key, obfuscateIfNeeded(key,props.getProperty(key))); + resultMap.put(key, ObfuscationUtil.obfuscateIfNeeded(key, props.getProperty(key))); } return resultMap; @@ -166,7 +166,7 @@ private Map getEnvironmentalVars(){ final Map resultMap=new LinkedHashMap<>(); Map vars = System.getenv(); for(String key : vars.keySet()) { - resultMap.put(key, obfuscateIfNeeded(key,vars.get(key))); + resultMap.put(key, ObfuscationUtil.obfuscateIfNeeded(key, vars.get(key))); } return resultMap; @@ -189,18 +189,10 @@ private Map getReleaseInfo(){ + /** @deprecated use {@link ObfuscationUtil#obfuscateIfNeeded(String, Object)} */ + @Deprecated public static String obfuscateIfNeeded(final String key, final Object valueObject) { - final String value = (String) valueObject; - if(UtilMethods.isEmpty(value)) return ""; - return obfuscateBasePattern.matcher(key).find() || obfuscatePattern.matcher(key).find() - ? obfuscate(value) - : value; - } - - private static String obfuscate(final String value) { - return value.charAt(0) - + "*********" - + value.charAt(value.length() - 1); + return ObfuscationUtil.obfuscateIfNeeded(key, valueObject); } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java index ef5af4f764fa..8ac05437ae88 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/ConfigurationResource.java @@ -5,7 +5,7 @@ import com.dotcms.featureflag.FeatureFlagName; import com.dotcms.rest.InitDataObject; import com.dotcms.rest.WebResource.InitBuilder; -import com.dotcms.rest.api.v1.maintenance.JVMInfoResource; +import com.dotcms.util.ObfuscationUtil; import com.dotcms.rest.api.v1.pagescanner.PageScannerResource; import com.dotmarketing.util.StringUtils; import com.google.common.collect.ImmutableSet; @@ -115,7 +115,7 @@ public class ConfigurationResource implements Serializable { FeatureFlagName.IMAGE_API_USE_LIBVIPS })); private boolean isOnBlackList(final String key) { - return null != JVMInfoResource.obfuscatePattern ? JVMInfoResource.obfuscatePattern.matcher(key).find() : false; + return ObfuscationUtil.matchesCustomPattern(key); } /** diff --git a/dotCMS/src/main/java/com/dotcms/util/ObfuscationUtil.java b/dotCMS/src/main/java/com/dotcms/util/ObfuscationUtil.java new file mode 100644 index 000000000000..a535da29845f --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/util/ObfuscationUtil.java @@ -0,0 +1,74 @@ +package com.dotcms.util; + +import com.dotmarketing.util.Config; +import com.dotmarketing.util.UtilMethods; +import java.util.regex.Pattern; + +/** + * Central place for the key-name based obfuscation rules used when rendering potentially + * sensitive configuration to users — the JVM info screen ({@code /api/v1/jvm}), the + * configuration REST endpoint, security logging, etc. + *

+ * A value is masked when its key matches either the built-in {@link #DEFAULT_OBFUSCATE_PATTERN} + * or the deployment-specific pattern configured via the + * {@code OBFUSCATE_SYSTEM_ENVIRONMENTAL_VARIABLES} property. + */ +public final class ObfuscationUtil { + + public static final String DEFAULT_OBFUSCATE_PATTERN = "passw|pass|passwd|secret|key|token"; + + /** + * The always-on rules — not overridable, so a misconfigured custom pattern can never + * un-mask the obvious cases. + */ + public static final Pattern BASE_PATTERN = + Pattern.compile(DEFAULT_OBFUSCATE_PATTERN, Pattern.CASE_INSENSITIVE); + + /** + * Deployment-specific rules from {@code OBFUSCATE_SYSTEM_ENVIRONMENTAL_VARIABLES}; + * defaults to the same expression as {@link #BASE_PATTERN}. + */ + public static final Pattern CUSTOM_PATTERN = Pattern.compile( + Config.getStringProperty("OBFUSCATE_SYSTEM_ENVIRONMENTAL_VARIABLES", + DEFAULT_OBFUSCATE_PATTERN), + Pattern.CASE_INSENSITIVE); + + private ObfuscationUtil() { + } + + /** + * @return true when the key matches the built-in or the configured custom pattern + */ + public static boolean shouldObfuscate(final String key) { + return BASE_PATTERN.matcher(key).find() || CUSTOM_PATTERN.matcher(key).find(); + } + + /** + * @return true when the key matches only the configured custom pattern (used by callers + * that intentionally honor just the {@code OBFUSCATE_SYSTEM_ENVIRONMENTAL_VARIABLES} rules) + */ + public static boolean matchesCustomPattern(final String key) { + return CUSTOM_PATTERN.matcher(key).find(); + } + + /** + * Masks the value when the key matches the obfuscation rules; returns it untouched otherwise. + */ + public static String obfuscateIfNeeded(final String key, final Object valueObject) { + final String value = (String) valueObject; + if (UtilMethods.isEmpty(value)) { + return ""; + } + return shouldObfuscate(key) ? obfuscate(value) : value; + } + + /** + * Masks a value keeping only its first and last character, e.g. {@code s*********t}. + */ + public static String obfuscate(final String value) { + return value.charAt(0) + + "*********" + + value.charAt(value.length() - 1); + } + +} diff --git a/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java index a35acb808003..ff7bbc0eb756 100644 --- a/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java +++ b/dotCMS/src/test/java/com/dotcms/rest/api/v1/system/ConfigurationResourceTest.java @@ -54,9 +54,9 @@ public class ConfigurationResourceTest { void setUp() { // Open the Config mock BEFORE constructing ConfigurationResource so that // any static initializer that calls Config (WHITE_LIST, and - // JVMInfoResource.obfuscatePattern via isOnBlackList) receives a safe + // ObfuscationUtil.CUSTOM_PATTERN via isOnBlackList) receives a safe // default instead of null. Without this, Pattern.compile(null) inside - // JVMInfoResource throws NullPointerException on first class load. + // ObfuscationUtil throws NullPointerException on first class load. // // Default answer: return the caller-supplied default (second argument) // for any getStringProperty / getStringArrayProperty call that is not