Skip to content

fix: F-2026-18784 | [Dual Defense] Uppercase Bech32 Fee Payer Bypasses DIRECT_AUX Fee-Payer Guard - #340

Merged
0xNilesh merged 1 commit into
audit-fixesfrom
F-2026-18784
Aug 26, 2026
Merged

fix: F-2026-18784 | [Dual Defense] Uppercase Bech32 Fee Payer Bypasses DIRECT_AUX Fee-Payer Guard#340
0xNilesh merged 1 commit into
audit-fixesfrom
F-2026-18784

Conversation

@0xNilesh

Copy link
Copy Markdown
Member

Finding

cosmossdk.io/x/tx guards against a fee payer also signing with DIRECT_AUX using a raw string compare (signing/directaux/direct_aux.go:93):

if feePayer == signerData.Address {
    return nil, fmt.Errorf("fee payer %s cannot sign with %s: unauthorized", ...)
}

BIP-173 permits an all-uppercase bech32 encoding of the same account, so PUSH1ABC… and push1abc… are the same account but different strings — the guard fails open. Everything downstream decodes both to identical AccAddress bytes and deduplicates signers, so no separate fee-payer signature is required.

A sponsor holding a victim's valid DIRECT_AUX signature over a fixed TxBody can rewrite AuthInfo — raise the fee, set the uppercase payer, drop themselves as a signer — and charge the victim.

Version note: the finding cites x/tx v0.13.3. We pin v0.14.0 (go.mod:20, a replace over a v1.2.0-alpha.1 requirement). I checked v0.14.0 directly: the raw == is still there, so the finding holds against what we actually run.

Fix — drop DIRECT_AUX (recommendation 2)

app.go previously did append(tx.DefaultSignModes, SIGN_MODE_TEXTUAL), and DefaultSignModes includes SIGN_MODE_DIRECT_AUX (SDK x/auth/tx/config.go:62). So the mode was enabled purely by inheriting an SDK default.

Nothing on Push uses it:

  • zero references to DIRECT_AUX / DirectAux anywhere in this repo
  • the universal client pins a single mode — authtx.NewTxConfig(cdc, []signing.SignMode{signing.SignMode_SIGN_MODE_DIRECT}) (universalClient/pushsigner/pushsigner.go:423)

So we were exposing surface for no benefit. The enabled list is now enumerated explicitly, omitting DIRECT_AUX, with a comment recording the mechanism and what has to change upstream before it is restored.

Recommendation 1 (upgrade x/tx) is not available — v0.14.0 is our pin and still has the raw compare. Worth reporting upstream; not something we can pull down today.

Completeness

There is a second place building a tx config from tx.DefaultSignModesapp/params/proto.go:34, in params.MakeEncodingConfig(). That one is not on any production path: its only caller is a test (app/ante/account_init_signer_binding_test.go:38). Every production path uses the app's own config — cmd/pchaind/root.go, cmd/puniversald/root.go and app/encoding.go all take tempApp.TxConfig(). Left alone deliberately rather than changed unnecessarily.

Incidental

append(tx.DefaultSignModes, …) appended to a package-level slice. It is a 3-element literal today so cap == len and the append always allocates, but if upstream ever added a fourth element with spare capacity this would write into the shared backing array. The explicit list removes that too.

Tests

app/sign_modes_test.go, three cases:

  • TestEnabledSignModes_ExcludesDirectAux — DIRECT_AUX absent from SupportedModes()
  • TestEnabledSignModes_KeepsTheModesWeActuallyUse — DIRECT, LEGACY_AMINO_JSON and TEXTUAL all still present, so the removal did not take anything else with it
  • TestDefaultSignModesStillContainsDirectAux — asserts upstream's default still contains it; if upstream ever drops it, this fails and the explicit list can be reconsidered
--- PASS: TestEnabledSignModes_ExcludesDirectAux (0.06s)
--- PASS: TestEnabledSignModes_KeepsTheModesWeActuallyUse (0.04s)
--- PASS: TestDefaultSignModesStillContainsDirectAux (0.00s)
ok  github.com/pushchain/push-chain-node/app  1.594s

The tests use setup(t, ChainID, false, 0) rather than Setup(t) — the latter passes the "testing" chain ID and panics in the EVM configurator, a trap app/nested_dispatch_test.go already documents.

Compatibility

Removing a sign mode is a client-facing change: any external wallet or tool signing Push transactions with DIRECT_AUX would stop working. Nothing in this repo or the universal client does, so the practical risk looks nil — but it is an ecosystem question rather than a code one and is worth a second opinion before merge.

x/tx compares fee payer to signer with a raw string compare, so an uppercase
bech32 alias of the victim slips past it. Nothing on Push signs with AUX.
@0xNilesh
0xNilesh merged commit 2f161b0 into audit-fixes Aug 26, 2026
7 checks passed
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