Skip to content

fix: F-2026-18197 | [Dual Defense] Nested Message Dispatch Bypasses EVM Ante for MsgEthereumTx - #317

Open
0xNilesh wants to merge 1 commit into
audit-fixesfrom
F-2026-18197
Open

fix: F-2026-18197 | [Dual Defense] Nested Message Dispatch Bypasses EVM Ante for MsgEthereumTx#317
0xNilesh wants to merge 1 commit into
audit-fixesfrom
F-2026-18197

Conversation

@0xNilesh

@0xNilesh 0xNilesh commented Aug 20, 2026

Copy link
Copy Markdown
Member

F-2026-18197 — remove the attack surface (chain side)

Ethereum signature, nonce and gas checks live only in the EVM ante handler; x/vm's
Keeper.EthereumTx performs none of them and assumes the ante already ran. The ante chain only
runs over a tx's top-level messages, so any module that unpacks an embedded sdk.Msg and
re-dispatches it through the message router delivers an MsgEthereumTx to the executor with the
ante already behind it — signature never checked, nonce force-set rather than verified
(x/vm/keeper/state_transition.go, // - reset sender's nonce to msg.Nonce() before calling evm),
so a victim-signed tx lifted off the mempool executes as the victim, repeatably.

Push had two such dispatchers wired. This PR deletes both rather than trying to enumerate the
message shapes they can carry.
The companion PR hardens the sink.

Changes

  • x/group removed entirely — imports, GroupKeeper, store key, keeper construction, app
    module, and both module-ordering lists (app/app.go). It is a generic proposal dispatcher
    (MsgSubmitProposal / MsgExec) with zero usage anywhere in Push: no group, no group policy
    and no proposal is created by the chain, the universal client or any user flow, and grouptypes
    appeared nowhere outside app wiring. The SDK 0.54 upgrade forces dropping it anyway, so this pulls
    that work forward.
  • remove-group upgrade handler (app/upgrades/remove-group/) —
    StoreUpgrades{Deleted: []string{group.StoreKey}} plus delete(fromVM, group.ModuleName) so
    RunMigrations does not try to migrate a module that is no longer registered. Registered in
    app/upgrades.go, following remove-utxverifier.
  • "stargate" dropped from both wasm capability lists (app/wasm.go for x/wasm,
    app/app.go for the 08-wasm light-client VM). It was present only because the boilerplate
    AllCapabilities() returns everything wasmvm supports; it lets a contract emit an arbitrary
    encoded sdk.Msg (CosmosMsg::Any / Stargate) straight into the message router.

Pre-flight

  • No x/group state found locally. Both local exported states — the devnet genesis at
    ~/.pchain/config/genesis.json and push-smart-account-v1/state.json (localchain_9000-1,
    height 21) — carry the default empty group genesis: group_seq: 0, groups: [],
    group_policies: [], proposals: [], votes: []. No group state exists anywhere in the repo's
    configs either. ⚠️ Open risk: this was not verified against live donut (deliberately not
    queried). If any group, group policy or open proposal exists on donut, the store deletion strands
    it. Please confirm against donut before scheduling the upgrade — it is a single
    q group groups-by-admin / genesis-export check.
  • No contract requires stargate. The only wasm binary in the repo,
    interchaintest/contracts/cw_template.wasm, declares requires_iterator, requires_cosmwasm_1_1,
    1_2, 1_3 — no requires_stargate. Both local exported states show wasm.codes: [] and
    wasm.contracts: [] (nothing uploaded at all), and code_upload_access is Everybody.
    ⚠️ Open risk: again not verified against live donut. If a contract there declares
    requires_stargate it will stop being instantiable.
  • Caveat, stated honestly: the wasmvm capability list is enforced at upload against the
    capabilities the contract declares in its exports. wasmd still wires EncodeAnyMsg at runtime,
    so a contract that deliberately omits the requires_stargate export could in principle still emit
    an Any message. That residual is exactly what the companion PR's VerifySender closes — the
    reason we are doing both halves rather than either alone. (Note the list also stops at
    cosmwasm_1_4, so cosmwasm_2_0 is not available either.)

Deliberately not done

  • Permissioning wasm code upload (plan item 3, marked "optional if low-risk"): skipped.
    Flipping code_upload_access off Everybody is a chain-policy change with no product decision
    behind it, it would break the existing TestCosmWasmIntegration interchaintest which uploads from
    an ordinary account, and its marginal value is small once stargate is gone and the sink is
    hardened. Easy to add later as a params change if wanted.

Hacken remediations

  • Change account prefix & gas fee token name #1 — shared nested-message denylist / guarded MessageRouter: DECLINED. This proposes redoing
    the fix that already failed. AuthzLimiterDecorator (app/ante/ante_cosmos.go) is that
    denylist — it is precisely the remediation Evmos shipped for GHSA-v6rw-hhgg-wc4x, the same bug
    class. It held until two more dispatch modules were enabled underneath it, at which point it
    silently stopped covering the surface. A denylist has to enumerate every dispatching module and
    every nesting shape forever, and fails open when someone adds a module. Fixing the sink is
    structural; and with x/group and stargate gone there are no callers left to guard.
  • add push prefix and denom #2 took the "disable stargate" branch; a custom WASM messenger is unnecessary work.
  • Re scaffold chain #3 — extend the ante to unpack group msgs: MOOT, the module is gone. (Worth noting their
    wording was insufficient anyway: group proposals are stored and executed later, so an ante-only
    check misses already-stored proposals — it would also have been needed at execution time.)
  • Last working branch via spawn #4(c) — bind gas to a Cosmos/block budget: tracked as a follow-up, lower priority now that no
    ante-skipping path exists.

Tests

Test Covers
app.TestGroupModuleNotWired group absent from the module manager, no store key, MsgSubmitProposal/MsgExec/MsgCreateGroup* unroutable on the msg service router and unresolvable in the interface registry (so tx decoding fails)
app.TestWasmStargateCapabilityDisabled stargate absent from both AllCapabilities() and the 08-wasm list
txpolicy.TestGaslessMsgTypesExcludeEthereumTx MsgEthereumTx is not gasless, bare or nested in authz.MsgExec; MsgExecutePayload still is
integrationtest.TestGaslessExecutePayloadWithModuleSender invariant guard — a gasless module-sender MsgExecutePayload still executes end to end, and the uexecutor module account has no pubkey (so it could never sign an MsgEthereumTx)

Results — ./app/, ./app/ante/..., ./app/decorators/..., ./app/txpolicy/...,
./app/upgrades/..., ./test/... all green; go build ./... clean.

The gasless invariant guard was additionally run against the patched EVM (local
replace github.com/cosmos/evm => ../push-chain-evm pointing at the companion branch): the whole
test/integration/uexecutor package passes, including TestExecutePayload and
TestGaslessExecutePayloadWithModuleSender. The replace was reverted before committing — see the
follow-up below.

Pre-existing failure unrelated to this PR: app.TestBlockedAddrs / Setup(t) panic with
unknown chain id: testing when the app package's tests are run without another test having
initialised the global EVM configurator first (const chainID = "testing" in test_helpers.go vs
ChainID = "localchain_9000-1"). The new tests use the deterministic constructor to avoid it.

Follow-up

Once the companion PR merges, bump this repo's github.com/cosmos/evm replace pin so the sink fix
actually ships with the chain.


Companion PR (EVM side, hardens the sink with VerifySender): pushchain/push-chain-evm#41

Both are generic nested-message dispatchers that reach the message router
after the ante handler has run, letting an MsgEthereumTx skip the EVM ante
(F-2026-18197). Neither is used by Push. Adds a remove-group upgrade handler
that prunes the group store.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant