This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 31eeeaceb56d9f744acb5625e108331de799e667
Author: opencode <[email protected]>
AuthorDate: Wed Oct 14 17:31:19 2026 +0200

    Decode service binding entry files as UTF-8
    
    getProperty() decoded the binding file bytes with new String(byte[],int,
    int), which uses the JVM default character set. Service binding entries
    under SERVICE_BINDING_ROOT are text files that follow the UTF-8
    convention (Kubernetes secret projections encode stringData as UTF-8)
    and the resolved values flow into configuration such as SSLHostConfig
    certificate key passwords, so on a JVM whose default character set is
    not UTF-8 every non-ASCII character was replaced with a question mark,
    producing a silently wrong credential.
    
    Decode the bytes as UTF-8 explicitly. Since the service binding
    specification fixes no encoding, the class Javadoc now states that
    entry files are decoded as UTF-8 and that invalid bytes become U+FFFD.
    
    Adds TestServiceBindingPropertySource covering the UTF-8 decoding of
    multi-byte characters (with and without the chomp: prefix), the full
    chomp matrix and the null results for unknown bindings and malformed
    keys. The environment variable cannot be set from the tests, so a
    package-private constructor pins the binding root for the test class;
    the public no-argument path is unchanged and still reads the
    environment on every resolution. Note that the tests pass on the
    default-UTF-8 test JVM regardless of the fix; they fail against the
    previous code when the default character set is not UTF-8, and they
    pin the contract either way.
---
 .../digester/ServiceBindingPropertySource.java     | 27 +++++--
 .../digester/TestServiceBindingPropertySource.java | 91 ++++++++++++++++++++++
 2 files changed, 113 insertions(+), 5 deletions(-)

diff --git 
a/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java 
b/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java
index dfb4389dc4..4db40f6182 100644
--- a/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java
+++ b/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java
@@ -18,6 +18,7 @@ package org.apache.tomcat.util.digester;
 
 import java.io.FilePermission;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -65,6 +66,10 @@ import org.apache.tomcat.util.security.PermissionCheck;
  * convenience for hand-edited files/values where removing a trailing newline 
is difficult, and trailing whitespace
  * changes the meaning of the value.
  * </p>
+ * <p>
+ * Binding entry files are decoded as UTF-8. Bytes that are not valid UTF-8 
are replaced with the Unicode replacement
+ * character (U+FFFD).
+ * </p>
  * How to configure:
  *
  * <pre>
@@ -88,11 +93,23 @@ import org.apache.tomcat.util.security.PermissionCheck;
 public class ServiceBindingPropertySource implements 
IntrospectionUtils.SecurePropertySource {
 
     /**
-     * Default constructor.
+     * Default constructor. The binding root is taken from the {@code 
SERVICE_BINDING_ROOT} environment variable when
+     * an expression is resolved.
      */
     public ServiceBindingPropertySource() {
+        this(null);
+    }
+
+    /*
+     * Test seam: pins the binding root so the tests do not need to set the 
environment variable. A null value selects
+     * the environment variable at resolution time.
+     */
+    ServiceBindingPropertySource(String serviceBindingRoot) {
+        this.serviceBindingRoot = serviceBindingRoot;
     }
 
+    private final String serviceBindingRoot;
+
     private static final String SERVICE_BINDING_ROOT_ENV_VAR = 
"SERVICE_BINDING_ROOT";
 
     /**
@@ -122,8 +139,8 @@ public class ServiceBindingPropertySource implements 
IntrospectionUtils.SecurePr
         }
 
         // get the root to search from
-        String serviceBindingRoot = 
System.getenv(SERVICE_BINDING_ROOT_ENV_VAR);
-        if (serviceBindingRoot == null) {
+        String root = serviceBindingRoot != null ? serviceBindingRoot : 
System.getenv(SERVICE_BINDING_ROOT_ENV_VAR);
+        if (root == null) {
             return null;
         }
 
@@ -139,7 +156,7 @@ public class ServiceBindingPropertySource implements 
IntrospectionUtils.SecurePr
             return null;
         }
 
-        Path path = Paths.get(serviceBindingRoot, parts[0], parts[1]);
+        Path path = Paths.get(root, parts[0], parts[1]);
 
         if (!path.toFile().exists()) {
             return null;
@@ -168,7 +185,7 @@ public class ServiceBindingPropertySource implements 
IntrospectionUtils.SecurePr
                 }
             }
 
-            return new String(bytes, 0, length);
+            return new String(bytes, 0, length, StandardCharsets.UTF_8);
         } catch (IOException ioe) {
             // Treat as not found
             return null;
diff --git 
a/test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java 
b/test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java
new file mode 100644
index 0000000000..3a7e8d43fa
--- /dev/null
+++ b/test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java
@@ -0,0 +1,91 @@
+/*
+ * 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.tomcat.util.digester;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class TestServiceBindingPropertySource {
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+
+    private File writeBinding(String name, String key, String content) throws 
Exception {
+        File bindingFolder = new File(temporaryFolder.getRoot(), name);
+        Assert.assertTrue(bindingFolder.mkdirs() || 
bindingFolder.isDirectory());
+        File file = new File(bindingFolder, key);
+        Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
+        return file;
+    }
+
+
+    @Test
+    public void testPropertyValueIsDecodedAsUtf8() throws Exception {
+        String value = "p\u00e4ssw\u00f6rd-\u00fcnicode-\u2603";
+        writeBinding("my-binding", "keyPassword", value);
+        writeBinding("my-binding", "withNewline", value + "\n");
+
+        ServiceBindingPropertySource source =
+                new 
ServiceBindingPropertySource(temporaryFolder.getRoot().getAbsolutePath());
+
+        Assert.assertEquals(value, 
source.getProperty("my-binding.keyPassword"));
+        Assert.assertEquals(value, 
source.getProperty("chomp:my-binding.withNewline"));
+    }
+
+
+    @Test
+    public void testChompTrimsSingleTrailingLineBreak() throws Exception {
+        writeBinding("my-binding", "lf", "value\n");
+        writeBinding("my-binding", "crlf", "value\r\n");
+        writeBinding("my-binding", "cr", "value\r");
+        writeBinding("my-binding", "none", "value");
+        writeBinding("my-binding", "blankline", "value\n\n");
+        writeBinding("my-binding", "trailingspace", "value \n");
+
+        ServiceBindingPropertySource source =
+                new 
ServiceBindingPropertySource(temporaryFolder.getRoot().getAbsolutePath());
+
+        Assert.assertEquals("value", 
source.getProperty("chomp:my-binding.lf"));
+        Assert.assertEquals("value", 
source.getProperty("chomp:my-binding.crlf"));
+        Assert.assertEquals("value", 
source.getProperty("chomp:my-binding.cr"));
+        Assert.assertEquals("value", 
source.getProperty("chomp:my-binding.none"));
+        Assert.assertEquals("value\n", 
source.getProperty("chomp:my-binding.blankline"));
+        Assert.assertEquals("value ", 
source.getProperty("chomp:my-binding.trailingspace"));
+    }
+
+
+    @Test
+    public void testUnknownBindingsResolveToNull() throws Exception {
+        writeBinding("my-binding", "known", "value");
+
+        ServiceBindingPropertySource source =
+                new 
ServiceBindingPropertySource(temporaryFolder.getRoot().getAbsolutePath());
+
+        Assert.assertNull(source.getProperty("my-binding.unknown"));
+        Assert.assertNull(source.getProperty("unknown-binding.known"));
+        // A key without exactly one dot is outside the <binding-name>.<key> 
contract
+        Assert.assertNull(source.getProperty("no-dot"));
+        Assert.assertNull(source.getProperty("too.many.dots"));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to