H2 coin selection uses Prepared Statement to protect against SQL injection (#1872)

* Updated H2 coin selection code to use Prepared Statement to protect against SQL Injection attacks.

* Clean-up deprecations and warnings.

* Revert correct indentation.

* Revert logging back to debug for SQL display.

* Fix broken tests.
This commit is contained in:
josecoll 2017-10-13 16:26:55 +01:00 committed by GitHub
parent 2680361696
commit be235673e1
2 changed files with 62 additions and 53 deletions

View File

@ -61,20 +61,10 @@ class CashSelectionH2Impl : CashSelection {
lockId: UUID,
withIssuerRefs: Set<OpaqueBytes>): List<StateAndRef<Cash.State>> {
val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1)
val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1)
val stateAndRefs = mutableListOf<StateAndRef<Cash.State>>()
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
// running total of such an accumulator
// 2) H2 uses session variables to perform this accumulator function:
// http://www.h2database.com/html/functions.html#set
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
for (retryCount in 1..MAX_RETRIES) {
if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, issuerKeysStr, withIssuerRefs, issuerRefsStr, stateAndRefs)) {
if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) {
log.warn("Coin selection failed on attempt $retryCount")
// TODO: revisit the back off strategy for contended spending.
if (retryCount != MAX_RETRIES) {
@ -91,32 +81,52 @@ class CashSelectionH2Impl : CashSelection {
return stateAndRefs
}
private fun attemptSpend(services: ServiceHub, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, issuerKeysStr: String, withIssuerRefs: Set<OpaqueBytes>, issuerRefsStr: String, stateAndRefs: MutableList<StateAndRef<Cash.State>>): Boolean {
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
// running total of such an accumulator
// 2) H2 uses session variables to perform this accumulator function:
// http://www.h2database.com/html/functions.html#set
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
private fun attemptSpend(services: ServiceHub, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, stateAndRefs: MutableList<StateAndRef<Cash.State>>): Boolean {
val connection = services.jdbcSession()
spendLock.withLock {
val statement = services.jdbcSession().createStatement()
val statement = connection.createStatement()
try {
statement.execute("CALL SET(@t, CAST(0 AS BIGINT));")
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
// the softLockReserve update will detect whether we try to lock states locked by others
val selectJoin = """
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
FROM vault_states AS vs, contract_cash_states AS ccs
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
AND vs.state_status = 0
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
""" +
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
FROM vault_states AS vs, contract_cash_states AS ccs
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
AND vs.state_status = 0
AND ccs.ccy_code = ? and @t < ?
AND (vs.lock_id = ? OR vs.lock_id is null)
""" +
(if (notary != null)
" AND vs.notary_name = '${notary.name}'" else "") +
" AND vs.notary_name = ?" else "") +
(if (onlyFromIssuerParties.isNotEmpty())
" AND ccs.issuer_key IN ($issuerKeysStr)" else "") +
" AND ccs.issuer_key IN (?)" else "") +
(if (withIssuerRefs.isNotEmpty())
" AND ccs.issuer_ref IN ($issuerRefsStr)" else "")
" AND ccs.issuer_ref IN (?)" else "")
// Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection)
val psSelectJoin = connection.prepareStatement(selectJoin)
var pIndex = 0
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
psSelectJoin.setLong(++pIndex, amount.quantity)
psSelectJoin.setString(++pIndex, lockId.toString())
if (notary != null)
psSelectJoin.setString(++pIndex, notary.name.toString())
if (onlyFromIssuerParties.isNotEmpty())
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() )
if (withIssuerRefs.isNotEmpty())
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray())
log.debug { psSelectJoin.toString() }
// Retrieve spendable state refs
val rs = statement.executeQuery(selectJoin)
log.debug(selectJoin)
val rs = psSelectJoin.executeQuery()
stateAndRefs.clear()
var totalPennies = 0L
while (rs.next()) {
val txHash = SecureHash.parse(rs.getString(1))

View File

@ -3,9 +3,7 @@ package net.corda.finance.contracts.asset
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.identity.*
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy
@ -32,25 +30,25 @@ import java.util.*
import kotlin.test.*
class CashTests : TestDependencyInjectionBase() {
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
val defaultIssuer = MEGA_CORP.ref(defaultRef)
val inState = Cash.State(
private val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
private val defaultIssuer = MEGA_CORP.ref(defaultRef)
private val inState = Cash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer,
owner = AnonymousParty(ALICE_PUBKEY)
)
// Input state held by the issuer
val issuerInState = inState.copy(owner = defaultIssuer.party)
val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY))
private val issuerInState = inState.copy(owner = defaultIssuer.party)
private val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY))
fun Cash.State.editDepositRef(ref: Byte) = copy(
private fun Cash.State.editDepositRef(ref: Byte) = copy(
amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref))))
)
lateinit var miniCorpServices: MockServices
lateinit var megaCorpServices: MockServices
private lateinit var miniCorpServices: MockServices
private lateinit var megaCorpServices: MockServices
val vault: VaultService get() = miniCorpServices.vaultService
lateinit var database: CordaPersistence
lateinit var vaultStatesUnconsumed: List<StateAndRef<Cash.State>>
private lateinit var vaultStatesUnconsumed: List<StateAndRef<Cash.State>>
@Before
fun setUp() {
@ -475,19 +473,20 @@ class CashTests : TestDependencyInjectionBase() {
//
// Spend tx generation
val OUR_KEY: KeyPair by lazy { generateKeyPair() }
val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public)
private val OUR_KEY: KeyPair by lazy { generateKeyPair() }
private val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public)
private val OUR_IDENTITY_AND_CERT = getTestPartyAndCertificate(CordaX500Name(organisation = "Me", locality = "London", country = "GB"), OUR_KEY.public)
val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY)
val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY)
private val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY)
private val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY)
fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
private fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
StateAndRef(
TransactionState<Cash.State>(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY),
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY),
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
)
val WALLET = listOf(
private val WALLET = listOf(
makeCash(100.DOLLARS, MEGA_CORP),
makeCash(400.DOLLARS, MEGA_CORP),
makeCash(80.DOLLARS, MINI_CORP),
@ -507,7 +506,7 @@ class CashTests : TestDependencyInjectionBase() {
private fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction {
Cash.generateSpend(miniCorpServices, tx, amount, dest)
Cash.generateSpend(miniCorpServices, tx, amount, OUR_IDENTITY_AND_CERT, dest)
}
return tx.toWireTransaction(miniCorpServices)
}
@ -541,7 +540,7 @@ class CashTests : TestDependencyInjectionBase() {
assertEquals(1, expectedInputs.size)
val inputState = expectedInputs.single()
val actualChange = wtx.outputs.single().data as Cash.State
val expectedChangeAmount = (inputState.state.data as Cash.State).amount.quantity - 50.DOLLARS.quantity
val expectedChangeAmount = inputState.state.data.amount.quantity - 50.DOLLARS.quantity
val expectedChange = WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner)
assertEquals(expectedChange, wtx.getOutput(0))
}
@ -588,9 +587,9 @@ class CashTests : TestDependencyInjectionBase() {
@Test
fun generateExitWithEmptyVault() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> {
assertFailsWith<IllegalArgumentException> {
val tx = TransactionBuilder(DUMMY_NOTARY)
Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList())
Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), OUR_IDENTITY_1)
}
}
@ -615,7 +614,7 @@ class CashTests : TestDependencyInjectionBase() {
database.transaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP))
Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, OUR_IDENTITY_AND_CERT, ALICE, setOf(MINI_CORP))
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
}
@ -631,13 +630,13 @@ class CashTests : TestDependencyInjectionBase() {
database.transaction {
val vaultState = vaultStatesUnconsumed.elementAt(0)
val changeAmount = 90.DOLLARS `issued by` defaultIssuer
val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).filter { state ->
val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).single { state ->
if (state is Cash.State) {
state.amount == changeAmount
} else {
false
}
}.single()
}
val changeOwner = (likelyChangeState as Cash.State).owner
assertEquals(1, miniCorpServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size)
assertEquals(vaultState.ref, wtx.inputs[0])