@@ -29,11 +29,14 @@ import org.sonar.ce.ComputeEngine; | |||
import org.sonar.ce.ComputeEngineImpl; | |||
import org.sonar.ce.container.ComputeEngineContainerImpl; | |||
import org.sonar.ce.logging.CeProcessLogging; | |||
import org.sonar.ce.security.PluginCeRule; | |||
import org.sonar.process.MinimumViableSystem; | |||
import org.sonar.process.Monitored; | |||
import org.sonar.process.PluginFileWriteRule; | |||
import org.sonar.process.PluginSecurityManager; | |||
import org.sonar.process.ProcessEntryPoint; | |||
import org.sonar.process.ProcessProperties; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.SecurityManagement; | |||
import static com.google.common.base.Preconditions.checkState; | |||
@@ -117,7 +120,12 @@ public class CeServer implements Monitored { | |||
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); | |||
Props props = entryPoint.getProps(); | |||
new CeProcessLogging().configure(props); | |||
SecurityManagement.restrictPlugins(); | |||
PluginFileWriteRule writeRule = new PluginFileWriteRule( | |||
props.nonNullValueAsFile(ProcessProperties.Property.PATH_HOME.getKey()).toPath(), | |||
props.nonNullValueAsFile(ProcessProperties.Property.PATH_TEMP.getKey()).toPath()); | |||
PluginCeRule ceRule = new PluginCeRule(); | |||
PluginSecurityManager.restrictPlugins(writeRule, ceRule); | |||
CeServer server = new CeServer( | |||
new ComputeEngineImpl(props, new ComputeEngineContainerImpl()), |
@@ -0,0 +1,53 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.ce.security; | |||
import java.security.Permission; | |||
import java.security.SecurityPermission; | |||
import java.util.Arrays; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
import org.sonar.process.PluginPolicyRule; | |||
public class PluginCeRule implements PluginPolicyRule { | |||
private static final Set<String> BLOCKED_RUNTIME_PERMISSIONS = new HashSet<>(Arrays.asList( | |||
"createClassLoader", | |||
"getClassLoader", | |||
"setContextClassLoader", | |||
"enableContextClassLoaderOverride", | |||
"closeClassLoader", | |||
"setSecurityManager", | |||
"createSecurityManager" | |||
)); | |||
private static final Set<String> BLOCKED_SECURITY_PERMISSIONS = new HashSet<>(Arrays.asList( | |||
"createAccessControlContext", | |||
"setPolicy" | |||
)); | |||
@Override public boolean implies(Permission permission) { | |||
if (permission instanceof RuntimePermission && BLOCKED_RUNTIME_PERMISSIONS.contains(permission.getName())) { | |||
return false; | |||
} | |||
if (permission instanceof SecurityPermission && BLOCKED_SECURITY_PERMISSIONS.contains(permission.getName())) { | |||
return false; | |||
} | |||
return true; | |||
} | |||
} |
@@ -0,0 +1,46 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.ce.security; | |||
import java.security.Permission; | |||
import java.security.SecurityPermission; | |||
import org.junit.Test; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class PluginCeRuleTest { | |||
private final PluginCeRule rule = new PluginCeRule(); | |||
private final Permission allowedRuntime = new RuntimePermission("getFileSystemAttributes"); | |||
private final Permission deniedRuntime = new RuntimePermission("getClassLoader"); | |||
private final Permission allowedSecurity = new SecurityPermission("getProperty.key"); | |||
private final Permission deniedSecurity = new SecurityPermission("setPolicy"); | |||
@Test | |||
public void rule_restricts_denied_permissions() { | |||
assertThat(rule.implies(deniedSecurity)).isFalse(); | |||
assertThat(rule.implies(deniedRuntime)).isFalse(); | |||
} | |||
@Test | |||
public void rule_allows_permissions() { | |||
assertThat(rule.implies(allowedSecurity)).isTrue(); | |||
assertThat(rule.implies(allowedRuntime)).isTrue(); | |||
} | |||
} |
@@ -0,0 +1,55 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.process; | |||
import java.io.File; | |||
import java.io.FilePermission; | |||
import java.nio.file.Path; | |||
import java.security.Permission; | |||
import java.util.Arrays; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
public class PluginFileWriteRule implements PluginPolicyRule { | |||
private static final Set<String> BLOCKED_FILE_ACTIONS = new HashSet<>(Arrays.asList( | |||
"write", | |||
"delete", | |||
"execute" | |||
)); | |||
private final FilePermission blockedFilePermission; | |||
private final FilePermission tmpFilePermission; | |||
public PluginFileWriteRule(Path home, Path tmp) { | |||
blockedFilePermission = new FilePermission(home.toAbsolutePath().toString() + File.separatorChar + "-", String.join(",", BLOCKED_FILE_ACTIONS)); | |||
tmpFilePermission = new FilePermission(tmp.toAbsolutePath().toString() + File.separatorChar + "-", String.join(",", BLOCKED_FILE_ACTIONS)); | |||
} | |||
@Override | |||
public boolean implies(Permission permission) { | |||
if (permission instanceof FilePermission) { | |||
FilePermission requestPermission = (FilePermission) permission; | |||
if (blockedFilePermission.implies(requestPermission) && !tmpFilePermission.implies(requestPermission)) { | |||
return false; | |||
} | |||
} | |||
return true; | |||
} | |||
} |
@@ -0,0 +1,26 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.process; | |||
import java.security.Permission; | |||
public interface PluginPolicyRule { | |||
boolean implies(Permission permission); | |||
} |
@@ -26,21 +26,19 @@ import java.security.Permissions; | |||
import java.security.Policy; | |||
import java.security.ProtectionDomain; | |||
import java.security.Security; | |||
import java.security.SecurityPermission; | |||
import java.util.Arrays; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
import java.util.List; | |||
public class SecurityManagement { | |||
public class PluginSecurityManager { | |||
private static final String CACHE_TTL_KEY = "networkaddress.cache.ttl"; | |||
private SecurityManagement() { | |||
private PluginSecurityManager() { | |||
// static only | |||
} | |||
public static void restrictPlugins() { | |||
public static void restrictPlugins(PluginPolicyRule... rules) { | |||
SecurityManager sm = new SecurityManager(); | |||
Policy.setPolicy(new CustomPolicy()); | |||
Policy.setPolicy(new PluginPolicy(Arrays.asList(rules))); | |||
System.setSecurityManager(sm); | |||
// SONAR-14870 By default, with a security manager installed, the DNS cache never times out. See InetAddressCachePolicy. | |||
if (Security.getProperty(CACHE_TTL_KEY) == null) { | |||
@@ -48,32 +46,19 @@ public class SecurityManagement { | |||
} | |||
} | |||
static class CustomPolicy extends Policy { | |||
private static final Set<String> BLOCKED_RUNTIME_PERMISSIONS = new HashSet<>(Arrays.asList( | |||
"createClassLoader", | |||
"getClassLoader", | |||
"setContextClassLoader", | |||
"enableContextClassLoaderOverride", | |||
"closeClassLoader", | |||
"setSecurityManager", | |||
"createSecurityManager" | |||
)); | |||
private static final Set<String> BLOCKED_SECURITY_PERMISSIONS = new HashSet<>(Arrays.asList( | |||
"createAccessControlContext", | |||
"setPolicy" | |||
)); | |||
static class PluginPolicy extends Policy { | |||
private final List<PluginPolicyRule> rules; | |||
PluginPolicy(List<PluginPolicyRule> rules) { | |||
this.rules = rules; | |||
} | |||
@Override | |||
public boolean implies(ProtectionDomain domain, Permission permission) { | |||
// classloader used to load plugins | |||
String clName = getDomainClassLoaderName(domain); | |||
if ("org.sonar.classloader.ClassRealm".equals(clName)) { | |||
if (permission instanceof RuntimePermission && BLOCKED_RUNTIME_PERMISSIONS.contains(permission.getName())) { | |||
return false; | |||
} | |||
if (permission instanceof SecurityPermission && BLOCKED_SECURITY_PERMISSIONS.contains(permission.getName())) { | |||
return false; | |||
} | |||
return rules.stream().allMatch(p -> p.implies(permission)); | |||
} | |||
return true; | |||
} |
@@ -0,0 +1,52 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.process; | |||
import java.io.FilePermission; | |||
import java.nio.file.Path; | |||
import java.nio.file.Paths; | |||
import java.util.PropertyPermission; | |||
import org.junit.Test; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class PluginFileWriteRuleTest { | |||
private final Path home = Paths.get("/path/to/home"); | |||
private final Path tmp = Paths.get("/path/to/home/tmp"); | |||
private final PluginFileWriteRule rule = new PluginFileWriteRule(home, tmp); | |||
@Test | |||
public void policy_restricts_modifying_home() { | |||
assertThat(rule.implies(new FilePermission(Paths.get("/path/to/home/file").toAbsolutePath().toString(), "write"))).isFalse(); | |||
assertThat(rule.implies(new FilePermission(Paths.get("/path/to/home/file").toAbsolutePath().toString(), "execute"))).isFalse(); | |||
assertThat(rule.implies(new FilePermission(Paths.get("/path/to/home/file").toAbsolutePath().toString(), "delete"))).isFalse(); | |||
assertThat(rule.implies(new FilePermission(Paths.get("/path/to/home/file").toAbsolutePath().toString(), "read"))).isTrue(); | |||
assertThat(rule.implies(new FilePermission(Paths.get("/path/to/home/file").toAbsolutePath().toString(), "readlink"))).isTrue(); | |||
assertThat(rule.implies(new FilePermission(Paths.get("/path/to/home/extensions/file").toAbsolutePath().toString(), "write"))).isFalse(); | |||
assertThat(rule.implies(new FilePermission(Paths.get("/path/to/").toAbsolutePath().toString(), "write"))).isTrue(); | |||
} | |||
@Test | |||
public void policy_implies_other_permissions() { | |||
assertThat(rule.implies(new PropertyPermission(Paths.get("/path/to/").toAbsolutePath().toString(), "write"))).isTrue(); | |||
} | |||
} |
@@ -21,59 +21,63 @@ package org.sonar.process; | |||
import java.security.Permission; | |||
import java.security.ProtectionDomain; | |||
import java.security.SecurityPermission; | |||
import java.util.Arrays; | |||
import javax.management.MBeanPermission; | |||
import org.junit.Test; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.RETURNS_DEEP_STUBS; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.verifyNoInteractions; | |||
import static org.mockito.Mockito.verifyNoMoreInteractions; | |||
import static org.mockito.Mockito.when; | |||
public class SecurityManagementTest { | |||
private ClassLoader classRealm = mock(ClassLoader.class, RETURNS_DEEP_STUBS); | |||
private ProtectionDomain pd = new ProtectionDomain(null, null, classRealm, null); | |||
public class PluginSecurityManagerTest { | |||
private final ClassLoader classRealm = mock(ClassLoader.class, RETURNS_DEEP_STUBS); | |||
private final ProtectionDomain pd = new ProtectionDomain(null, null, classRealm, null); | |||
private final Permission permission = mock(Permission.class); | |||
private final PluginPolicyRule rule1 = mock(PluginPolicyRule.class); | |||
private final PluginPolicyRule rule2 = mock(PluginPolicyRule.class); | |||
private Permission allowedRuntime = new RuntimePermission("getFileSystemAttributes"); | |||
private Permission deniedRuntime = new RuntimePermission("getClassLoader"); | |||
private Permission allowedSecurity = new SecurityPermission("getProperty.key"); | |||
private Permission deniedSecurity = new SecurityPermission("setPolicy"); | |||
@Test | |||
public void protection_domain_can_have_no_classloader() { | |||
PluginSecurityManager.PluginPolicy policy = new PluginSecurityManager.PluginPolicy(Arrays.asList(rule1, rule2)); | |||
ProtectionDomain domain = new ProtectionDomain(null, null, null, null); | |||
Permission permission = new MBeanPermission("com.sun.management.internal.HotSpotThreadImpl", "getMBeanInfo"); | |||
assertThat(policy.implies(domain, permission)).isTrue(); | |||
verifyNoInteractions(rule1, rule2); | |||
} | |||
@Test | |||
public void policy_restricts_class_realm() { | |||
SecurityManagement.CustomPolicy policy = new SecurityManagement.CustomPolicy() { | |||
public void policy_doesnt_restrict_other_classloaders() { | |||
PluginSecurityManager.PluginPolicy policy = new PluginSecurityManager.PluginPolicy(Arrays.asList(rule1, rule2)) { | |||
@Override | |||
String getDomainClassLoaderName(ProtectionDomain domain) { | |||
return "org.sonar.classloader.ClassRealm"; | |||
return "classloader"; | |||
} | |||
}; | |||
assertThat(policy.implies(pd, allowedSecurity)).isTrue(); | |||
assertThat(policy.implies(pd, deniedSecurity)).isFalse(); | |||
assertThat(policy.implies(pd, allowedRuntime)).isTrue(); | |||
assertThat(policy.implies(pd, deniedRuntime)).isFalse(); | |||
policy.implies(pd, permission); | |||
verifyNoInteractions(rule1, rule2); | |||
} | |||
@Test | |||
public void policy_does_not_restrict_other_classloaders() { | |||
SecurityManagement.CustomPolicy policy = new SecurityManagement.CustomPolicy() { | |||
public void policy_restricts_class_realm_classloader() { | |||
when(rule1.implies(permission)).thenReturn(true); | |||
PluginSecurityManager.PluginPolicy policy = new PluginSecurityManager.PluginPolicy(Arrays.asList(rule1, rule2)) { | |||
@Override | |||
String getDomainClassLoaderName(ProtectionDomain domain) { | |||
return "classloader"; | |||
return "org.sonar.classloader.ClassRealm"; | |||
} | |||
}; | |||
assertThat(policy.implies(pd, allowedSecurity)).isTrue(); | |||
assertThat(policy.implies(pd, deniedSecurity)).isTrue(); | |||
assertThat(policy.implies(pd, allowedRuntime)).isTrue(); | |||
assertThat(policy.implies(pd, deniedRuntime)).isTrue(); | |||
policy.implies(pd, permission); | |||
verify(rule1).implies(permission); | |||
verify(rule2).implies(permission); | |||
verifyNoMoreInteractions(rule1, rule2); | |||
} | |||
@Test | |||
public void protection_domain_can_have_no_classloader() { | |||
SecurityManagement.CustomPolicy policy = new SecurityManagement.CustomPolicy(); | |||
ProtectionDomain domain = new ProtectionDomain(null, null, null, null); | |||
Permission permission = new MBeanPermission("com.sun.management.internal.HotSpotThreadImpl", "getMBeanInfo"); | |||
assertThat(policy.implies(domain, permission)).isTrue(); | |||
} | |||
} |
@@ -24,8 +24,11 @@ import java.io.File; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.process.MinimumViableSystem; | |||
import org.sonar.process.Monitored; | |||
import org.sonar.process.PluginFileWriteRule; | |||
import org.sonar.process.PluginSecurityManager; | |||
import org.sonar.process.ProcessEntryPoint; | |||
import org.sonar.process.ProcessId; | |||
import org.sonar.process.ProcessProperties; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.sharedmemoryfile.DefaultProcessCommands; | |||
@@ -95,6 +98,13 @@ public class WebServer implements Monitored { | |||
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); | |||
Props props = entryPoint.getProps(); | |||
new WebServerProcessLogging().configure(props); | |||
PluginFileWriteRule writeRule = new PluginFileWriteRule( | |||
props.nonNullValueAsFile(ProcessProperties.Property.PATH_HOME.getKey()).toPath(), | |||
props.nonNullValueAsFile(ProcessProperties.Property.PATH_TEMP.getKey()).toPath()); | |||
PluginSecurityManager.restrictPlugins(writeRule); | |||
WebServer server = new WebServer(props); | |||
entryPoint.launch(server); | |||
} |