This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch karaf-4.4.x
in repository https://gitbox.apache.org/repos/asf/karaf.git
The following commit(s) were added to refs/heads/karaf-4.4.x by this push:
new 4576eaa694 [4.4.x] Reject shell metacharacters in instance javaOpts
and add missing instance ACL (#2879)
4576eaa694 is described below
commit 4576eaa6945c175954652177ac7e698844c890b3
Author: JB Onofré <[email protected]>
AuthorDate: Sat Sep 12 05:58:24 2026 +0200
[4.4.x] Reject shell metacharacters in instance javaOpts and add missing
instance ACL (#2879)
* Reject shell metacharacters in instance javaOpts and add missing instance
ACL
InstanceServiceImpl builds the child-instance launch command by
concatenating
the caller-supplied javaOpts unquoted into a string that is ultimately
handed
to /bin/sh (via ScriptUtils/ProcessImpl), so a javaOpts value containing
shell
metacharacters was executed as an OS command rather than passed to the JVM.
This was reachable through instance:create, instance:start, instance:restart
and instance:change-opts (and the corresponding JMX operations).
Reject any javaOpts value that isn't valid JVM-option syntax before it is
used to build the launch command, in both doStart() and the self-restart
branch of restartInstance().
Also ship org.apache.karaf.command.acl.instance.cfg, since the instance
subshell had no ACL config at all and therefore failed open: any
authenticated shell user could invoke instance:* commands regardless of
role. The new ACL restricts state-changing operations to admin and
read-only ones (list/status) to viewer, matching the pattern used by the
other command ACLs.
* Fix InstanceTest to authenticate with admin/viewer roles for new instance
ACL
The new org.apache.karaf.command.acl.instance.cfg restricts instance:*
commands to the admin (and list/status to viewer) role. InstanceTest was
invoking these commands without any role principal, so the secured shell
rejected them as CommandNotFoundException, failing CI.
---
.../features/standard/src/main/feature/feature.xml | 46 ++++++++++++++++++++++
.../core/internal/InstanceServiceImpl.java | 15 +++++++
.../etc/org.apache.karaf.command.acl.instance.cfg | 41 +++++++++++++++++++
.../java/org/apache/karaf/itests/InstanceTest.java | 46 ++++++++++++----------
4 files changed, 127 insertions(+), 21 deletions(-)
diff --git a/assemblies/features/standard/src/main/feature/feature.xml
b/assemblies/features/standard/src/main/feature/feature.xml
index 3d9d474aca..0dab1e9c9e 100644
--- a/assemblies/features/standard/src/main/feature/feature.xml
+++ b/assemblies/features/standard/src/main/feature/feature.xml
@@ -649,6 +649,52 @@ update = manager
<feature name="instance" description="Provide Instance support"
version="${project.version}">
<bundle
start-level="30">mvn:org.apache.karaf.instance/org.apache.karaf.instance.core/${project.version}</bundle>
+ <conditional>
+ <condition>shell</condition>
+ <config name="org.apache.karaf.command.acl.instance">
+################################################################################
+#
+# 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.
+#
+################################################################################
+
+#
+# This configuration file defines the ACLs for commands in the instance
subshell
+#
+# For an explanation of the syntax of this file, see the file:
+# org.apache.karaf.command.acl.system.cfg
+#
+list = viewer
+status = viewer
+clone = admin
+connect = admin
+create = admin
+destroy = admin
+opts-change = admin
+package = admin
+rename = admin
+restart = admin
+rmi-registry-port-change = admin
+rmi-server-port-change = admin
+ssh-host-change = admin
+ssh-port-change = admin
+start = admin
+stop = admin
+ </config>
+ </conditional>
</feature>
<feature name="jaas-deployer" description="Allow support of blueprint JAAS
configuration in the deploy folder" version="${project.version}">
diff --git
a/instance/src/main/java/org/apache/karaf/instance/core/internal/InstanceServiceImpl.java
b/instance/src/main/java/org/apache/karaf/instance/core/internal/InstanceServiceImpl.java
index 2c56c3c15f..5c375f62ba 100644
---
a/instance/src/main/java/org/apache/karaf/instance/core/internal/InstanceServiceImpl.java
+++
b/instance/src/main/java/org/apache/karaf/instance/core/internal/InstanceServiceImpl.java
@@ -40,6 +40,7 @@ import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeMap;
+import java.util.regex.Pattern;
import org.apache.felix.utils.properties.InterpolationHelper;
import org.apache.felix.utils.properties.Properties;
@@ -88,6 +89,10 @@ public class InstanceServiceImpl implements InstanceService {
public static final String DEFAULT_JAVA_OPTS = "-Xmx512m
-XX:+UnlockDiagnosticVMOptions";
+ // javaOpts is concatenated unquoted into a shell command line (see
doStart/restartInstance), so it must
+ // never contain shell metacharacters; only characters that can legally
appear in JVM option syntax are allowed.
+ private static final Pattern SAFE_JAVA_OPTS = Pattern.compile("[A-Za-z0-9_
\\t\\-+.,:=/@*]*");
+
private LinkedHashMap<String, InstanceImpl> proxies = new
LinkedHashMap<>();
private File storageLocation;
@@ -351,6 +356,7 @@ public class InstanceServiceImpl implements InstanceService
{
"etc/org.apache.karaf.command.acl.bundle.cfg",
"etc/org.apache.karaf.command.acl.config.cfg",
"etc/org.apache.karaf.command.acl.feature.cfg",
+ "etc/org.apache.karaf.command.acl.instance.cfg",
"etc/org.apache.karaf.command.acl.jaas.cfg",
"etc/org.apache.karaf.command.acl.kar.cfg",
"etc/org.apache.karaf.command.acl.scope_bundle.cfg",
@@ -483,6 +489,13 @@ public class InstanceServiceImpl implements
InstanceService {
}, true);
}
+ private static void validateJavaOpts(String opts) {
+ if (opts != null && !SAFE_JAVA_OPTS.matcher(opts).matches()) {
+ throw new IllegalArgumentException("Invalid javaOpts: only JVM
option characters are allowed "
+ + "(letters, digits, whitespace and _-+.,:=/@*)");
+ }
+ }
+
private void doStart(InstanceState instance, String name, String javaOpts)
throws IOException {
String opts = javaOpts;
if (opts == null || opts.length() == 0) {
@@ -491,6 +504,7 @@ public class InstanceServiceImpl implements InstanceService
{
if (opts == null || opts.length() == 0) {
opts = DEFAULT_JAVA_OPTS;
}
+ validateJavaOpts(opts);
// fallback and read karafOpts from KARAF_OPTS environment if no
System property present
String karafOptsEnv = System.getenv("KARAF_OPTS");
@@ -619,6 +633,7 @@ public class InstanceServiceImpl implements InstanceService
{
}
String current = System.getProperty("karaf.name");
if (name.equals(current)) {
+ validateJavaOpts(javaOpts);
String location = System.getProperty("karaf.home");
StringBuilder classpath = new StringBuilder();
addJar(classpath, "org.apache.karaf.instance",
"org.apache.karaf.instance.core");
diff --git
a/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.command.acl.instance.cfg
b/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.command.acl.instance.cfg
new file mode 100644
index 0000000000..c09da30087
--- /dev/null
+++
b/instance/src/main/resources/org/apache/karaf/instance/resources/etc/org.apache.karaf.command.acl.instance.cfg
@@ -0,0 +1,41 @@
+################################################################################
+#
+# 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.
+#
+################################################################################
+
+#
+# This configuration file defines the ACLs for commands in the instance
subshell
+#
+# For an explanation of the syntax of this file, see the file:
+# org.apache.karaf.command.acl.system.cfg
+#
+list = viewer
+status = viewer
+clone = admin
+connect = admin
+create = admin
+destroy = admin
+opts-change = admin
+package = admin
+rename = admin
+restart = admin
+rmi-registry-port-change = admin
+rmi-server-port-change = admin
+ssh-host-change = admin
+ssh-port-change = admin
+start = admin
+stop = admin
diff --git
a/itests/test/src/test/java/org/apache/karaf/itests/InstanceTest.java
b/itests/test/src/test/java/org/apache/karaf/itests/InstanceTest.java
index 90ce247862..ae49f00d23 100644
--- a/itests/test/src/test/java/org/apache/karaf/itests/InstanceTest.java
+++ b/itests/test/src/test/java/org/apache/karaf/itests/InstanceTest.java
@@ -13,6 +13,7 @@
*/
package org.apache.karaf.itests;
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,6 +40,9 @@ import static org.junit.Assert.assertTrue;
@ExamReactorStrategy(PerClass.class)
public class InstanceTest extends BaseTest {
+ private static final RolePrincipal ADMIN_ROLE = new RolePrincipal("admin");
+ private static final RolePrincipal VIEWER_ROLE = new
RolePrincipal("viewer");
+
private String generateRandomInstanceName() {
return "instance-" + UUID.randomUUID();
}
@@ -46,10 +50,10 @@ public class InstanceTest extends BaseTest {
@Test
public void createDestroyCommand() throws Exception {
String instanceName = generateRandomInstanceName();
- System.out.println(executeCommand("instance:create " + instanceName));
- assertContains(instanceName, executeCommand("instance:list"));
- System.out.println(executeCommand("instance:destroy " + instanceName));
- assertContainsNot(instanceName, executeCommand("instance:list"));
+ System.out.println(executeCommand("instance:create " + instanceName,
ADMIN_ROLE));
+ assertContains(instanceName, executeCommand("instance:list",
VIEWER_ROLE));
+ System.out.println(executeCommand("instance:destroy " + instanceName,
ADMIN_ROLE));
+ assertContainsNot(instanceName, executeCommand("instance:list",
VIEWER_ROLE));
}
@Test
@@ -68,10 +72,10 @@ public class InstanceTest extends BaseTest {
@Test
public void createStartStopDestroyCommand() throws Exception {
String instanceName = generateRandomInstanceName();
- System.out.println(executeCommand("instance:create " + instanceName));
- assertContains(instanceName, executeCommand("instance:list"));
- System.out.println(executeCommand("instance:start " + instanceName));
- String output = executeCommand("instance:status " + instanceName);
+ System.out.println(executeCommand("instance:create " + instanceName,
ADMIN_ROLE));
+ assertContains(instanceName, executeCommand("instance:list",
VIEWER_ROLE));
+ System.out.println(executeCommand("instance:start " + instanceName,
ADMIN_ROLE));
+ String output = executeCommand("instance:status " + instanceName,
VIEWER_ROLE);
int i = 0;
while (!output.contains("Started")) {
if (i >= 10) {
@@ -79,12 +83,12 @@ public class InstanceTest extends BaseTest {
}
i = i + 1;
Thread.sleep(5000);
- output = executeCommand("instance:status " + instanceName);
+ output = executeCommand("instance:status " + instanceName,
VIEWER_ROLE);
}
System.out.println("itest instance status: " + output);
assertContains("Started", output);
- System.out.println(executeCommand("instance:stop " + instanceName));
- output = executeCommand("instance:status " + instanceName);
+ System.out.println(executeCommand("instance:stop " + instanceName,
ADMIN_ROLE));
+ output = executeCommand("instance:status " + instanceName,
VIEWER_ROLE);
i = 0;
while (!output.contains("Stopped")) {
if (i >= 10) {
@@ -92,18 +96,18 @@ public class InstanceTest extends BaseTest {
}
i = i + 1;
Thread.sleep(5000);
- output = executeCommand("instance:status " + instanceName);
+ output = executeCommand("instance:status " + instanceName,
VIEWER_ROLE);
}
System.out.println("itest instance status: " + output);
assertContains("Stopped", output);
- executeCommand("instance:destroy " + instanceName);
+ executeCommand("instance:destroy " + instanceName, ADMIN_ROLE);
}
@Test
public void packageCommand() throws Exception {
String instanceName = generateRandomInstanceName();
- executeCommand("instance:create " + instanceName);
- executeCommand("instance:package " + instanceName + " archive.zip");
+ executeCommand("instance:create " + instanceName, ADMIN_ROLE);
+ executeCommand("instance:package " + instanceName + " archive.zip",
ADMIN_ROLE);
String zipPath = Paths.get(System.getProperty("karaf.home"),
"archive.zip").toString();
ZipFile zipFile = new ZipFile(zipPath);
@@ -112,7 +116,7 @@ public class InstanceTest extends BaseTest {
assertTrue(entries.stream().anyMatch(e ->
e.getName().equals("bin/karaf")));
assertTrue(entries.stream().anyMatch(e ->
e.getName().equals("etc/system.properties")));
- executeCommand("instance:destroy " + instanceName);
+ executeCommand("instance:destroy " + instanceName, ADMIN_ROLE);
}
private int getInstancesNum(MBeanServerConnection connection, ObjectName
name) throws Exception {
@@ -123,8 +127,8 @@ public class InstanceTest extends BaseTest {
@Test
public void cloneCommand() throws Exception {
String instanceName = generateRandomInstanceName();
- System.out.println(executeCommand("instance:clone root " +
instanceName));
- assertContains(instanceName, executeCommand("instance:list"));
+ System.out.println(executeCommand("instance:clone root " +
instanceName, ADMIN_ROLE));
+ assertContains(instanceName, executeCommand("instance:list",
VIEWER_ROLE));
}
@Test
@@ -141,9 +145,9 @@ public class InstanceTest extends BaseTest {
@Test
public void renameCommand() throws Exception {
String instanceName = generateRandomInstanceName();
- executeCommand("instance:create " + instanceName);
- executeCommand("instance:rename " + instanceName + " new_" +
instanceName);
- assertContains("new_" + instanceName, executeCommand("instance:list"));
+ executeCommand("instance:create " + instanceName, ADMIN_ROLE);
+ executeCommand("instance:rename " + instanceName + " new_" +
instanceName, ADMIN_ROLE);
+ assertContains("new_" + instanceName, executeCommand("instance:list",
VIEWER_ROLE));
}
@Test