ENT-2188 fix H2 insecure default configuration (#3692)

Set the "h2.allowedClasses" system property, require database password when exposing H2 server on non-localhost address, samples start H2 server by default (reintroduces the behaviour before h2Settings.address configuration option was added)
This commit is contained in:
szymonsztuka
2018-08-01 11:50:42 +01:00
committed by GitHub
parent 7182542724
commit c23167f08e
19 changed files with 220 additions and 15 deletions

View File

@ -28,7 +28,7 @@ class AddressBindingFailureTests {
fun `rpc admin address`() = assertBindExceptionForOverrides { address -> mapOf("rpcSettings" to mapOf("adminAddress" to address.toString())) }
@Test
fun `H2 address`() = assertBindExceptionForOverrides { address -> mapOf("h2Settings" to mapOf("address" to address.toString())) }
fun `H2 address`() = assertBindExceptionForOverrides { address -> mapOf("h2Settings" to mapOf("address" to address.toString()), "dataSourceProperties.dataSource.password" to "password") }
private fun assertBindExceptionForOverrides(overrides: (NetworkHostAndPort) -> Map<String, Any?>) {

View File

@ -0,0 +1,136 @@
package net.corda.node.persistence
import co.paralleluniverse.fibers.Suspendable
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.node.services.Permissions
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
import net.corda.testing.node.User
import org.junit.Test
import java.net.InetAddress
import java.sql.DriverManager
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
import kotlin.test.assertTrue
class H2SecurityTests {
companion object {
private val port = PortAllocation.Incremental(21_000)
private fun getFreePort() = port.nextPort()
private const val h2AddressKey = "h2Settings.address"
private const val dbPasswordKey = "dataSourceProperties.dataSource.password"
}
@Test
fun `h2 server starts when h2Settings are set`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
val port = getFreePort()
startNode(customOverrides = mapOf(h2AddressKey to "localhost:$port")).getOrThrow()
DriverManager.getConnection("jdbc:h2:tcp://localhost:$port/node", "sa", "").use {
assertTrue(it.createStatement().executeQuery("SELECT 1").next())
}
}
}
@Test
fun `h2 server on the host name requires non-default database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
assertFailsWith(CouldNotCreateDataSourceException::class) {
startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostName}:${getFreePort()}")).getOrThrow()
}
}
}
@Test
fun `h2 server on the external host IP requires non-default database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
assertFailsWith(CouldNotCreateDataSourceException::class) {
startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostAddress}:${getFreePort()}")).getOrThrow()
}
}
}
@Test
fun `h2 server on host name requires non-blank database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
assertFailsWith(CouldNotCreateDataSourceException::class) {
startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostName}:${getFreePort()}",
dbPasswordKey to " ")).getOrThrow()
}
}
}
@Test
fun `h2 server on external host IP requires non-blank database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
assertFailsWith(CouldNotCreateDataSourceException::class) {
startNode(customOverrides = mapOf(h2AddressKey to "${InetAddress.getLocalHost().hostAddress}:${getFreePort()}",
dbPasswordKey to " ")).getOrThrow()
}
}
}
@Test
fun `h2 server on localhost runs with the default database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList())) {
startNode(customOverrides = mapOf(h2AddressKey to "localhost:${getFreePort()}")).getOrThrow()
}
}
@Test
fun `h2 server to loopback IP runs with the default database password`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = isQuasarAgentSpecified(), notarySpecs = emptyList())) {
startNode(customOverrides = mapOf(h2AddressKey to "127.0.0.1:${getFreePort()}")).getOrThrow()
}
}
@Test
fun `remote code execution via h2 server is disabled`() {
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList())) {
val port = getFreePort()
startNode(customOverrides = mapOf(h2AddressKey to "localhost:$port", dbPasswordKey to "x")).getOrThrow()
DriverManager.getConnection("jdbc:h2:tcp://localhost:$port/node", "sa", "x").use {
assertFailsWith(org.h2.jdbc.JdbcSQLException::class) {
it.createStatement().execute("CREATE ALIAS SET_PROPERTY FOR \"java.lang.System.setProperty\"")
it.createStatement().execute("CALL SET_PROPERTY('abc', '1')")
}
}
assertNull(System.getProperty("abc"))
}
}
@Test
fun `malicious flow tries to enable remote code execution via h2 server`() {
val user = User("mark", "dadada", setOf(Permissions.startFlow<MaliciousFlow>()))
driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, notarySpecs = emptyList())) {
val port = getFreePort()
val nodeHandle = startNode(rpcUsers = listOf(user), customOverrides = mapOf(h2AddressKey to "localhost:$port",
dbPasswordKey to "x")).getOrThrow()
CordaRPCClient(nodeHandle.rpcAddress).start(user.username, user.password).use {
it.proxy.startFlow(::MaliciousFlow).returnValue.getOrThrow()
}
DriverManager.getConnection("jdbc:h2:tcp://localhost:$port/node", "sa", "x").use {
assertFailsWith(org.h2.jdbc.JdbcSQLException::class) {
it.createStatement().execute("CREATE ALIAS SET_PROPERTY FOR \"java.lang.System.setProperty\"")
it.createStatement().execute("CALL SET_PROPERTY('abc', '1')")
}
}
assertNull(System.getProperty("abc"))
}
}
@StartableByRPC
class MaliciousFlow : FlowLogic<Boolean>() {
@Suspendable
override fun call(): Boolean {
System.clearProperty("h2.allowedClasses")
return true
}
}
}

View File

@ -54,6 +54,7 @@ import net.corda.nodeapi.internal.bridging.BridgeControlListener
import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.serialization.internal.*
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
import org.h2.jdbc.JdbcSQLException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@ -61,11 +62,13 @@ import rx.Observable
import rx.Scheduler
import rx.schedulers.Schedulers
import java.net.BindException
import java.net.InetAddress
import java.nio.file.Path
import java.time.Clock
import java.util.concurrent.atomic.AtomicInteger
import javax.management.ObjectName
import kotlin.system.exitProcess
import java.nio.file.Paths
class NodeWithInfo(val node: Node, val info: NodeInfo) {
val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {}
@ -331,13 +334,20 @@ open class Node(configuration: NodeConfiguration,
if (databaseUrl != null && databaseUrl.startsWith(h2Prefix)) {
val effectiveH2Settings = configuration.effectiveH2Settings
//forbid execution of arbitrary code via SQL except those classes required by H2 itself
System.setProperty("h2.allowedClasses", "org.h2.mvstore.db.MVTableEngine,org.locationtech.jts.geom.Geometry,org.h2.server.TcpServer")
if (effectiveH2Settings?.address != null) {
if (!InetAddress.getByName(effectiveH2Settings.address.host).isLoopbackAddress
&& configuration.dataSourceProperties.getProperty("dataSource.password").isBlank()) {
throw CouldNotCreateDataSourceException("Database password is required for H2 server listening on ${InetAddress.getByName(effectiveH2Settings.address.host)}.")
}
val databaseName = databaseUrl.removePrefix(h2Prefix).substringBefore(';')
val baseDir = Paths.get(databaseName).parent.toString()
val server = org.h2.tools.Server.createTcpServer(
"-tcpPort", effectiveH2Settings.address.port.toString(),
"-tcpAllowOthers",
"-tcpDaemon",
"-baseDir", baseDir,
"-key", "node", databaseName)
// override interface that createTcpServer listens on (which is always 0.0.0.0)
System.setProperty("h2.bindAddress", effectiveH2Settings.address.host)