From 82b3bda2aa8bc381e474394188d0435a8719c067 Mon Sep 17 00:00:00 2001
From: dfuchs
Date: Mon, 9 Mar 2009 23:50:11 +0100
Subject: [PATCH] 6721651: Security problem with out-of-the-box management
Reviewed-by: emcmanus, lmalvent
---
.../security/MBeanServerAccessController.java | 52 ++-
.../MBeanServerFileAccessController.java | 374 +++++++++++++++---
src/share/lib/management/jmxremote.access | 47 ++-
3 files changed, 388 insertions(+), 85 deletions(-)
diff --git a/src/share/classes/com/sun/jmx/remote/security/MBeanServerAccessController.java b/src/share/classes/com/sun/jmx/remote/security/MBeanServerAccessController.java
index d75f829c3..71e6f6ff0 100644
--- a/src/share/classes/com/sun/jmx/remote/security/MBeanServerAccessController.java
+++ b/src/share/classes/com/sun/jmx/remote/security/MBeanServerAccessController.java
@@ -111,6 +111,22 @@ public abstract class MBeanServerAccessController
*/
protected abstract void checkWrite();
+ /**
+ * Check if the caller can create the named class. The default
+ * implementation of this method calls {@link #checkWrite()}.
+ */
+ protected void checkCreate(String className) {
+ checkWrite();
+ }
+
+ /**
+ * Check if the caller can unregister the named MBean. The default
+ * implementation of this method calls {@link #checkWrite()}.
+ */
+ protected void checkUnregister(ObjectName name) {
+ checkWrite();
+ }
+
//--------------------------------------------
//--------------------------------------------
//
@@ -148,7 +164,7 @@ public abstract class MBeanServerAccessController
}
/**
- * Call checkWrite(), then forward this method to the
+ * Call checkCreate(className), then forward this method to the
* wrapped object.
*/
public ObjectInstance createMBean(String className, ObjectName name)
@@ -158,7 +174,7 @@ public abstract class MBeanServerAccessController
MBeanRegistrationException,
MBeanException,
NotCompliantMBeanException {
- checkWrite();
+ checkCreate(className);
SecurityManager sm = System.getSecurityManager();
if (sm == null) {
Object object = getMBeanServer().instantiate(className);
@@ -170,7 +186,7 @@ public abstract class MBeanServerAccessController
}
/**
- * Call checkWrite(), then forward this method to the
+ * Call checkCreate(className), then forward this method to the
* wrapped object.
*/
public ObjectInstance createMBean(String className, ObjectName name,
@@ -181,7 +197,7 @@ public abstract class MBeanServerAccessController
MBeanRegistrationException,
MBeanException,
NotCompliantMBeanException {
- checkWrite();
+ checkCreate(className);
SecurityManager sm = System.getSecurityManager();
if (sm == null) {
Object object = getMBeanServer().instantiate(className,
@@ -196,7 +212,7 @@ public abstract class MBeanServerAccessController
}
/**
- * Call checkWrite(), then forward this method to the
+ * Call checkCreate(className), then forward this method to the
* wrapped object.
*/
public ObjectInstance createMBean(String className,
@@ -209,7 +225,7 @@ public abstract class MBeanServerAccessController
MBeanException,
NotCompliantMBeanException,
InstanceNotFoundException {
- checkWrite();
+ checkCreate(className);
SecurityManager sm = System.getSecurityManager();
if (sm == null) {
Object object = getMBeanServer().instantiate(className,
@@ -222,7 +238,7 @@ public abstract class MBeanServerAccessController
}
/**
- * Call checkWrite(), then forward this method to the
+ * Call checkCreate(className), then forward this method to the
* wrapped object.
*/
public ObjectInstance createMBean(String className,
@@ -237,7 +253,7 @@ public abstract class MBeanServerAccessController
MBeanException,
NotCompliantMBeanException,
InstanceNotFoundException {
- checkWrite();
+ checkCreate(className);
SecurityManager sm = System.getSecurityManager();
if (sm == null) {
Object object = getMBeanServer().instantiate(className,
@@ -394,45 +410,45 @@ public abstract class MBeanServerAccessController
}
/**
- * Call checkWrite(), then forward this method to the
+ * Call checkCreate(className), then forward this method to the
* wrapped object.
*/
public Object instantiate(String className)
throws ReflectionException, MBeanException {
- checkWrite();
+ checkCreate(className);
return getMBeanServer().instantiate(className);
}
/**
- * Call checkWrite(), then forward this method to the
+ * Call checkCreate(className), then forward this method to the
* wrapped object.
*/
public Object instantiate(String className,
Object params[],
String signature[])
throws ReflectionException, MBeanException {
- checkWrite();
+ checkCreate(className);
return getMBeanServer().instantiate(className, params, signature);
}
/**
- * Call checkWrite(), then forward this method to the
+ * Call checkCreate(className), then forward this method to the
* wrapped object.
*/
public Object instantiate(String className, ObjectName loaderName)
throws ReflectionException, MBeanException, InstanceNotFoundException {
- checkWrite();
+ checkCreate(className);
return getMBeanServer().instantiate(className, loaderName);
}
/**
- * Call checkWrite(), then forward this method to the
+ * Call checkCreate(className), then forward this method to the
* wrapped object.
*/
public Object instantiate(String className, ObjectName loaderName,
Object params[], String signature[])
throws ReflectionException, MBeanException, InstanceNotFoundException {
- checkWrite();
+ checkCreate(className);
return getMBeanServer().instantiate(className, loaderName,
params, signature);
}
@@ -579,12 +595,12 @@ public abstract class MBeanServerAccessController
}
/**
- * Call checkWrite(), then forward this method to the
+ * Call checkUnregister(), then forward this method to the
* wrapped object.
*/
public void unregisterMBean(ObjectName name)
throws InstanceNotFoundException, MBeanRegistrationException {
- checkWrite();
+ checkUnregister(name);
getMBeanServer().unregisterMBean(name);
}
diff --git a/src/share/classes/com/sun/jmx/remote/security/MBeanServerFileAccessController.java b/src/share/classes/com/sun/jmx/remote/security/MBeanServerFileAccessController.java
index 8bca71dbc..01bb6108e 100644
--- a/src/share/classes/com/sun/jmx/remote/security/MBeanServerFileAccessController.java
+++ b/src/share/classes/com/sun/jmx/remote/security/MBeanServerFileAccessController.java
@@ -31,11 +31,17 @@ import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
-import java.util.Collection;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
import java.util.Properties;
import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
import javax.management.MBeanServer;
+import javax.management.ObjectName;
import javax.security.auth.Subject;
/**
@@ -46,7 +52,8 @@ import javax.security.auth.Subject;
* not allowed; in this case the request is not forwarded to the
* wrapped object.
*
- *
This class implements the {@link #checkRead()} and {@link #checkWrite()}
+ *
This class implements the {@link #checkRead()}, {@link #checkWrite()},
+ * {@link #checkCreate(String)}, and {@link #checkUnregister(ObjectName)}
* methods based on an access level properties file containing username/access
* level pairs. The set of username/access level pairs is passed either as a
* filename which denotes a properties file on disk, or directly as an instance
@@ -56,14 +63,50 @@ import javax.security.auth.Subject;
* has exactly one access level. The same access level can be shared by several
* usernames.
*
- *
The supported access level values are readonly and
- * readwrite.
+ *
The supported access level values are {@code readonly} and
+ * {@code readwrite}. The {@code readwrite} access level can be
+ * qualified by one or more clauses, where each clause looks
+ * like create classNamePattern or {@code
+ * unregister}. For example:
(The continuation lines with {@code \} come from the parser for
+ * Properties files.)
*/
public class MBeanServerFileAccessController
extends MBeanServerAccessController {
- public static final String READONLY = "readonly";
- public static final String READWRITE = "readwrite";
+ static final String READONLY = "readonly";
+ static final String READWRITE = "readwrite";
+
+ static final String CREATE = "create";
+ static final String UNREGISTER = "unregister";
+
+ private enum AccessType {READ, WRITE, CREATE, UNREGISTER};
+
+ private static class Access {
+ final boolean write;
+ final String[] createPatterns;
+ private boolean unregister;
+
+ Access(boolean write, boolean unregister, List createPatternList) {
+ this.write = write;
+ int npats = (createPatternList == null) ? 0 : createPatternList.size();
+ if (npats == 0)
+ this.createPatterns = NO_STRINGS;
+ else
+ this.createPatterns = createPatternList.toArray(new String[npats]);
+ this.unregister = unregister;
+ }
+
+ private final String[] NO_STRINGS = new String[0];
+ }
/**
*
Create a new MBeanServerAccessController that forwards all the
@@ -87,8 +130,8 @@ public class MBeanServerFileAccessController
throws IOException {
super();
this.accessFileName = accessFileName;
- props = propertiesFromFile(accessFileName);
- checkValues(props);
+ Properties props = propertiesFromFile(accessFileName);
+ parseProperties(props);
}
/**
@@ -123,14 +166,14 @@ public class MBeanServerFileAccessController
* #setMBeanServer} method after doing access checks based on read and
* write permissions.
*
- *
This instance is initialized from the specified properties instance.
- * This constructor makes a copy of the properties instance using its
- * clone method and it is the copy that is consulted to check
- * the username and access level of an incoming connection. The original
- * properties object can be modified without affecting the copy. If the
- * {@link #refresh} method is then called, the
- * MBeanServerFileAccessController will make a new copy of the
- * properties object at that time.
+ *
This instance is initialized from the specified properties
+ * instance. This constructor makes a copy of the properties
+ * instance and it is the copy that is consulted to check the
+ * username and access level of an incoming connection. The
+ * original properties object can be modified without affecting
+ * the copy. If the {@link #refresh} method is then called, the
+ * MBeanServerFileAccessController will make a new
+ * copy of the properties object at that time.
*
* @param accessFileProps properties list containing the username/access
* level entries.
@@ -145,8 +188,7 @@ public class MBeanServerFileAccessController
if (accessFileProps == null)
throw new IllegalArgumentException("Null properties");
originalProps = accessFileProps;
- props = (Properties) accessFileProps.clone();
- checkValues(props);
+ parseProperties(accessFileProps);
}
/**
@@ -155,14 +197,14 @@ public class MBeanServerFileAccessController
* #setMBeanServer} method after doing access checks based on read and
* write permissions.
*
- *
This instance is initialized from the specified properties instance.
- * This constructor makes a copy of the properties instance using its
- * clone method and it is the copy that is consulted to check
- * the username and access level of an incoming connection. The original
- * properties object can be modified without affecting the copy. If the
- * {@link #refresh} method is then called, the
- * MBeanServerFileAccessController will make a new copy of the
- * properties object at that time.
+ *
This instance is initialized from the specified properties
+ * instance. This constructor makes a copy of the properties
+ * instance and it is the copy that is consulted to check the
+ * username and access level of an incoming connection. The
+ * original properties object can be modified without affecting
+ * the copy. If the {@link #refresh} method is then called, the
+ * MBeanServerFileAccessController will make a new
+ * copy of the properties object at that time.
*
* @param accessFileProps properties list containing the username/access
* level entries.
@@ -184,16 +226,36 @@ public class MBeanServerFileAccessController
* Check if the caller can do read operations. This method does
* nothing if so, otherwise throws SecurityException.
*/
+ @Override
public void checkRead() {
- checkAccessLevel(READONLY);
+ checkAccess(AccessType.READ, null);
}
/**
* Check if the caller can do write operations. This method does
* nothing if so, otherwise throws SecurityException.
*/
+ @Override
public void checkWrite() {
- checkAccessLevel(READWRITE);
+ checkAccess(AccessType.WRITE, null);
+ }
+
+ /**
+ * Check if the caller can create MBeans or instances of the given class.
+ * This method does nothing if so, otherwise throws SecurityException.
+ */
+ @Override
+ public void checkCreate(String className) {
+ checkAccess(AccessType.CREATE, className);
+ }
+
+ /**
+ * Check if the caller can do unregister operations. This method does
+ * nothing if so, otherwise throws SecurityException.
+ */
+ @Override
+ public void checkUnregister(ObjectName name) {
+ checkAccess(AccessType.UNREGISTER, null);
}
/**
@@ -218,14 +280,13 @@ public class MBeanServerFileAccessController
* @exception IllegalArgumentException if any of the supplied access
* level values differs from "readonly" or "readwrite".
*/
- public void refresh() throws IOException {
- synchronized (props) {
- if (accessFileName == null)
- props = (Properties) originalProps.clone();
- else
- props = propertiesFromFile(accessFileName);
- checkValues(props);
- }
+ public synchronized void refresh() throws IOException {
+ Properties props;
+ if (accessFileName == null)
+ props = (Properties) originalProps;
+ else
+ props = propertiesFromFile(accessFileName);
+ parseProperties(props);
}
private static Properties propertiesFromFile(String fname)
@@ -240,7 +301,7 @@ public class MBeanServerFileAccessController
}
}
- private void checkAccessLevel(String accessLevel) {
+ private synchronized void checkAccess(AccessType requiredAccess, String arg) {
final AccessControlContext acc = AccessController.getContext();
final Subject s =
AccessController.doPrivileged(new PrivilegedAction() {
@@ -250,39 +311,234 @@ public class MBeanServerFileAccessController
});
if (s == null) return; /* security has not been enabled */
final Set principals = s.getPrincipals();
+ String newPropertyValue = null;
for (Iterator i = principals.iterator(); i.hasNext(); ) {
final Principal p = (Principal) i.next();
- String grantedAccessLevel;
- synchronized (props) {
- grantedAccessLevel = props.getProperty(p.getName());
- }
- if (grantedAccessLevel != null) {
- if (accessLevel.equals(READONLY) &&
- (grantedAccessLevel.equals(READONLY) ||
- grantedAccessLevel.equals(READWRITE)))
- return;
- if (accessLevel.equals(READWRITE) &&
- grantedAccessLevel.equals(READWRITE))
+ Access access = accessMap.get(p.getName());
+ if (access != null) {
+ boolean ok;
+ switch (requiredAccess) {
+ case READ:
+ ok = true; // all access entries imply read
+ break;
+ case WRITE:
+ ok = access.write;
+ break;
+ case UNREGISTER:
+ ok = access.unregister;
+ if (!ok && access.write)
+ newPropertyValue = "unregister";
+ break;
+ case CREATE:
+ ok = checkCreateAccess(access, arg);
+ if (!ok && access.write)
+ newPropertyValue = "create " + arg;
+ break;
+ default:
+ throw new AssertionError();
+ }
+ if (ok)
return;
}
}
- throw new SecurityException("Access denied! Invalid access level for " +
- "requested MBeanServer operation.");
+ SecurityException se = new SecurityException("Access denied! Invalid " +
+ "access level for requested MBeanServer operation.");
+ // Add some more information to help people with deployments that
+ // worked before we required explicit create clauses. We're not giving
+ // any information to the bad guys, other than that the access control
+ // is based on a file, which they could have worked out from the stack
+ // trace anyway.
+ if (newPropertyValue != null) {
+ SecurityException se2 = new SecurityException("Access property " +
+ "for this identity should be similar to: " + READWRITE +
+ " " + newPropertyValue);
+ se.initCause(se2);
+ }
+ throw se;
+ }
+
+ private static boolean checkCreateAccess(Access access, String className) {
+ for (String classNamePattern : access.createPatterns) {
+ if (classNameMatch(classNamePattern, className))
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean classNameMatch(String pattern, String className) {
+ // We studiously avoided regexes when parsing the properties file,
+ // because that is done whenever the VM is started with the
+ // appropriate -Dcom.sun.management options, even if nobody ever
+ // creates an MBean. We don't want to incur the overhead of loading
+ // all the regex code whenever those options are specified, but if we
+ // get as far as here then the VM is already running and somebody is
+ // doing the very unusual operation of remotely creating an MBean.
+ // Because that operation is so unusual, we don't try to optimize
+ // by hand-matching or by caching compiled Pattern objects.
+ StringBuilder sb = new StringBuilder();
+ StringTokenizer stok = new StringTokenizer(pattern, "*", true);
+ while (stok.hasMoreTokens()) {
+ String tok = stok.nextToken();
+ if (tok.equals("*"))
+ sb.append("[^.]*");
+ else
+ sb.append(Pattern.quote(tok));
+ }
+ return className.matches(sb.toString());
+ }
+
+ private void parseProperties(Properties props) {
+ this.accessMap = new HashMap();
+ for (Map.Entry