Bypass fee payer authentication
Description
The authenticator's job is to validate the signature of a message and ensure that the required accounts have signed it, including the fee payer if one is specified. When custom CosmWasm authenticators are added (or if an empty AllOfAuthenticator
is used) then it is possible for an authenticator to be added that will return iface.Authenticated()
regardless of whether the fee payer has signed the message or not:
// Consume the authenticator's static gas
cacheCtx.GasMeter().ConsumeGas(authenticator.StaticGas(), "authenticator static gas")
// Get the authentication data for the transaction
neverWriteCacheCtx, _ := cacheCtx.CacheContext() // GetAuthenticationData is not allowed to modify the state
authData, err := authenticator.GetAuthenticationData(neverWriteCacheCtx, tx, msgIndex, simulate)
if err != nil {
return ctx, err
}
authentication := authenticator.Authenticate(cacheCtx, account, msg, authData)
if authentication.IsRejected() {
return ctx, authentication.Error()
}
if authentication.IsAuthenticated() {
msgAuthenticated = true
// Once the fee payer is authenticated, we can set the gas limit to its original value
if !feePayerAuthenticated && account.Equals(feePayer) {
originalGasMeter.ConsumeGas(payerGasMeter.GasConsumed(), "fee payer gas")
// Reset this for both contexts
cacheCtx = ad.authenticatorKeeper.TransientStore.
GetTransientContextWithGasMeter(originalGasMeter)
ctx = ctx.WithGasMeter(originalGasMeter)
feePayerAuthenticated = true
}
break
}
This will cause the entire fee to be deducted from the fee payer in the DeductFeeDecorator
ante handler, but since the feePayerAuthenticated
will not be set to true (account
is based off the message's GetSigner
, which will not match if a separate fee payer is specified), the amount of gas will be limited to 20,000.
Impact
A malicious user can set up an authenticator to always verify any message, then send messages with high fees and a separate fee payer to drain any account of its funds.
An example POC, which is located in the appendix ref↗, was provided to Osmosis Labs that demonstrates forcing someone to pay 100,0000 in fees without signing the message:
The POC will output the following:
Balances before:
hacker (osmo1m6a73d0qhl9kphwx84syysnrr3t3myxvhw3f5d): amount: "103875"
victum (osmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj): amount: "99362536125"
# transfer log
{
"type": "transfer",
"attributes": [
{
"key": "recipient",
"value": "osmo17xpfvakm2amg962yls6f84z3kell8c5lczssa0",
"index": false
},
{
"key": "sender",
"value": "osmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj",
"index": false
},
{
"key": "amount",
"value": "1000000uosmo",
"index": false
}
]
}
Balances after:
hacker: amount: "103875"
victim: amount: "99361536125"
Recommendations
The fee payer should always be authenticated regardless of the authenticator used.
Remediation
This issue has been acknowledged by Osmosis Labs, and a fix was implemented in commit 651eccd9↗. The feePayerAuthenticated
is always authenticated now.