'Firebase Realtime database was manipulated

I am using firebase realtime database for a money transfer app where a user creates an account with a unique username and gets to send/receive money. The backend just subtracts numbers from one account and adds to another account. Everything worked fine until an attacker created about 3 accounts. Then added money to one legally but then sent the same amount to the other two accounts at the same time. For context, He created account A,B,C, added $10 to 'A' legally, then sent that $10 to 'B' & 'C' at the same time making 'B' & 'C' have $10 each. Then repeated the process allover.

Each transaction has a timestamp but the difference between the two transactions was just milliseconds apart which seems like it was in a forloop and could have been executed by a human cause of delay.

How my database is structured

enter image description here

The transaction dates between the two shows just milliseconds apart which shouldn't be possible sending to two different accounts enter image description here

Below is my kotlin code

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.pay_activity_review_local)

    ref  = Firebase.database.getReference("UsersAccount")
    progressHud = SVProgressHUD(this)

    nameTV.text = recipientFullName
    tagTV.text = userTag
    currencyCodeTV.text = recipientCurrencyCode
    amountTV.text = "${CurrentUser.currencySymbol}${sendReceiveAmount.commaRepresentation()}"
    swipeBtn.setText("Swipe to $sendRequestText")

    swipeBtn.setOnStateChangeListener {

        if (it) {

           sendBtnPressed()
        } 
    }
}

private fun sendBtnPressed() {

    val valueEventListener = object : ValueEventListener {

        override fun onDataChange(dataSnapshot: DataSnapshot) {

            if (dataSnapshot.exists()) {

                currentUserBalance = dataSnapshot.child("AvailableAmount").getValue(Double::class.java)
                Log.i("xxx currentUserBalance", "$currentUserBalance")

                val desc = descriptionET.text.trim().toString()

                if (desc.isNotEmpty()) {

                    if (isRequest) {
                        // issa request
                        generateRequest(recipientId, desc, isRequest, sendReceiveAmount)

                    } else {
                        // issa send
                        if (currentUserBalance!! >= sendReceiveAmount) {

                            sendAmountToRecipientAccount(sendReceiveAmount, recipientId, desc)

                        } else {
                            // insufficient
                            progressHud.dismissImmediately()
                            extensions.infoAlertDialog(this@ReviewLocalActivity, "Why?", "Your funds are insufficient, deposit funds in your wallet to continue", false)
                        }
                    }

                } else {
                    // description is empty
                    progressHud.dismissImmediately()
                    extensions.infoAlertDialog(this@ReviewLocalActivity, "Description field empty", "Enter description of payment", false)
                }

            } else {
                return
            }
        }

        override fun onCancelled(databaseError: DatabaseError) {
            // Getting Post failed, log a message
            Log.i("xxx onCan", databaseError.toException().toString())
        }
    }

    ref = Firebase.database.getReference("UsersAccount")
    ref.child(CurrentUser.userId).addListenerForSingleValueEvent(valueEventListener) 
}

private fun sendAmountToRecipientAccount(amountToSend: Double, recipientId: String, desc: String) {

    progressHud.showWithStatus("")

    val valueEventListener = object : ValueEventListener {
        override fun onDataChange(snapshot: DataSnapshot) {

            if (snapshot.exists()) {

                val recipientBalance = snapshot.child("AvailableAmount").getValue(Double::class.java)!!

                //add amount to recipient current account
                val newBalance = recipientBalance + amountToSend

                val values: MutableMap<String, Any> = HashMap()
                values["AvailableAmount"] = newBalance

                ref.child(recipientId).updateChildren(values).addOnSuccessListener {

                    // debiting current user account
                    val amountToDebit = currentUserBalance!! - amountToSend //force unwrap cos value def exists

                    val values2: MutableMap<String, Any> = HashMap()
                    values2["AvailableAmount"] = amountToDebit

                    ref.child(CurrentUser.userId).updateChildren(values2).addOnSuccessListener {

                        //sending push notification to recipient
                        val notifyObject = JSONObject()
                        val notifyInfo = JSONObject()

                        notifyInfo.put("title","Credit")
                        notifyInfo.put("body","${CurrentUser.firstName} sent you ${CurrentUser.currencySymbol}${amountToSend.commaRepresentation()}.")
                        notifyInfo.put("badge",1)
                        notifyInfo.put("sound","default")

                        notifyObject.put("to", recipientRegToken)
                        notifyObject.put("notification", notifyInfo)

                        extensions.sendFirebasePushNotification(notifyObject, this@ReviewLocalActivity)

                        // creating transaction receipt
                        generateTransactionReceipt(recipientId, desc, isRequest, amountToSend)

                    }.addOnFailureListener {

                        Log.e("xxx firebase", "Error getting data", it)
                    }

                }.addOnFailureListener {

                    Log.e("xxx firebase", "Error getting data", it)
                }
            }
        }

        override fun onCancelled(error: DatabaseError) {

            Log.i("xxx onCancelled", error.message)
        }

    }
    ref.child(recipientId).addListenerForSingleValueEvent(valueEventListener)
}

private fun generateTransactionReceipt(recipientId: String, desc: String, isRequest: Boolean, amount: Double) {

    val currentUserFullName = "${CurrentUser.firstName} ${CurrentUser.lastName}"
    val transactionDate = ServerValue.TIMESTAMP
    val transactionId = ref.child(CurrentUser.userId).push().key

    // updating currentUser transaction history
    val values: MutableMap<String, Any> = HashMap()
    values["TransactionDate"] = transactionDate
    values["SenderId"] = recipientId
    values["SenderName"] = recipientFullName // recipientName bcos historyActivity will display it for current user
    values["SenderEmail"] = recipientEmail
    values["Description"] = desc
    values["ImageUrl"] = recipientImgUrl
    values["Amount"] = amount
    values["CurrencyCode"] = recipientCurrencyCode
    values["IsRequest"] = isRequest
    values["IsCredit"] = false
    values["IsWithdraw"] = false
    values["IsDeclined"] = false

    ref.child(CurrentUser.userId).child("TransactionHistory").child("$transactionId").updateChildren(values)
            .addOnSuccessListener {

                extensions.claimFirstTimeBonus(CurrentUser.userId) //claiming user first time bonus

                // updating recipient transaction history
                val values2: MutableMap<String, Any> = HashMap()
                values2["TransactionDate"] = transactionDate
                values2["SenderId"] = CurrentUser.userId
                values2["SenderName"] = currentUserFullName
                values2["SenderEmail"] = CurrentUser.email
                values2["Description"] = desc
                values2["ImageUrl"] = CurrentUser.imageUrl
                values2["Amount"] = amount
                values2["CurrencyCode"] = CurrentUser.currencyCode
                values2["IsRequest"] = isRequest // will be true
                values2["IsCredit"] = true
                values2["IsWithdraw"] = false
                values2["IsDeclined"] = false

                ref.child(recipientId).child("TransactionHistory").child("$transactionId").updateChildren(values2)
                        .addOnSuccessListener {

                            progressHud.dismissImmediately()

                            val intent = Intent(this, SentActivity::class.java)

                            intent.putExtra("senderAmount", amount)
                            intent.putExtra("senderCurrCode", CurrentUser.currencyCode) // both recip/sender have same code for local transaction in Sent Activity
                            intent.putExtra("recipientCurrCode", recipientCurrencyCode)
                            intent.putExtra("sendOrRequestText", sendRequestText)
                            intent.putExtra("recipientImgUrl", recipientImgUrl)
                            intent.putExtra("recipientFullName", recipientFullName)
                            intent.putExtra("isRequest", isRequest)
                            intent.putExtra("isVerified", isVerified)
                            this.startActivity(intent)

                        }.addOnFailureListener {

                            Log.e("xxx firebase", "Error getting data", it)
                        }

            }.addOnFailureListener {

                Log.e("xxx firebase", "Error getting data", it)
            }
}

private fun generateRequest(recipientId: String,
                            desc: String,
                            isRequest: Boolean,
                            amount: Double) {

    progressHud.showWithStatus("")

    val currentUserFullName = "${CurrentUser.firstName} ${CurrentUser.lastName}"
    val requestDate = ServerValue.TIMESTAMP
    val transactionId = ref.child(CurrentUser.userId).push().key

    // goes to recipient & checks if current userId isBlocked by recipient
    ref.child(recipientId).child("BlockedLists").child(CurrentUser.userId).get().addOnSuccessListener {

        val isBlocked = it.exists()

        if (isBlocked) {

            progressHud.dismissImmediately()
            extensions.infoAlertDialog(this, "Blocked", "You have been blocked by this user.", false)

        } else {

            // generating currentUser request
            val values: MutableMap<String, Any> = HashMap()
            values["TransactionDate"] = requestDate
            values["SenderId"] = recipientId
            values["SenderName"] = recipientFullName // recipientName bcos historyActivity will display it for current user
            values["SenderEmail"] = recipientEmail
            values["Description"] = desc
            values["ImageUrl"] = recipientImgUrl
            values["Amount"] = amount
            values["CurrencyCode"] = recipientCurrencyCode
            values["IsRequest"] = isRequest // will be true
            values["IsCredit"] = false
            values["IsWithdraw"] = false
            values["IsDeclined"] = false

            ref.child(CurrentUser.userId).child("TransactionHistory").child("$transactionId").updateChildren(values)
                .addOnSuccessListener {

                //sending push notification to recipient
                val notifyObject = JSONObject()
                val notifyInfo = JSONObject()

                notifyInfo.put("title","Request")
                notifyInfo.put("body","${CurrentUser.firstName} requested ${CurrentUser.currencySymbol}${amount.commaRepresentation()} from you.")
                notifyInfo.put("badge",1)
                notifyInfo.put("sound","default")

                notifyObject.put("to", recipientRegToken)
                notifyObject.put("notification", notifyInfo)

                extensions.sendFirebasePushNotification(notifyObject, this)

                // generating recipient transaction history
                val values2: MutableMap<String, Any> = HashMap()
                    values2["TransactionDate"] = requestDate
                    values2["SenderId"] = CurrentUser.userId
                    values2["SenderName"] = currentUserFullName
                    values2["SenderEmail"] = CurrentUser.email
                    values2["Description"] = desc
                    values2["ImageUrl"] = CurrentUser.imageUrl
                    values2["Amount"] = amount
                    values2["CurrencyCode"] = CurrentUser.currencyCode
                    values2["IsRequest"] = isRequest // will be true
                    values2["IsCredit"] = true
                    values2["IsWithdraw"] = false
                    values2["IsDeclined"] = false
                    values2["IsVerified"] = isVerified

                ref.child(recipientId).child("TransactionHistory").child("$transactionId").updateChildren(values2)
                    .addOnSuccessListener {

                        progressHud.dismissImmediately()

                        val intent = Intent(this, SentActivity::class.java)

                        intent.putExtra("senderAmount", sendReceiveAmount)
                        intent.putExtra("recipientCurrCode", recipientCurrencyCode)
                        intent.putExtra("sendOrRequestText", sendRequestText)
                        intent.putExtra("recipientImgUrl", recipientImgUrl)
                        intent.putExtra("recipientFullName", recipientFullName)
                        intent.putExtra("isRequest", isRequest)
                        intent.putExtra("isVerified", isVerified)
                        this.startActivity(intent)


                    }.addOnFailureListener {
                        Log.i("xxx firebase", "Error getting data", it)
                    }

                }
                .addOnFailureListener {
                    Log.i("xxx firebase", "Error getting data", it)
                }
        }

    }.addOnFailureListener {

        Log.i("xxx firebase", "Error getting data", it)
    }
  }
}

My last solution was coming here as I have been trying for a while. Thank you so much for taking your time out to help. For further clarification please leave a comment



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source