Browse Source

SONAR-14886 Plugins should not modify SonarQube's home directory

tags/9.0.0.45539
Duarte Meneses 3 years ago
parent
commit
3ea8726919

+ 10
- 2
server/sonar-ce/src/main/java/org/sonar/ce/app/CeServer.java View File

@@ -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()),

+ 53
- 0
server/sonar-ce/src/main/java/org/sonar/ce/security/PluginCeRule.java View File

@@ -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;
}
}

+ 46
- 0
server/sonar-ce/src/test/java/org/sonar/ce/security/PluginCeRuleTest.java View File

@@ -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();
}
}

+ 55
- 0
server/sonar-process/src/main/java/org/sonar/process/PluginFileWriteRule.java View File

@@ -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;
}
}

+ 26
- 0
server/sonar-process/src/main/java/org/sonar/process/PluginPolicyRule.java View File

@@ -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);
}

server/sonar-process/src/main/java/org/sonar/process/SecurityManagement.java → server/sonar-process/src/main/java/org/sonar/process/PluginSecurityManager.java View File

@@ -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;
}

+ 52
- 0
server/sonar-process/src/test/java/org/sonar/process/PluginFileWriteRuleTest.java View File

@@ -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();
}
}

server/sonar-process/src/test/java/org/sonar/process/SecurityManagementTest.java → server/sonar-process/src/test/java/org/sonar/process/PluginSecurityManagerTest.java View File

@@ -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();
}
}

+ 10
- 0
server/sonar-webserver/src/main/java/org/sonar/server/app/WebServer.java View File

@@ -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);
}

Loading…
Cancel
Save