This is an automated email from the ASF dual-hosted git repository.
ffang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new cb0fc82057 [CXF-9164]An example to demonstrate how to extend
MaskSensitiveHelper and mask sensitive xml attributes
cb0fc82057 is described below
commit cb0fc820574af0c0a707fd84a49dad1e083f283e
Author: Freeman Fang <[email protected]>
AuthorDate: Wed Aug 27 16:20:57 2025 -0400
[CXF-9164]An example to demonstrate how to extend MaskSensitiveHelper and
mask sensitive xml attributes
---
.../cxf/ext/logging/AttributeMaskingHelper.java | 116 ++++++++++
.../logging/AttributeMaskSensitiveHelperTest.java | 257 +++++++++++++++++++++
2 files changed, 373 insertions(+)
diff --git
a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AttributeMaskingHelper.java
b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AttributeMaskingHelper.java
new file mode 100644
index 0000000000..742659f9b8
--- /dev/null
+++
b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AttributeMaskingHelper.java
@@ -0,0 +1,116 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.ext.logging;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.apache.cxf.message.Message;
+
+
+/**
+ * Adds XML/HTML attribute value masking on top of the parent
MaskSensitiveHelper.
+ *
+ */
+public class AttributeMaskingHelper extends MaskSensitiveHelper {
+
+ private static final String ATTR_NAME_TEMPLATE = "-ATTR_NAME-";
+
+ // Re-declare namespace prefix class per Namespaces in XML (private in
parent; reproduce here)
+ private static final String PATTERN_XML_NAMESPACE_PREFIX =
"[\\w.\\-\\u00B7\\u00C0-\\u00D6\\u00D8-\\u00F6"
+ +
"\\u00F8-\\u02FF\\u0300-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u203F-\\u2040\\u2070-\\u218F"
+ + "\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD]+";
+
+ // Case-sensitive attribute pattern; supports optional namespace prefix;
preserves original quotes
+ // Groups: 1=full attr name (w/ optional prefix), 2=open quote, 3=value,
4=close quote (backref to 2)
+ private static final String MATCH_PATTERN_XML_ATTR_TEMPLATE =
+ "(\\b(?:" + PATTERN_XML_NAMESPACE_PREFIX + ":)?" +
ATTR_NAME_TEMPLATE + ")\\s*=\\s*(\"|')(.*?)(\\2)";
+ private static final String REPLACEMENT_XML_ATTR_TEMPLATE = "$1=$2XXX$4";
+
+ private static final String XML_CONTENT = "xml";
+ private static final String HTML_CONTENT = "html";
+
+ private static class ReplacementPair {
+ private final Pattern matchPattern;
+ private final String replacement;
+ ReplacementPair(String matchPattern, String replacement) {
+
+ this.matchPattern = Pattern.compile(matchPattern, Pattern.DOTALL);
+ this.replacement = replacement;
+ }
+ }
+
+ private final Set<ReplacementPair> replacementsXMLAttributes = new
HashSet<>();
+
+ /** Adds attribute names to be masked in XML/HTML logs (values replaced
with "XXX"). */
+ public void addSensitiveAttributeNames(final Set<String>
inSensitiveAttirbuteNames) {
+ if (inSensitiveAttirbuteNames == null ||
inSensitiveAttirbuteNames.isEmpty()) {
+ return;
+ }
+ for (final String attr : inSensitiveAttirbuteNames) {
+ final String match =
MATCH_PATTERN_XML_ATTR_TEMPLATE.replace(ATTR_NAME_TEMPLATE,
Pattern.quote(attr));
+ final String repl =
REPLACEMENT_XML_ATTR_TEMPLATE.replace(ATTR_NAME_TEMPLATE,
escapeForReplacement(attr));
+ replacementsXMLAttributes.add(new ReplacementPair(match, repl));
+ }
+ }
+
+ /** Optional convenience resetter if you want it. */
+ public void setSensitiveAttributeNames(final Set<String>
inSensitiveAttributeNames) {
+ replacementsXMLAttributes.clear();
+ addSensitiveAttributeNames(inSensitiveAttributeNames);
+ }
+
+ @Override
+ public String maskSensitiveElements(final Message message, final String
originalLogString) {
+ // First, do all base-class masking (elements/JSON/headers)
+ String masked = super.maskSensitiveElements(message,
originalLogString);
+ if (masked == null || message == null) {
+ return masked;
+ }
+ final String contentType = (String) message.get(Message.CONTENT_TYPE);
+ if (contentType == null) {
+ return masked;
+ }
+ final String lower = contentType.toLowerCase();
+ if (lower.contains(XML_CONTENT) || lower.contains(HTML_CONTENT)) {
+ // Then apply attribute-value masking
+ return applyMasks(masked, replacementsXMLAttributes);
+ }
+ return masked;
+ }
+
+ // --- helpers (local copy; parent versions are private) ---
+
+ private static String escapeForReplacement(String s) {
+ if (s == null || s.isEmpty()) {
+ return s;
+ }
+ return s.replace("\\", "\\\\").replace("$", "\\$");
+ }
+
+ private String applyMasks(String input, Set<ReplacementPair> pairs) {
+ String out = input;
+ for (final ReplacementPair rp : pairs) {
+ out = rp.matchPattern.matcher(out).replaceAll(rp.replacement);
+ }
+ return out;
+ }
+}
diff --git
a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java
b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java
new file mode 100644
index 0000000000..1ff7db1242
--- /dev/null
+++
b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/AttributeMaskSensitiveHelperTest.java
@@ -0,0 +1,257 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.ext.logging;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.cxf.ext.logging.event.LogEvent;
+import org.apache.cxf.message.Exchange;
+import org.apache.cxf.message.ExchangeImpl;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.message.MessageImpl;
+import org.apache.cxf.phase.PhaseInterceptor;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(Parameterized.class)
+public class AttributeMaskSensitiveHelperTest {
+
+ private static final String SENSITIVE_LOGGING_CONTENT_XML =
+ "<user>testUser</user><password>my secret password</password>";
+ private static final String MASKED_LOGGING_CONTENT_XML =
+ "<user>testUser</user><password>XXX</password>";
+
+ private static final String SENSITIVE_LOGGING_CONTENT_XML_WITH_ATTRIBUTE =
+ "<user>testUser</user><password myAttribute=\"test\">my secret
password</password>";
+ private static final String MASKED_LOGGING_CONTENT_XML_WITH_ATTRIBUTE =
+ "<user>testUser</user><password
myAttribute=\"XXX\">XXX</password>";
+
+ private static final String SENSITIVE_LOGGING_CONTENT_XML_WITH_MULTILINE =
+ "<user>testUser</user><password>my \nsecret \npassword</password>";
+ private static final String MASKED_LOGGING_CONTENT_XML_WITH_MULTILINE =
+ "<user>testUser</user><password>XXX</password>";
+
+ private static final String SENSITIVE_LOGGING_CONTENT_XML_WITH_WRAPPER =
+ "<passwords><password>my secret password</password></passwords>";
+ private static final String MASKED_LOGGING_CONTENT_XML_WITH_WITH_WRAPPER =
+ "<passwords><password>XXX</password></passwords>";
+
+ private static final String SENSITIVE_LOGGING_XML_EMPTY_TAG_REPEATED =
+ "<user1><password/></user1><user2><password>VALUE</password></user2>";
+ private static final String MASKED_LOGGING_XML_EMPTY_TAG_REPEATED =
+ "<user1><password/></user1><user2><password>XXX</password></user2>";
+
+ private static final String SENSITIVE_LOGGING_CONTENT_JSON =
+ "\"user\":\"testUser\", \"password\": \"my secret password\"";
+ private static final String MASKED_LOGGING_CONTENT_JSON =
+ "\"user\":\"testUser\", \"password\": \"XXX\"";
+
+ private static final String SENSITIVE_LOGGING_MULTIPLE_ELEMENT_XML =
+ "<item><user>testUser1</user><password myAttribute=\"test\">my secret
password 1</password></item>"
+ + "<item><user>testUser2</user><password>my secret password
2</password></item>";
+ private static final String MASKED_LOGGING_MULTIPLE_ELEMENT_XML =
+ "<item><user>testUser1</user><password
myAttribute=\"XXX\">XXX</password></item>"
+ + "<item><user>testUser2</user><password>XXX</password></item>";
+
+ private static final String SENSITIVE_LOGGING_CONTENT_XML_WITH_NAMESPACE =
+ "<ns:user>testUser</ns:user><ns:password>my secret
password</ns:password>";
+
+ private static final String MASKED_LOGGING_CONTENT_XML_WITH_NAMESPACE =
+ "<ns:user>testUser</ns:user><ns:password>XXX</ns:password>";
+
+ private static final Set<String> SENSITIVE_ELEMENTS = new
HashSet<>(Arrays.asList("password"));
+ private static final Set<String> SENSITIVE_ATTRIBUTES = new
HashSet<>(Arrays.asList("myAttribute"));
+ private static final String APPLICATION_XML = "application/xml";
+ private static final String APPLICATION_JSON = "application/json";
+
+ private final String loggingContent;
+ private final String maskedContent;
+ private final String contentType;
+ private LogEventSenderMock logEventSender = new LogEventSenderMock();
+ public AttributeMaskSensitiveHelperTest(String loggingContent, String
maskedContent, String contentType) {
+ this.loggingContent = loggingContent;
+ this.maskedContent = maskedContent;
+ this.contentType = contentType;
+ }
+
+ @Parameterized.Parameters
+ public static Collection<Object[]> primeNumbers() {
+ return Arrays.asList(new Object[][] {
+ {SENSITIVE_LOGGING_CONTENT_XML, MASKED_LOGGING_CONTENT_XML,
APPLICATION_XML},
+ {SENSITIVE_LOGGING_CONTENT_XML_WITH_ATTRIBUTE,
MASKED_LOGGING_CONTENT_XML_WITH_ATTRIBUTE, APPLICATION_XML},
+ {SENSITIVE_LOGGING_CONTENT_XML_WITH_MULTILINE,
MASKED_LOGGING_CONTENT_XML_WITH_MULTILINE, APPLICATION_XML},
+ {SENSITIVE_LOGGING_CONTENT_XML_WITH_WRAPPER,
MASKED_LOGGING_CONTENT_XML_WITH_WITH_WRAPPER, APPLICATION_XML},
+ {SENSITIVE_LOGGING_XML_EMPTY_TAG_REPEATED,
MASKED_LOGGING_XML_EMPTY_TAG_REPEATED, APPLICATION_XML},
+ {SENSITIVE_LOGGING_MULTIPLE_ELEMENT_XML,
MASKED_LOGGING_MULTIPLE_ELEMENT_XML, APPLICATION_XML},
+ {SENSITIVE_LOGGING_CONTENT_XML_WITH_NAMESPACE,
MASKED_LOGGING_CONTENT_XML_WITH_NAMESPACE, APPLICATION_XML},
+ {SENSITIVE_LOGGING_CONTENT_JSON, MASKED_LOGGING_CONTENT_JSON,
APPLICATION_JSON}
+ });
+ }
+
+ @Test
+ public void shouldReplaceSensitiveDataInWithAdd() {
+ // Arrange
+ final LoggingInInterceptor inInterceptor = new
LoggingInInterceptor(logEventSender);
+ AttributeMaskingHelper attrMaskHelper = new AttributeMaskingHelper();
+ attrMaskHelper.setSensitiveAttributeNames(SENSITIVE_ATTRIBUTES);
+ attrMaskHelper.setSensitiveElementNames(SENSITIVE_ELEMENTS);
+ inInterceptor.setSensitiveDataHelper(attrMaskHelper);
+
+ final Message message = prepareInMessage();
+
+ // Act
+ Collection<PhaseInterceptor<? extends Message>> interceptors =
inInterceptor.getAdditionalInterceptors();
+ for (PhaseInterceptor intercept : interceptors) {
+ intercept.handleMessage(message);
+ }
+ inInterceptor.handleMessage(message);
+
+ // Verify
+ LogEvent event = logEventSender.getLogEvent();
+ assertNotNull(event);
+ assertEquals(maskedContent, event.getPayload());
+ }
+
+ @Test
+ public void shouldReplaceSensitiveDataInWithSet() {
+ // Arrange
+ final LoggingInInterceptor inInterceptor = new
LoggingInInterceptor(logEventSender);
+
+ AttributeMaskingHelper attrMaskHelper = new AttributeMaskingHelper();
+ attrMaskHelper.setSensitiveAttributeNames(SENSITIVE_ATTRIBUTES);
+ attrMaskHelper.setSensitiveElementNames(SENSITIVE_ELEMENTS);
+ inInterceptor.setSensitiveDataHelper(attrMaskHelper);
+ final Message message = prepareInMessage();
+
+ // Act
+ Collection<PhaseInterceptor<? extends Message>> interceptors =
inInterceptor.getAdditionalInterceptors();
+ for (PhaseInterceptor intercept : interceptors) {
+ intercept.handleMessage(message);
+ }
+ inInterceptor.handleMessage(message);
+
+ // Verify
+ LogEvent event = logEventSender.getLogEvent();
+ assertNotNull(event);
+ assertEquals(maskedContent, event.getPayload());
+ }
+
+ @Test
+ public void shouldReplaceSensitiveDataOutWithAdd() throws IOException {
+ // Arrange
+ final LoggingOutInterceptor outInterceptor = new
LoggingOutInterceptor(logEventSender);
+ AttributeMaskingHelper attrMaskHelper = new AttributeMaskingHelper();
+ attrMaskHelper.setSensitiveAttributeNames(SENSITIVE_ATTRIBUTES);
+ attrMaskHelper.setSensitiveElementNames(SENSITIVE_ELEMENTS);
+ outInterceptor.setSensitiveDataHelper(attrMaskHelper);
+
+ final Message message = prepareOutMessage();
+
+ // Act
+ outInterceptor.handleMessage(message);
+ byte[] payload = loggingContent.getBytes(StandardCharsets.UTF_8);
+ OutputStream out = message.getContent(OutputStream.class);
+ out.write(payload);
+ out.close();
+
+ // Verify
+ LogEvent event = logEventSender.getLogEvent();
+ assertNotNull(event);
+ assertEquals(maskedContent, event.getPayload());
+ }
+
+ @Test
+ public void shouldReplaceSensitiveDataOutWithSet() throws IOException {
+ // Arrange
+ final LoggingOutInterceptor outInterceptor = new
LoggingOutInterceptor(logEventSender);
+ AttributeMaskingHelper attrMaskHelper = new AttributeMaskingHelper();
+ attrMaskHelper.setSensitiveAttributeNames(SENSITIVE_ATTRIBUTES);
+ attrMaskHelper.setSensitiveElementNames(SENSITIVE_ELEMENTS);
+ outInterceptor.setSensitiveDataHelper(attrMaskHelper);
+
+ final Message message = prepareOutMessage();
+
+ // Act
+ outInterceptor.handleMessage(message);
+ byte[] payload = loggingContent.getBytes(StandardCharsets.UTF_8);
+ OutputStream out = message.getContent(OutputStream.class);
+ out.write(payload);
+ out.close();
+
+ // Verify
+ LogEvent event = logEventSender.getLogEvent();
+ assertNotNull(event);
+ assertEquals(maskedContent, event.getPayload());
+ }
+
+ @Test
+ public void shouldNotReplaceSensitiveDataEmptyExpression() throws
IOException {
+ // Arrange
+ final LoggingOutInterceptor outInterceptor = new
LoggingOutInterceptor(logEventSender);
+ final Message message = prepareOutMessage();
+
+ // Act
+ outInterceptor.handleMessage(message);
+ byte[] payload = loggingContent.getBytes(StandardCharsets.UTF_8);
+ OutputStream out = message.getContent(OutputStream.class);
+ out.write(payload);
+ out.close();
+
+ // Verify
+ LogEvent event = logEventSender.getLogEvent();
+ assertNotNull(event);
+ assertEquals(loggingContent, event.getPayload());
+ }
+
+ private Message prepareInMessage() {
+ Message message = new MessageImpl();
+ ByteArrayInputStream inputStream =
+ new
ByteArrayInputStream(loggingContent.getBytes(StandardCharsets.UTF_8));
+ message.put(Message.CONTENT_TYPE, contentType);
+ message.setContent(InputStream.class, inputStream);
+ Exchange exchange = new ExchangeImpl();
+ message.setExchange(exchange);
+ return message;
+ }
+
+ private Message prepareOutMessage() {
+ Message message = new MessageImpl();
+ message.put(Message.CONTENT_TYPE, contentType);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ message.setContent(OutputStream.class, outputStream);
+ Exchange exchange = new ExchangeImpl();
+ message.setExchange(exchange);
+ return message;
+ }
+
+}