package com.voxeo.tropo; import java.awt.AWTPermission; import java.io.File; import java.io.FilePermission; import java.net.SocketPermission; import java.net.URL; import java.security.KeyStore; import java.security.MessageDigest; import java.security.Permission; import java.security.Security; import java.security.SecurityPermission; import java.security.Signature; import java.util.HashSet; import java.util.Map; import java.util.PropertyPermission; import java.util.Set; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.net.ssl.SSLPermission; import javax.security.auth.kerberos.DelegationPermission; import javax.servlet.ServletContext; import javax.sound.sampled.AudioPermission; import org.apache.log4j.Logger; import sun.security.util.SecurityConstants; import com.voxeo.tropo.app.ApplicationInstance; import com.voxeo.tropo.app.LocalApplication; import com.voxeo.tropo.app.SimpleInstance; import com.voxeo.tropo.util.Utils; /** * * all comparisons should be in lower case since getRealPath() will return in lower * case no matter whether there are capital letters in the pat or not */ public class ScriptSecurityManager extends SecurityManager { private static final Logger LOG = Logger.getLogger(ScriptSecurityManager.class); private String _base; private String _common; private String _shared; private String _server; private String _bin; private String _jdk; private String _jruby; private String _jython; private boolean _windows; private String[] _scriptCodeBases = new String[]{ "org.jruby", "org.python", "com.ziclix.python", "org.mozilla.javascript", "org.codehaus.groovy", "groovy", "com.caucho", }; /** * key-action * * value- a set of all allowed target which starts with something in this set */ private Map<String, Set<String>> _allow = null; /** * key-action * * value- a set of all disallowed target which starts with something in this set */ private Map<String, Set<String>> _forbid = null; public ScriptSecurityManager(ServletContext ctx) { _windows = System.getProperty("os.name").startsWith("Windows") ? true : false; _base = normalize(ctx.getRealPath("/")); // getRealPath() always return in lower case _common = _base.substring(0, _base.length() - 11) + "common" + File.separator; _shared = _base.substring(0, _base.length() - 11) + "shared" + File.separator; _server = _base.substring(0, _base.length() - 11) + "server" + File.separator; _bin = _base.substring(0, _base.length() - 11) + "bin" + File.separator; _jdk = normalize(System.getProperty("java.home")); _jdk = _jdk.substring(0, _jdk.lastIndexOf(File.separator)); _jruby = normalize(Utils.rubyHome()); _jython = normalize(Utils.pythonHome()); LOG.info("Tropo base directory is " + _base); LOG.info("Tropo JDK directory is " + _jdk); LOG.info("Tropo JRuby directory is " + _jruby); LOG.info("Tropo Jython directory is " + _jython); _allow = Configuration.get().getSandboxAllow(); _forbid = Configuration.get().getSandboxForbid(); allowSysTempDirs(); LOG.info("Allow preconfigured permissions:" + _allow); LOG.info("Forbid preconfigured permissions:" + _forbid); securityInitialization(); } public void allowSysTempDirs() { Set<String> s = new HashSet(); // for MAC OS String t = System.getenv("TMPDIR"); if (t != null && t.length() > 0) { s.add(normalize(t)); } // for Windows OS t = System.getenv("TEMP"); if (t != null && t.length() > 0) { s.add(normalize(t)); } t = System.getenv("TMP"); if (t != null && t.length() > 0) { s.add(normalize(t)); } // allowing all permissions addRestrictions(_allow, SecurityConstants.FILE_READ_ACTION, s); addRestrictions(_allow, SecurityConstants.FILE_WRITE_ACTION, s); addRestrictions(_allow, SecurityConstants.FILE_DELETE_ACTION, s); addRestrictions(_allow, SecurityConstants.FILE_EXECUTE_ACTION, s); } private void addRestrictions(Map<String, Set<String>> m, String key, Set<String> s) { Set<String> t = m.get(key); if (t == null) { m.put(key, s); } else { t.addAll(s); } } public boolean isPreconfiguredAllow(String action, String target) { Set<String> e = _allow.get(action); if (e != null) { for (String s : e) { if (target.startsWith(s)) { return true; } } } return false; } public boolean isPreconfiguredForbid(String action, String target) { Set<String> e = _forbid.get(action); if (e != null) { for (String s : e) { if (target.startsWith(s)) { return true; } } } return false; } void securityInitialization() { try { Security.getProviders(); } catch (Throwable t) { ; //ignore } Set<String> names = null; names = Security.getAlgorithms("Signature"); for (String name : names) { try { Signature.getInstance(name); } catch (Throwable t) { ; //ignore } } names = Security.getAlgorithms("MessageDigest"); for (String name : names) { try { MessageDigest.getInstance(name); } catch (Throwable t) { ; //ignore } } names = Security.getAlgorithms("Cipher"); for (String name : names) { try { Cipher.getInstance(name); } catch (Throwable t) { ; //ignore } } names = Security.getAlgorithms("Mac"); for (String name : names) { try { Mac.getInstance(name); } catch (Throwable t) { ; //ignore } } names = Security.getAlgorithms("KeyStore"); for (String name : names) { try { KeyStore.getInstance(name); } catch (Throwable t) { ; //ignore } } try { new URL("https://www.verisign.com/").openStream(); } catch (Throwable t) { ; //ignore } } @Override public void checkExit(int code) { ApplicationInstance ai = currentApplicationInstance(); if (ai != null) { if (LOG.isDebugEnabled()) { LOG.debug("No exitVM RuntimePermisson."); } throw new SecurityException("No exitVM RuntimePermisson."); } super.checkExit(code); } @Override public void checkExec(String cmd) { if (isPreconfiguredForbid(SecurityConstants.FILE_EXECUTE_ACTION, cmd)) { if (LOG.isDebugEnabled()) { LOG.debug("No " + SecurityConstants.FILE_EXECUTE_ACTION + " FilePermission to command '" + cmd + "'"); } throw new SecurityException("No " + SecurityConstants.FILE_EXECUTE_ACTION + " FilePermission to command '" + cmd + "'"); } if (isPreconfiguredAllow(SecurityConstants.FILE_EXECUTE_ACTION, cmd)) { return; } try { super.checkExec(cmd); } catch (SecurityException e) { LOG.error("No " + SecurityConstants.FILE_EXECUTE_ACTION + " FilePermission to command '" + cmd + "'"); throw e; } } @Override public void checkPermission(Permission p) { checkPermission(p, null); } @Override public void checkPermission(Permission p, Object ctx) { ApplicationInstance ai = currentApplicationInstance(); if (ai != null) { if (p instanceof FilePermission) { String target = normalize(p.getName()); String action = p.getActions().toLowerCase(); String abase = normalize(((LocalApplication)(ai.getApp())).getBaseDir()); // check the forbid configuration in tropo.xml first if (isPreconfiguredForbid(action, target)) { if (LOG.isDebugEnabled()) { LOG.debug("No " + action + " FilePermission to " + target); } throw new SecurityException("No " + action + " FilePermission to " + target); } // check the allow configuration in tropo.xml if (isPreconfiguredAllow(action, target)) { return; } if (target.startsWith(abase)) { if (SecurityConstants.FILE_EXECUTE_ACTION.equals(action)) { if (LOG.isDebugEnabled()) { LOG.debug("No execute FilePermission to " + target); } throw new SecurityException("No execute FilePermission to " + target); } else { return; } } else if (isReservedTarget(target)) { if (SecurityConstants.FILE_READ_ACTION.equals(action)) { return; } else if ((target.startsWith(_jython) || target.startsWith(_jruby)) && target.endsWith(".class") && SecurityConstants.FILE_WRITE_ACTION.equals(action)) { return; } else { if (LOG.isDebugEnabled()) { LOG.debug("No " + action + " FilePermission to " + target); } throw new SecurityException("No " + action + " FilePermission to " + target); } } else if (target.startsWith(_base)) { if (SecurityConstants.FILE_READ_ACTION.equals(action)) { return; } if (SecurityConstants.FILE_WRITE_ACTION.equals(action) && (target.startsWith(_base + "logs") || target.startsWith(_base + "slogs"))) { return; } else { if (LOG.isDebugEnabled()) { LOG.debug("No " + action + " FilePermission to " + target); } throw new SecurityException("No " + action + " FilePermission to " + target); } } else if (target.indexOf("__classpath__") >= 0) { if (SecurityConstants.FILE_READ_ACTION.equals(action)) { return; } else { if (LOG.isDebugEnabled()) { LOG.debug("No " + action + " FilePermission to " + target); } throw new SecurityException("No " + action + " FilePermission to " + target); } } else if (target.indexOf("groovy\\script") >= 0 || target.indexOf("/var/tmp/java3d/") >= 0 || target.indexOf("/groovy/script") >= 0) { //this is a temporary fix for running groovy return; } else if (target.toLowerCase().indexOf("c:\\documents and settings") >= 0 ) { //this is a temporary fix for running jruby / require 'rest_client' , script/rest_client.rb return; } else { if (LOG.isDebugEnabled()) { LOG.debug("No " + action + " FilePermission to " + target); } throw new SecurityException("No " + action + " FilePermission to " + target); } } else if (p instanceof RuntimePermission) { String target = p.getName(); if ("exitVM".equals(target) || "stopThread".equals(target) || "loadLibrary".equals(target) || "setIO".equals(target) || "shutdownHooks".equals(target) || "createSecurityManager".equals(target) || "setSecurityManager".equals(target) || "queuePrintJob".equals(target) || "setFactory".equals(target)) { if (LOG.isDebugEnabled()) { LOG.debug("No RuntimePermission to " + target); } throw new SecurityException("No RuntimePermission to " + target); } } else if (p instanceof AudioPermission) { if (LOG.isDebugEnabled()) { LOG.debug("No AudioPermission to " + p.getName()); } throw new SecurityException("No AudioPermission to " + p.getName()); } else if (p instanceof AWTPermission) { if (LOG.isDebugEnabled()) { LOG.debug("No AWTPermission to " + p.getName()); } throw new SecurityException("No AWTPermission to " + p.getName()); } else if (p instanceof DelegationPermission) { if (LOG.isDebugEnabled()) { LOG.debug("No DelegationPermission to " + p.getName()); } throw new SecurityException("No DelegationPermission to " + p.getName()); } else if (p instanceof PropertyPermission) { String target = p.getName(); String action = p.getActions(); if (SecurityConstants.FILE_WRITE_ACTION.equals(action)) { if (LOG.isDebugEnabled()) { LOG.debug("No write PropertyPermission to " + target); } throw new SecurityException("No write PropertyPermission to " + target); } else if (SecurityConstants.FILE_READ_ACTION.equals(action)) { return; } } else if (p instanceof SecurityPermission) { if (p.getName().startsWith("getProperty")) { return; } if (LOG.isDebugEnabled()) { LOG.debug("No SecurityPermission to " + p.getName()); } throw new SecurityException("No SecurityPermission to " + p.getName()); } else if (p instanceof SSLPermission) { if (LOG.isDebugEnabled()) { LOG.debug("No SSLPermission to " + p.getName()); } throw new SecurityException("No SSLPermission to " + p.getName()); } else if (p instanceof SocketPermission) { String action = p.getActions(); if (SecurityConstants.SOCKET_ACCEPT_ACTION.equals(action) || SecurityConstants.SOCKET_LISTEN_ACTION.equals(action)) { if (LOG.isDebugEnabled()) { LOG.debug("No " + action + " SocketPermission to " + p.getName()); } throw new SecurityException("No " + action + " SocketPermission to " + p.getName()); } } } } boolean isReservedTarget(String target) { return target.startsWith(_jdk) || target.startsWith(_common) || target.startsWith(_jruby) || target.startsWith(_jython) || target.startsWith(_shared) || target.startsWith(_server) || target.startsWith(_bin) || target.endsWith("libkeychain.jnilib"); } void printCalls() { for (Class<?> cls : getClassContext()) { System.out.println(cls); } } String normalize(String str) { if (_windows && str != null) { if (str.indexOf(File.separator) == 0) { str = str.substring(1); } return str.toLowerCase().replace("/", File.separator); } return str.toLowerCase(); } boolean isTrusted() { for (Class<?> cls : getClassContext()) { String name = cls.getName(); if (isScriptCodeBase(name)) { return false; } else if (name.startsWith("com.voxoe.tropo")) { return true; } } return true; } boolean isScriptCodeBase(String name) { for (String s : _scriptCodeBases) { if (name.startsWith(s)) { return true; } } return false; } ApplicationInstance currentApplicationInstance() { String name = Thread.currentThread().getName(); if (name.equals("Script")) { ApplicationInstance ai = SimpleInstance.getCurrentApplicationInstance(); if (ai == null) { SecurityException e = new SecurityException("Invalid Script Thread."); LOG.error("Invalid Script Thread", e); throw e; } return ai; } return null; } }