Skip to content

Commit 7d11860

Browse files
mergify[bot]srdtrkaljo242
authored
imp: add extra validation to ica msgs (backport #8734) (#8742)
* imp: add extra validation to ica msgs (#8734) * imp: add extra validation * chore: lint * imp: error msg * test: fix tests * docs: document the validation change --------- Co-authored-by: Alex | Cosmos Labs <alex@cosmoslabs.io> (cherry picked from commit 7574333) * fix: test compilation * fix: remove non-existant field * docs: update changelog --------- Co-authored-by: srdtrk <59252793+srdtrk@users.noreply.github.com> Co-authored-by: Alex | Cosmos Labs <alex@cosmoslabs.io> Co-authored-by: srdtrk <srdtrk@hotmail.com>
1 parent 14e9bc0 commit 7d11860

File tree

4 files changed

+167
-20
lines changed

4 files changed

+167
-20
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@ Ref: https://keepachangelog.com/en/1.0.0/
3434

3535
# Changelog
3636

37-
## [Unreleased]
37+
## [v10.5.0](https://github.com/cosmos/ibc-go/releases/tag/v10.5.0) - 2025-12-18
38+
39+
### Improvements
40+
41+
* [\#8734](https://github.com/cosmos/ibc-go/pull/8734) Add extra validation for ProtoJSON unmarshalling in ICS-27 ICA.
42+
43+
## [v10.4.0](https://github.com/cosmos/ibc-go/releases/tag/v10.4.0) - 2025-10-10
44+
45+
### Improvements
3846

3947
* [\#8573](https://github.com/cosmos/ibc-go/pull/8573) Support custom address codecs in transfer.
4048

docs/docs/02-apps/02-interchain-accounts/07-tx-encoding.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,7 @@ The proto3 JSON encoding presents an alternative encoding technique for `CosmosT
5656
```
5757

5858
Here, the `"messages"` array is populated with transactions. Each transaction is represented as a JSON object with the `@type` field denoting the transaction type and the remaining fields representing the transaction's attributes.
59+
60+
:::warning
61+
When utilizing proto3 JSON encoding, we have extra validations to ensure the integrity of the encoded data. Specifically, after decoding the `CosmosTx` from JSON, we re-encode it back to JSON and compare it with the original input. If non-formatting discrepancies arise, an error is returned. This validation step ensures that the JSON representation remains consistent throughout the encoding and decoding processes. This additional check means that all optional fields must be explicitly set in the JSON input, all enums and integers must be represented as strings.
62+
:::

modules/apps/27-interchain-accounts/host/keeper/relay_test.go

Lines changed: 131 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
566566
expErr error
567567
}{
568568
{
569-
"interchain account successfully executes an arbitrary message type using the * (allow all message types) param",
569+
"success: interchain account successfully executes an arbitrary message type using the * (allow all message types) param",
570570
func(icaAddress string) {
571571
proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
572572
suite.Require().NoError(err)
@@ -581,8 +581,9 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
581581
{
582582
"@type": "/cosmos.gov.v1.MsgVote",
583583
"voter": "` + icaAddress + `",
584-
"proposal_id": 1,
585-
"option": 1
584+
"proposal_id": "1",
585+
"option": "VOTE_OPTION_YES",
586+
"metadata": ""
586587
}
587588
]
588589
}`)
@@ -601,7 +602,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
601602
nil,
602603
},
603604
{
604-
"interchain account successfully executes banktypes.MsgSend",
605+
"success: interchain account successfully executes banktypes.MsgSend",
605606
func(icaAddress string) {
606607
msgBytes := []byte(`{
607608
"messages": [
@@ -626,7 +627,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
626627
nil,
627628
},
628629
{
629-
"interchain account successfully executes govtypesv1.MsgSubmitProposal",
630+
"success: interchain account successfully executes govtypesv1.MsgSubmitProposal",
630631
func(icaAddress string) {
631632
msgBytes := []byte(`{
632633
"messages": [
@@ -655,7 +656,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
655656
nil,
656657
},
657658
{
658-
"interchain account successfully executes govtypesv1.MsgVote",
659+
"success: interchain account successfully executes govtypesv1.MsgVote",
659660
func(icaAddress string) {
660661
proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
661662
suite.Require().NoError(err)
@@ -670,8 +671,9 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
670671
{
671672
"@type": "/cosmos.gov.v1.MsgVote",
672673
"voter": "` + icaAddress + `",
673-
"proposal_id": 1,
674-
"option": 1
674+
"proposal_id": "1",
675+
"option": "VOTE_OPTION_YES",
676+
"metadata": ""
675677
}
676678
]
677679
}`)
@@ -688,7 +690,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
688690
nil,
689691
},
690692
{
691-
"interchain account successfully executes govtypesv1.MsgSubmitProposal, govtypesv1.MsgDeposit, and then govtypesv1.MsgVote sequentially",
693+
"success: interchain account successfully executes govtypesv1.MsgSubmitProposal, govtypesv1.MsgDeposit, and then govtypesv1.MsgVote sequentially",
692694
func(icaAddress string) {
693695
msgBytes := []byte(`{
694696
"messages": [
@@ -704,15 +706,16 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
704706
},
705707
{
706708
"@type": "/cosmos.gov.v1.MsgDeposit",
707-
"proposal_id": 1,
709+
"proposal_id": "1",
708710
"depositor": "` + icaAddress + `",
709711
"amount": [{ "denom": "stake", "amount": "10000000" }]
710712
},
711713
{
712714
"@type": "/cosmos.gov.v1.MsgVote",
713715
"voter": "` + icaAddress + `",
714-
"proposal_id": 1,
715-
"option": 1
716+
"proposal_id": "1",
717+
"option": "VOTE_OPTION_YES",
718+
"metadata": ""
716719
}
717720
]
718721
}`)
@@ -729,7 +732,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
729732
nil,
730733
},
731734
{
732-
"interchain account successfully executes transfertypes.MsgTransfer",
735+
"success: interchain account successfully executes transfertypes.MsgTransfer",
733736
func(icaAddress string) {
734737
transferPath := ibctesting.NewTransferPath(suite.chainB, suite.chainC)
735738

@@ -744,9 +747,11 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
744747
"token": { "denom": "stake", "amount": "100" },
745748
"sender": "` + icaAddress + `",
746749
"receiver": "cosmos15ulrf36d4wdtrtqzkgaan9ylwuhs7k7qz753uk",
747-
"timeout_height": { "revision_number": 1, "revision_height": 100 },
748-
"timeout_timestamp": 0,
749-
"memo": ""
750+
"timeout_height": { "revision_number": "1", "revision_height": "100" },
751+
"timeout_timestamp": "0",
752+
"memo": "",
753+
"encoding": ""
754+
750755
}
751756
]
752757
}`)
@@ -763,7 +768,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
763768
nil,
764769
},
765770
{
766-
"unregistered sdk.Msg",
771+
"failure: unregistered sdk.Msg",
767772
func(icaAddress string) {
768773
msgBytes := []byte(`{"messages":[{}]}`)
769774
byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") //nolint:staticcheck
@@ -779,7 +784,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
779784
ibcerrors.ErrInvalidType,
780785
},
781786
{
782-
"message type not allowed banktypes.MsgSend",
787+
"failure: message type not allowed banktypes.MsgSend",
783788
func(icaAddress string) {
784789
msgBytes := []byte(`{
785790
"messages": [
@@ -804,7 +809,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
804809
ibcerrors.ErrUnauthorized,
805810
},
806811
{
807-
"unauthorised: signer address is not the interchain account associated with the controller portID",
812+
"failure: signer address is not the interchain account associated with the controller portID",
808813
func(icaAddress string) {
809814
msgBytes := []byte(`{
810815
"messages": [
@@ -828,6 +833,113 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
828833
},
829834
ibcerrors.ErrInvalidType,
830835
},
836+
{
837+
"failure: missing optional metadata field",
838+
func(icaAddress string) {
839+
proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
840+
suite.Require().NoError(err)
841+
842+
err = suite.chainB.GetSimApp().GovKeeper.SetProposal(suite.chainB.GetContext(), proposal)
843+
suite.Require().NoError(err)
844+
err = suite.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(suite.chainB.GetContext(), proposal)
845+
suite.Require().NoError(err)
846+
847+
msgBytes := []byte(`{
848+
"messages": [
849+
{
850+
"@type": "/cosmos.gov.v1.MsgVote",
851+
"voter": "` + icaAddress + `",
852+
"proposal_id": "1",
853+
"option": "VOTE_OPTION_YES"
854+
}
855+
]
856+
}`)
857+
// this is the way cosmwasm encodes byte arrays by default
858+
// golang doesn't use this encoding by default, but it can still deserialize:
859+
byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") //nolint:staticcheck
860+
861+
packetData = []byte(`{
862+
"type": 1,
863+
"data":` + byteArrayString + `
864+
}`)
865+
866+
params := types.NewParams(true, []string{"*"})
867+
suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params)
868+
},
869+
ibcerrors.ErrInvalidType,
870+
},
871+
{
872+
"failure: alternative int representation of enum field",
873+
func(icaAddress string) {
874+
proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
875+
suite.Require().NoError(err)
876+
877+
err = suite.chainB.GetSimApp().GovKeeper.SetProposal(suite.chainB.GetContext(), proposal)
878+
suite.Require().NoError(err)
879+
err = suite.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(suite.chainB.GetContext(), proposal)
880+
suite.Require().NoError(err)
881+
882+
msgBytes := []byte(`{
883+
"messages": [
884+
{
885+
"@type": "/cosmos.gov.v1.MsgVote",
886+
"voter": "` + icaAddress + `",
887+
"proposal_id": "1",
888+
"option": 1,
889+
"metadata": ""
890+
}
891+
]
892+
}`)
893+
// this is the way cosmwasm encodes byte arrays by default
894+
// golang doesn't use this encoding by default, but it can still deserialize:
895+
byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") //nolint:staticcheck
896+
897+
packetData = []byte(`{
898+
"type": 1,
899+
"data":` + byteArrayString + `
900+
}`)
901+
902+
params := types.NewParams(true, []string{"*"})
903+
suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params)
904+
},
905+
ibcerrors.ErrInvalidType,
906+
},
907+
{
908+
"failure: alternative integer field representation",
909+
func(icaAddress string) {
910+
proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
911+
suite.Require().NoError(err)
912+
913+
err = suite.chainB.GetSimApp().GovKeeper.SetProposal(suite.chainB.GetContext(), proposal)
914+
suite.Require().NoError(err)
915+
err = suite.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(suite.chainB.GetContext(), proposal)
916+
suite.Require().NoError(err)
917+
918+
msgBytes := []byte(`{
919+
"messages": [
920+
{
921+
"@type": "/cosmos.gov.v1.MsgVote",
922+
"voter": "` + icaAddress + `",
923+
"proposal_id": 1,
924+
"option": "VOTE_OPTION_YES",
925+
"metadata": ""
926+
}
927+
]
928+
}`)
929+
// this is the way cosmwasm encodes byte arrays by default
930+
// golang doesn't use this encoding by default, but it can still deserialize:
931+
byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") //nolint:staticcheck
932+
933+
packetData = []byte(`{
934+
"type": 1,
935+
"data":` + byteArrayString + `
936+
}`)
937+
938+
params := types.NewParams(true, []string{"*"})
939+
suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params)
940+
},
941+
ibcerrors.ErrInvalidType,
942+
},
831943
}
832944

833945
for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {

modules/apps/27-interchain-accounts/types/codec.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package types
22

33
import (
4+
"encoding/json"
5+
"reflect"
6+
47
"github.com/cosmos/gogoproto/proto"
58

69
errorsmod "cosmossdk.io/errors"
@@ -92,6 +95,15 @@ func DeserializeCosmosTx(cdc codec.Codec, data []byte, encoding string) ([]sdk.M
9295
if err := cdc.UnmarshalJSON(data, &cosmosTx); err != nil {
9396
return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "cannot unmarshal CosmosTx with proto3 json: %v", err)
9497
}
98+
reconstructedData, err := cdc.MarshalJSON(&cosmosTx)
99+
if err != nil {
100+
return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "cannot remarshal CosmosTx with proto3 json: %v", err)
101+
}
102+
if isEqual, err := equalJSON(data, reconstructedData); err != nil {
103+
return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "cannot compare original and reconstructed JSON: %v", err)
104+
} else if !isEqual {
105+
return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "original and reconstructed JSON objects do not match, original: %s, reconstructed: %s", string(data), string(reconstructedData))
106+
}
95107
default:
96108
return nil, errorsmod.Wrapf(ErrInvalidCodec, "unsupported encoding format %s", encoding)
97109
}
@@ -109,3 +121,14 @@ func DeserializeCosmosTx(cdc codec.Codec, data []byte, encoding string) ([]sdk.M
109121

110122
return msgs, nil
111123
}
124+
125+
func equalJSON(a, b []byte) (bool, error) {
126+
var x, y any
127+
if err := json.Unmarshal(a, &x); err != nil {
128+
return false, err
129+
}
130+
if err := json.Unmarshal(b, &y); err != nil {
131+
return false, err
132+
}
133+
return reflect.DeepEqual(x, y), nil
134+
}

0 commit comments

Comments
 (0)