mirror of
https://github.com/corda/corda.git
synced 2025-06-16 22:28:15 +00:00
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:
@ -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?>) {
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
Reference in New Issue
Block a user