Footer
General•November 11, 2026

The Secret Network Research Path

The Parser Behind the Governance Gate

By Rahul Saxena (@saxenism)

How Stargate, raw witnesses, and an unchecked slice halt validators

By Rahul Saxena (@saxenism)

Severity: Critical (a deterministic four-validator halt and restart-persistent crash were demonstrated; mainnet reachability requires an active-set Byzantine proposer and assumes no production-only policy outside the audited source blocks the contract route).

Affected component: the then-current scrtlabs/SecretNetwork repository at commit 266cd4fd96e816656f520ba85d309cc5b1c78ef9, release tag v1.24.1, now publicly maintained as Secret3dev/SecretNetwork; x/compute Stargate dispatch, SGX block-message verification, and the machine-whitelist parser. Full revision table in the technical appendix.

Fix: The Secret3dev community's v1.27.2 rejects privileged compute messages from contract Stargate, re-checks machine-whitelist governance in the message server, bounds-checks the SGX parser, and propagates enclave approval failures. Released and activated on secret-4 on 2026-09-23.

Disclosure: first submitted to Secret Labs under coordinated disclosure on [DATE TO CONFIRM]; later fixed by community maintainers in the Secret3dev organization; fix released and activated on mainnet 2026-09-23.

Secret Network copied every top-level transaction message's raw Any.Value into an SGX cache before normal SDK execution. A Byzantine proposer could combine those bytes with a contract-emitted MsgUpdateMachineWhitelist, bypassing the top-level governance check and making SGX parse a malformed witness whose declared machine_id length exceeded the remaining input.

The parser sliced to the attacker-declared length without checking the buffer. Honest validators then panicked while finalizing the same block. In software the four validator processes exited and a restarted validator crashed again while replaying the poisoned block. A separate four-validator Azure run reproduced the SGX panic on real hardware and stopped at the next height.

This writeup describes a vulnerability first disclosed by Bluethroat Labs to Secret Labs under coordinated disclosure and later remediated by the community maintaining Secret Network in the Secret3dev organization. All observations reflect the audited revisions above. No transaction was broadcast to secret-4; all positive runs used isolated local networks and researcher-controlled SGX hardware.

The chain, in five steps

  1. Construct. An active-set Byzantine proposer places a valid contract execution first and an outer-protobuf-valid witness transaction second. The witness contains a malformed MsgUpdateMachineWhitelist value.
  2. Capture. SubmitBlockSignatures verifies the block data hash and copies both transactions' raw message values into VERIFIED_BLOCK_MESSAGES before normal SDK execution.
  3. Bypass. The contract emits MsgUpdateMachineWhitelist through Stargate. The dispatcher checks basic validity and contract signer equality, then routes directly to the message server without the top-level governance ante check.
  4. Crash. OnApproveMachineID asks SGX to find the machine ID in the raw witness cache. is_msg_machine_id reads length 127 with one byte remaining and performs an out-of-bounds slice.
  5. Persist. Validators panic while applying the block. A normal restart replays the same block and reaches the same parser panic.

The software witness crashed all four validators at the height-82 finalization boundary. The hardware witness committed height 22 with all four signatures, then remained at height 22 while consensus repeatedly attempted height 23. Receipts and the distinction between those two measurements are in the technical appendix.

Why this is a consensus problem

The trigger is not an ordinary mempool transaction. The malformed witness is expected to fail normal ante or SDK handling. A proposer, however, chooses the transaction bytes and order in its own proposal. Secret's SGX verifier treated those data-hash-consistent bytes as authorization evidence before the application decided whether their messages were valid.

BFT consensus is expected to retain liveness with less than one-third Byzantine voting power. This path required one active-set validator and one proposer slot, not one-third of voting power. Every honest validator processing the proposal entered the same deterministic parser path.

Security property violated. Proposer-selected raw block bytes must not become trusted SGX authorization evidence unless they have passed the same semantic checks as the operation they authorize.

Root cause

1. Contracts could route a governance-sensitive SDK message

The Stargate encoder unpacked a contract-provided Any, and the SDK message handler checked ValidateBasic and signer equality before calling the normal router. It did not execute the top-level ante decorator.

Vulnerable-source excerpt (handler_plugin.go:580-603); ... marks routing code omitted for brevity:

if m, ok := msg.(sdk.HasValidateBasic); ok {
    if err := m.ValidateBasic(); err != nil {
        return nil, errorsmod.Wrapf(err, "failed basic validation for message type %T", msg)
    }
}

signers, _, err := h.cdc.GetMsgV1Signers(msg)
// ...
for _, acct := range signers {
    if !contractAddr.Equals(sdk.AccAddress(acct)) {
        return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission")
    }
}

The governance-to-message comparison existed only in the top-level ante path. The UpdateMachineWhitelist message server parsed the IDs and called api.OnApproveMachineID without repeating that check.

2. SGX cached raw message values before SDK acceptance

After validating the Tendermint data hash, the enclave parsed each outer transaction and appended every message's Any.Value.

Verbatim vulnerable source (wasm_messages.rs:38-42):

pub fn append_msg_from_tx(&mut self, mut tx: Tx) {
    for msg in tx.take_body().messages {
        self.messages.push_back(msg.value);
    }
}

The hardware run made the mismatch observable: the contract transaction succeeded, the top-level witness later returned tx parse error, and SGX had already consumed that witness's raw inner value.

3. The witness parser trusted its length field

The parser read the protobuf length and sliced without comparing it with the remaining cursor.

Verbatim vulnerable source (offchain.rs:784-790):

if let Some(x) = r.read_uint() {
    r.cursor = &r.cursor[0..x];
} else {
    trace!("wrong sub6");
    return false;
};

The witness encoded x = 127 with one byte left. Rust panicked with range end index 127 out of range for slice of length 1.

Reproduction

LayerEstablishesDoes not establish
Go route preflightContract Stargate reaches UpdateMachineWhitelist; the equivalent top-level message fails without matching governanceSGX execution or a consensus halt
Four-validator software localnetOrdered attack pair, all four exits at the height-82 finalization boundary, and restart replay into the same panicA clean height-82 RPC commit record or real SGX hardware
Four-validator Azure SGX localnetReal-SGX parser panic; common height-22 block and commit; four signatures; no progress beyond height 22A positive run with the complete production,verify-validator-whitelist feature set
Mainnet-release artifact runv1.24.1 release-path provenance and byte-identical enclave/bridge artifacts between honest and proposer imagesExploitation using those exact precompiled images
Public source reviewlight-client-validation includes the vulnerable scanner in the mainnet build pathAn unknown policy outside the audited checkout

No single run executes the entire sequence from the exact mainnet release image through a production secret-4 block. The evidence is compositional. Source review supplies the feature-gate and dispatch links between the measured layers.

The malformed input is an outer-protobuf-valid transaction whose first Any.Value resembles MsgUpdateMachineWhitelist but declares a 127-byte machine-ID field with one byte available. The production trigger requires no failure injected into honest validators; the proposer image deterministically inserts the two transaction bytes to model Byzantine proposal construction.

Identities, hashes, commands, and full receipts are in the technical appendix.

Halt reachability

The demonstrated result is a deterministic localnet halt. In software, every validator exited while applying height 82; one restarted validator replayed height 82 and exited again in about four seconds. On Azure SGX, all four RPC endpoints agreed on the two-transaction height-22 block and its four-signature commit. Post-trigger monitoring retained height 22 across 1,120 status and 1,120 consensus-state samples while the network attempted height 23.

Production reachability requires an active-set Byzantine validator to obtain a proposer slot and a contract able to emit the Stargate message. The public v1.24.1 source enabled Stargate, registered the sensitive SDK message, and included the scanner under mainnet's light-client-validation feature. The positive hardware run did not use the full mainnet feature set, and Bluethroat could not observe whether an external deployment policy restricted contract upload, code IDs, or message type URLs.

What this did not allow

  • No ordinary remote-user trigger was demonstrated. The malformed witness was proposer-inserted block data and would likely fail normal mempool handling.
  • No safety split was observed. The measured effect was loss of liveness, not conflicting finalized histories.
  • No live-network transaction was sent. Every positive run used an isolated four-validator network.
  • No complete end-to-end production run was performed. The Reproduction table states where composition stops.

Severity

Bluethroat Labs classification: Critical, with a Byzantine-proposer reachability qualifier.

The software and real-SGX witnesses demonstrate that one proposer can select a block which deterministically panics honest validator SGX execution and prevents normal restart recovery. The remaining uncertainty is production reachability: the hardware witness used light-client-validation,random, while the mainnet-feature connection is source-derived and any deployment policy outside the audited checkout was unobservable. Critical is the strongest classification supported if active-set Byzantine proposers are in scope; otherwise the precondition materially lowers practical exposure.

Fix

The Secret3dev community's public v1.27.2 tag closes the demonstrated chain at multiple boundaries. Contract Stargate rejects both privileged message type URLs. The UpdateMachineWhitelist message server repeats the governance check, enclave approval errors now fail the message, and the parser checks the remaining length before slicing.

Verbatim patched source (offchain.rs:797-806):

if let Some(x) = r.read_uint() {
    if x > r.cursor.len() {
        trace!("malformed field");
        return false;
    }
    r.cursor = &r.cursor[0..x];
} else {
    trace!("wrong sub6");
    return false;
};

The tag includes a malformed-length regression test and a privileged-message denylist covering MsgUpdateMachineWhitelist. Bluethroat verified these changes against the public source tag; it did not rebuild or test the signed enclave running on secret-4.

Timeline

  • 2026-06-02 — Software and Azure SGX evidence captured.
  • [DATE TO CONFIRM] — Finding first submitted to Secret Labs under coordinated disclosure with software, hardware, proof-of-concept, and integrity evidence.
  • 2026-09-23 — The Secret3dev community released v1.27.2; it was activated on secret-4 at height 27,286,266.
  • 2026-11-10 — Bluethroat verified the parser, privileged-message, message server, and error-propagation fixes in the public v1.27.2 source tag.
  • [DATE TO CONFIRM] — Secret Labs maintainers identify this finding as eligible for publication, subject to draft review.

Key takeaway

Cryptographic inclusion proves that bytes were proposed; it does not prove that the application accepted their meaning. An enclave cache of raw block messages cannot safely stand in for governance or successful SDK execution.

For TEE-integrated chains, the audit question is whether privileged enclave state consumes proposer-controlled bytes before the application has applied the authorization rules for those bytes. Here, that ordering turned a rejected witness into a validator-wide parser crash.

References

Technical Appendix: Secret Network Machine-Whitelist Parser Halt

Supporting material for the main writeup. This appendix records the revisions, source bridge, artifact identities, measured outputs, controls, and limitations behind the article.

1. Pinned revisions

ComponentRevision
Software PoC checkoutedcbccbbbe65cae1bd93d4d9089ff5d98228f704
Hardware PoC and confirmed affected releasescrtlabs/SecretNetwork v1.24.1, commit 266cd4fd96e816656f520ba85d309cc5b1c78ef9
Community remediation sourceSecret3dev/SecretNetwork v1.27.2, commit 72c548f39602e87924f69289715ae8d7c8cfa7b4
Software enclave featuresdebug-print,random,light-client-validation
Hardware positive-run featuresrandom,light-client-validation
Public mainnet workflow features at v1.24.1verify-validator-whitelist,light-client-validation,random,production

The finding was first submitted to Secret Labs. Its eventual remediation was shipped by community maintainers in the Secret3dev organization.

The public v1.24.1 tag contains the same unchecked slice at tag-local cosmwasm/enclaves/execute/src/registration/offchain.rs:785-786. Its release workflow selects hardware SGX and includes light-client-validation.

The public v1.27.2 tag was inspected on 2026-11-10. The relevant fixes are:

  • handler_plugin.go:463-483: contract Stargate rejects privileged type URLs;
  • privileged.go:292-319: machine-whitelist governance validation is shared;
  • msg_server.go:347-391: the message server checks governance and propagates enclave failure;
  • offchain.rs:797-806: malformed lengths return false before slicing;
  • offchain.rs:831-837: a malformed-length regression test.

2. Release-path provenance

The software localnet used separate honest and proposer images. The proposer image differed in the host daemon and contained the deterministic insertion marker. The enclave and bridge artifacts matched:

librandom_api.so
c033f743bc5a254abede89cfe749487285d0eb7e7771abefd87766f5940ae266

librust_cosmwasm_enclave.signed.so
b7727ec129774a3ce1a97b5b143537e753090b4286ae9c79d1a7224b62f2c1ab

libgo_cosmwasm.so
ee56c94bc283ec6dd8813568f2d11d566c87bcc53f8b761b3fcf24426394c678

The host daemons differed as expected:

honest    d8ff10460467bc09cfc0c7e5b64c8f1c0048cb77a7bd30fc7071c5f1c21a1653
proposer  94d119f5840e6d0b7223038cfb6a0bfe349bea7599c281db32b4242cb94ade97

A separate Azure run built through the v1.24.1 mainnet-release Docker path and fetched the /v124/ precompiled enclave and bridge blobs. It established release-path provenance but did not progress far enough to serve as the positive exploit witness. The source-built hardware run is the positive SGX receipt.

3. Evidence layers

Layer 1 — contract-route preflight

The Go preflight contained two controls:

  • TestStargateReachesUpdateMachineWhitelistWithContractSigner reached the message server when the contract address was the SDK message signer.
  • TestTopLevelUpdateMachineWhitelistRequiresGovAnte rejected the equivalent top-level path without a matching passed proposal.

This isolates the dispatch difference. It does not execute SGX.

Layer 2 — four-validator software localnet

Configuration:

validator count       4
voting power          equal
proposer image        val0 only
honest images         val1, val2, val3
SGX_MODE              SW
attack threshold      H80
actual insertion      H82
consensus timeout     700ms

The proposer inserted two transactions:

  1. a valid signed contract execution whose response emitted /secret.compute.v1beta1.MsgUpdateMachineWhitelist;
  2. an outer-protobuf-valid top-level transaction whose first Any.Value declared a 127-byte machine_id field with one byte remaining.

All validator excerpts reached the height-82 consensus/finalization boundary, dispatched the Stargate message, and logged:

thread '<unnamed>' panicked at
'range end index 127 out of range for slice of length 1',
execute/src/registration/offchain.rs:795:21

Container state:

val0 Exited (2)
val1 Exited (2)
val2 Exited (2)
val3 Exited (2)

The final successful RPC status sample remained at height 81 on all validators. The height-82 block and commit RPC files are empty because RPC disappeared while the block was being applied. This layer therefore establishes a common consensus/finalization boundary and the four crashes, not a clean RPC proof that height 82 committed.

Layer 3 — restart replay

The val1 restart receipt recorded:

ABCI Replay Blocks appHeight=81 stateHeight=81 storeHeight=82
Replay last block using real app
[DispatchSubmessages] height=82 ... type=stargate
thread '<unnamed>' panicked at
'range end index 127 out of range for slice of length 1',
execute/src/registration/offchain.rs:795:21
github.com/cometbft/cometbft/consensus.(*Handshaker).replayBlock(...)

The process exited with code 2 before restart and code 2 again about four seconds after restart. Normal process restart did not remove the poisoned block from replay.

Layer 4 — four-validator Azure SGX localnet

Environment:

Azure SKU             Standard_DC8s_v3
region                eastus
SGX mode              HW
enclave health        Success
target commit         266cd4fd96e816656f520ba85d309cc5b1c78ef9
features              random,light-client-validation
malicious proposer    val0
honest validators     val1, val2, val3
actual insertion      H22

All four RPC endpoints returned the same attack block:

height                 22
encrypted_random       non-null
transaction count      2
block hash             2B9289884A8E98A58EB27B588FE7914B393E16AF19B996F64E723293E8983E9D
parts hash             91C0BFE517D52BD059A5687E5EEC2BA86243C340798FAB72B47A477B8BE3B1CA

All four commit captures returned height 22, the same block hash, and four signatures. All four block-results captures returned:

tx0  code=0
tx1  code=2  log=tx parse error

This split is expected. SGX copied the raw witness value while processing block signatures; normal SDK execution rejected the witness later.

Honest validator logs contained:

thread '<unnamed>' panicked at
'range end index 127 out of range for slice of length 1',
execute/src/registration/offchain.rs:786:21
Failed to add machine_id: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

The next-height path reported:

CONSENSUS FAILURE!!! err="Failed to submit validator set to enclave"

Post-trigger monitoring recorded:

status samples             1120
consensus-state samples    1120
latest committed height    22
consensus state            attempting H23 with nil/failure states

The dedicated cloud resources were deleted after capture. The teardown sweep reported no remaining VMs, disks, public IPs, NICs, snapshots, or tagged resources.

4. Prepared-input identities

The software receipt recorded these SHA-256 values:

tx0-exec.bin      eadb0615449c904be81e9ac70bd7f6aa8d2c3bde04129ec25dd03bc17a62aca9
tx1-witness.bin   e06a348b28fbb946db1630ec4bb0db35ab6d19b5c0611edea085eee98c9d29f4
msg-valid.bin     2511e95ff44ad07ad3ec23f3047bdbed9bde4c3f19aff6ed890018bb8facba1a
msg-malformed.bin 9b4b9c675620c50f6d1d7243733b21d7f00cd9949a53cf6ee3646445aa251eec

The malformed value preserves a parseable outer Tx. Its invalidity is inside the first message's Any.Value, after the sender and proposal fields:

field tag 0x1a
declared length 0x7f
remaining field bytes 1

That distinction matters because an earlier remediation skipped transactions whose outer protobuf parse failed. It did not make this inner parser safe.

5. Feature-gate bridge

The positive hardware witness enabled light-client-validation,random. The public mainnet workflow also enabled production and verify-validator-whitelist.

The source bridge is:

  1. light-client-validation enables check_machine_id_in_block.
  2. ecall_onchain_approve_machine_id calls that scanner before approval.
  3. production and verify-validator-whitelist do not replace is_msg_machine_id or add a check around its length-derived slice.
  4. MsgUpdateMachineWhitelist reaches api.OnApproveMachineID in the affected message server.
  5. The compute keeper advertises Stargate support and registers the message service.

This establishes code inclusion. It does not substitute for a positive full-mainnet-feature reproduction.

6. Evidence locations in the disclosure bundle

Primary software run:

evidence/sw-4val-poc/
machine-whitelist-sgx-parser-crash-sw-emitter-mvp-20260602T095521Z/

Restart receipt:

evidence/sw-4val-poc/
machine-whitelist-sgx-parser-crash-sw-emitter-mvp-20260602T095521Z/
evidence/restart-replay-val1-20260602T100352Z/

Primary hardware run:

evidence/azure-sgx-hw-poc/
secret-sgx-hw-machine-whitelist-positive-20260602T115118Z/
remote/success/
machine-whitelist-sgx-parser-crash-hw-positive-v124-20260602T175439Z/

Mainnet-release artifact run:

evidence/mainnet-release-artifact-provenance/
secret-sgx-hw-mainnet-artifact-20260602T101830Z/

These paths are provenance for a disclosure-bundle reviewer. They are not public download links.

7. Limitations

  • The trigger requires proposer control; no ordinary mempool path was demonstrated.
  • The software run lost RPC while height 82 was being applied. The hardware run supplies the clean block-and-commit receipt.
  • The hardware positive run was source-built with random,light-client-validation, not the complete mainnet feature set.
  • The exact precompiled mainnet-release images did not progress far enough to become a positive exploit witness.
  • A production-only contract upload, code-ID, or type-URL policy outside the audited source could affect reachability.
  • The work demonstrates liveness loss, not a safety violation.

8. Safety

No transaction was broadcast to secret-4. All blocks, contracts, machine IDs, and keys were researcher-controlled fixtures on isolated networks. Cloud resources were deleted after capture. No production hardware identifier, credential, private key, PPID-bearing quote, or unredacted cloud response is included here.