Using a SecurityManager: identifying requirements
Bear Giles | September 29, 2010This is the first in a series of entries regarding the Java SecurityManager. The JVM was designed with security as a foremost concern but most of us run without a SecurityManager for three reasons:
- we are unfamiliar with it,
- we don’t perceive a need,
- it is a real PITA to set up.
The first point is a simple matter of education.
The second point is more subtle. Most of us have always developed software in a completely open environment. That doesn’t need to change. Unfortunately we’ve also always run our software in a completely open environment. That wasn’t a huge problem historically because data was passive and libraries were simple enough for experienced developers to understand what was happening under the covers.
That’s changed though. Now data is much more intelligent and at a minimum we need to be able to parse it and perform decompression and/or decryption (all of which introduces the possibility of exploitable stack overflows). It might even include explicit or implicit scripting capabilities. How many people would realize that XML external entities (XEE) could be used to pull in arbitrary files from the filesystem or perform a denial of service attack on a service?
Careful C developers have addressed this for years. At the top of the main procedure they would call setrlimit() to set limits on various system resources. If you know you already have all necessary files open there’s no reason to have permission to open another file. You can prevent forked jobs. You can limit the maximum amount of memory consumed.
This is crude but available everywhere. Secure Linux (SELinux) takes this to a while new level but it requires a lot more effort to do it right.
The java equivalence is a SecurityManager and its policy. Like C’s setrlimit you don’t have to be perfect to do have a dramatic improvement in security.
For instance, how many files do you need to open? Really? On a typical webapp I only count a handful of locations:
- the class path (read-only)
- the temp directory (${java.io.tmpdir}) (read, write, delete, but not execute)
- the servlet context (read-only)
- the logging facility (write-only?)
- any directories for persistent storage, if any.
- maybe the user’s home directory, for configuration information. (${user.home})
It’s extraordinary for any webapp to require more than that – and most would only require the first four if they use a database for storage and JNDI for configuration.
Network/Socket requirements are similar. You need to be able to find and connect to your resources – database, mail server, JMS, etc. But after that you probably don’t need to be able to initiate any more connections. You can either specify no permissions, or just the ‘resolve’ and ‘connect’ permissions to those specific servers.
The other permissions are a good news/bad news situation. It’s great that you have fine control over these permissions, but it’s a pain that you have to specify them explicitly. However there are shortcuts that are Good Enough in nearly all cases. E.g., grant ‘read-only’ permission to all system properties. When was the last time you needed to programmatically set a system property? When was the last time you needed to do it after program initialization? (The downside to this is that you could leak information if somebody managed to get a bit of core into your system — so you just don’t provide sensitive information like details about the user or system.)
The final point is very real. The security manager was designed to be set up via an external policy file and much of it is ‘write only’. You can’t easily learn what your existing permissions are. (I think you can do it by specifying a DomainCombiner and instrumenting the method that combines the sets of properties, but I haven’t verified this.) There’s no handy reference to all of the required permissions for various tasks, e.g., the dozens of permissions required if you need to create an object. Any object.
However we can create our own SecurityManager programmatically, one that provides exactly the permissions we need. It’s contrary to current “best practices”… but those best practices aren’t worth much if nobody uses them. (Besides you can still enforce the external policy file if you have a dedicated security team that’s responsible for maintaining it.)
If we want to get fancy we can even define our own Annotations that we can use on each class and/or method to specify exactly what permissions that class or method requires. It’s not necessary for there to be a single list of required permissions.
Finally we can use an instrumented SecurityManager to learn what permissions we require. This isn’t a game of “mother may I”, we can run our application with a transparent SecurityManagement during development and integration tests to learn exactly what permissions we should provide in the production environment.
An example instrumented SecurityManager follows. To use it simply call System.setSecurityManager(new LoggingSecurityManager()) near the top of your application.
- /**
- * Security manager that records missing permissions but otherwise
- * allows everything.
- *
- * @author bgiles
- */
- public class LoggingSecurityManager extends SecurityManager {
- private AccessControlContext ctx;
- private Set missingPermissions = new HashSet();
- public LoggingSecurityManager() {
- ctx = new AccessControlContext(new ProtectionDomain[] {
- new ProtectionDomain(null, null)
- });
- }
- public LoggingSecurityManager(AccessControlContext ctx) {
- this.ctx = ctx;
- }
- @Override
- public AccessControlContext getSecurityContext() {
- return ctx;
- }
- @Override
- public void checkPermission(Permission perm) {
- try {
- ctx.checkPermission(perm);
- } catch (SecurityException e) {
- if (!missingPermissions.contains(perm)) {
- missingPermissions.add(perm);
- System.out.println(perm);
- }
- }
- }
- public Set getMissingPermissions() {
- return missingPermissions;
- }
- }
/** * Security manager that records missing permissions but otherwise * allows everything. * * @author bgiles */ public class LoggingSecurityManager extends SecurityManager { private AccessControlContext ctx; private Set missingPermissions = new HashSet(); public LoggingSecurityManager() { ctx = new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, null) }); } public LoggingSecurityManager(AccessControlContext ctx) { this.ctx = ctx; } @Override public AccessControlContext getSecurityContext() { return ctx; } @Override public void checkPermission(Permission perm) { try { ctx.checkPermission(perm); } catch (SecurityException e) { if (!missingPermissions.contains(perm)) { missingPermissions.add(perm); System.out.println(perm); } } } public Set getMissingPermissions() { return missingPermissions; } }
A good initial set of Permissions follow. This approach is nice because it would be easy to write the permissions as a standard Policy file.
- public class LoggingSecurityManager extends SecurityManager {
- private AccessControlContext ctx;
- private Properties properties = new Properties;
- private Set missingProperties = new HashSet();
- public LoggingSecurityManager() {
- properties.add(
- new FilePermission(System.get("java.io.tmpdir") + "/-", "read,write,delete"));
- // maybe...
- properties.add(
- new FilePermission(System.get("user.home") + "/-", "read,write,delete"));
- addSystemPropertyPermissions();
- addSecurityPermissions();
- addClassPathPermissions();
- addOtherPropertyPermissions();
- permissions.add(new RuntimePermission("accessClassInPackage.sun.reflect"));
- permissions.add(new RuntimePermission("accessClassInPackage.sun.jdbc.odbc"));
- permissions.add(new RuntimePermission("accessClassInPackage.sun.security.provider"));
- permissions.add(new SocketPermission("localhost", "resolve"));
- permissions.add(new NetPermission("getProxySelector"));
- // scary permissions required for constructors
- permissions.add(new ReflectPermission("suppressAccessChecks")); (!!)
- permissions.add(new RuntimePermission("createClassLoader")); (!!)
- permissions.add(new SecurityPermission("putProviderProperty.SUN"));
- permissions.add(new RuntimePermission("readFileDescriptor"));
- permissions.add(new RuntimePermission("writeFileDescriptor"));
- ctx = new AccessControlContext(new ProtectionDomain[] {
- new ProtectionDomain(null, permissions)
- });
- }
- /**
- * Add read-only permission to read system properties.
- * We may want to filter this list to remove sensitive information
- */
- public void addSystemPropertyPermissions() {
- for (Object key : Collections.list(System.getProperties().keys())) {
- permissions.add(new PropertyPermission((String) key, "read"));
- }
- }
- /**
- * Add read-only permissions for initializing security.
- */
- public void addSecurityPermissions() {
- permissions.add(new SecurityPermission("getPolicy"));
- permissions.add(new SecurityPermission("getProperty.random.source"));
- permissions.add(new SecurityPermission("getProperty.securerandom.source"));
- for (int i = 1; i < 10; i++) {
- permissions.add(new SecurityPermission("getProperty.security.provider." + i));
- }
- String s = Security.getProperty("securerandom.source");
- if ((s != null) && s.startsWith("file:/")) {
- permissions.add(new FilePermission(s.substring(5), "read"));
- }
- // should have been covered already but wasn't....
- permissions.add(new FilePermission("/dev/random", "read"));
- }
- /**
- * Add read-only permissions for everything on classpath.
- */
- public void addClassPathPermissions() {
- permissions.add(new FilePermission(String.format("%/lib/-",
- System.getProperty("java.home")), "read"));
- // add standard class path.
- String pathSep = System.getProperty("path.separator");
- for (String entry : System.getProperty("java.class.path").split(pathSep)) {
- File f = new File(entry);
- if (f.isFile()) {
- permissions.add(new FilePermission(entry, "read"));
- } else if (f.isDirectory()) {
- permissions.add(new FilePermission(String.format("%s/-", entry), "read"));
- } // or could be neither fish nor fowl
- }
- // add endorsed extensions.
- for (String dir : System.getProperty("java.ext.dirs").split(pathSep)) {
- permissions.add(new FilePermission(String.format("%s/-", dir), "read"));
- }
- }
- /**
- * Add other standard properties.
- */
- public void addOtherPropertyPermissions() {
- permissions.add(new PropertyPermission("jdbc.drivers", "read"));
- permissions.add(new PropertyPermission("java.security.egd", "read"));
- permissions.add(new PropertyPermission("socksProxyHost", "read"));
- }
- }
public class LoggingSecurityManager extends SecurityManager { private AccessControlContext ctx; private Properties properties = new Properties; private Set missingProperties = new HashSet(); public LoggingSecurityManager() { properties.add( new FilePermission(System.get("java.io.tmpdir") + "/-", "read,write,delete")); // maybe... properties.add( new FilePermission(System.get("user.home") + "/-", "read,write,delete")); addSystemPropertyPermissions(); addSecurityPermissions(); addClassPathPermissions(); addOtherPropertyPermissions(); permissions.add(new RuntimePermission("accessClassInPackage.sun.reflect")); permissions.add(new RuntimePermission("accessClassInPackage.sun.jdbc.odbc")); permissions.add(new RuntimePermission("accessClassInPackage.sun.security.provider")); permissions.add(new SocketPermission("localhost", "resolve")); permissions.add(new NetPermission("getProxySelector")); // scary permissions required for constructors permissions.add(new ReflectPermission("suppressAccessChecks")); (!!) permissions.add(new RuntimePermission("createClassLoader")); (!!) permissions.add(new SecurityPermission("putProviderProperty.SUN")); permissions.add(new RuntimePermission("readFileDescriptor")); permissions.add(new RuntimePermission("writeFileDescriptor")); ctx = new AccessControlContext(new ProtectionDomain[] { new ProtectionDomain(null, permissions) }); } /** * Add read-only permission to read system properties. * We may want to filter this list to remove sensitive information */ public void addSystemPropertyPermissions() { for (Object key : Collections.list(System.getProperties().keys())) { permissions.add(new PropertyPermission((String) key, "read")); } } /** * Add read-only permissions for initializing security. */ public void addSecurityPermissions() { permissions.add(new SecurityPermission("getPolicy")); permissions.add(new SecurityPermission("getProperty.random.source")); permissions.add(new SecurityPermission("getProperty.securerandom.source")); for (int i = 1; i < 10; i++) { permissions.add(new SecurityPermission("getProperty.security.provider." + i)); } String s = Security.getProperty("securerandom.source"); if ((s != null) && s.startsWith("file:/")) { permissions.add(new FilePermission(s.substring(5), "read")); } // should have been covered already but wasn't.... permissions.add(new FilePermission("/dev/random", "read")); } /** * Add read-only permissions for everything on classpath. */ public void addClassPathPermissions() { permissions.add(new FilePermission(String.format("%/lib/-", System.getProperty("java.home")), "read")); // add standard class path. String pathSep = System.getProperty("path.separator"); for (String entry : System.getProperty("java.class.path").split(pathSep)) { File f = new File(entry); if (f.isFile()) { permissions.add(new FilePermission(entry, "read")); } else if (f.isDirectory()) { permissions.add(new FilePermission(String.format("%s/-", entry), "read")); } // or could be neither fish nor fowl } // add endorsed extensions. for (String dir : System.getProperty("java.ext.dirs").split(pathSep)) { permissions.add(new FilePermission(String.format("%s/-", dir), "read")); } } /** * Add other standard properties. */ public void addOtherPropertyPermissions() { permissions.add(new PropertyPermission("jdbc.drivers", "read")); permissions.add(new PropertyPermission("java.security.egd", "read")); permissions.add(new PropertyPermission("socksProxyHost", "read")); } }
Finally we can provide programmatic checks within our SecurityManager. This is less desirable since it would not be possible to replace them with standard policy file but it might be the best (or only) way to solve the problem.
For instance, if we need to be able to connect to any MySQL server we can use
- public class MySqlSecurityManager extends SecurityManager {
- /**
- * Allow connection to any mysql server
- */
- public void checkPermission(Permission perm) {
- if ((perm instanceof SocketPermission) && perm.getName().endsWith(":3306")
- && perm.getActions().contains("connect")) {
- return;
- }
- super.checkPermission(perm);
- }
- }
public class MySqlSecurityManager extends SecurityManager { /** * Allow connection to any mysql server */ public void checkPermission(Permission perm) { if ((perm instanceof SocketPermission) && perm.getName().endsWith(":3306") && perm.getActions().contains("connect")) { return; } super.checkPermission(perm); } }