GHSA-CWFQ-RFCR-8HMP

Vulnerability from github – Published: 2026-05-07 21:02 – Updated: 2026-05-07 21:02
VLAI?
Summary
Zebra's Transparent SIGHASH_SINGLE Handling Diverges from zcashd for Corresponding Outputs
Details

Zebra Transparent SIGHASH_SINGLE Corresponding-Output Handling Diverges From zcashd

Summary

For V5+ transparent spends, Zebra and zcashd disagree on the same consensus rule: SIGHASH_SINGLE must fail when the input index has no corresponding output. zcashd treats this as consensus-invalid under ZIP-244, while Zebra's transparent verification path computes a digest for the missing-output case instead of failing.

The result is a direct block-validity split. A malformed V5 transparent transaction can be accepted by Zebra, retained in Zebra's mempool, selected into Zebra getblocktemplate, mined into a block, and then rejected by zcashd.

Details

Validated code revisions used during analysis:

  • zcashd: 2c63e9aa08cb170b0feb374161bea94720c3e1f5
  • Zebra: a905fa19e3a91c7b4ead331e2709e6dec5db12cb

Scope note:

  • earlier triage material grouped pre-V5 and V5 behavior together;
  • re-execution on the pinned revisions did not reproduce the claimed pre-V5 / V4 reject-side behavior;
  • this advisory therefore covers the V5+ / ZIP-244 variant only.

zcashd side:

  • Transparent scripts in blocks are checked through TransactionSignatureChecker::CheckSig() and SignatureHash(): zcash/src/script/interpreter.cpp.
  • In the ZIP-244 branch, SignatureHash() explicitly throws when SIGHASH_SINGLE or SIGHASH_SINGLE|ANYONECANPAY is used with nIn >= txTo.vout.size(): zcash/src/script/interpreter.cpp.
  • CheckSig() catches that exception and returns false, causing the transparent script to fail.

Zebra side:

Why this is exploitable:

  • the malformed transaction only needs fewer transparent outputs than inputs;
  • the attacker signs the digest that Zebra computes for the missing-output case;
  • Zebra then sees a valid transparent signature, while zcashd never reaches the same digest because it fails first.

Ordinary path viability:

PoC

Validated commits:

  • zcashd: 2c63e9aa08cb170b0feb374161bea94720c3e1f5
  • Zebra: a905fa19e3a91c7b4ead331e2709e6dec5db12cb

Manual reproduction steps:

  1. Build an otherwise-valid V5 transaction with at least two transparent inputs and only one transparent output.
  2. Sign input 0 normally.
  3. Sign input 1 with canonical SIGHASH_SINGLE or SIGHASH_SINGLE|ANYONECANPAY.
  4. Use the digest returned by Zebra's ZIP-244 path, where the missing output contributes transparent_outputs_hash([]).
  5. Submit the transaction to Zebra and to zcashd.
  6. Observe:
  7. Zebra accepts it into the mempool;
  8. Zebra selects it into getblocktemplate;
  9. Zebra can mine and accept a block containing it;
  10. zcashd rejects it in the ordinary mempool path.

Impact

This is a direct V5+ transparent consensus split.

Who can trigger it:

  • an ordinary transaction author can craft the malformed V5 transparent transaction;
  • the accept-side stock path is Zebra's mempool and block-template path;
  • an external miner still has to include the transaction in a block for the split to materialize.

Who is impacted:

  • Zebra can accept and template a transaction / block that zcashd rejects;
  • this makes the issue both a consensus-divergence problem and a practical Zebra block-template safety problem.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "crates.io",
        "name": "zebrad"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.4.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-573",
      "CWE-354"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-07T21:02:30Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "# `Zebra` Transparent `SIGHASH_SINGLE` Corresponding-Output Handling Diverges From `zcashd`\n\n### Summary\nFor V5+ transparent spends, `Zebra` and `zcashd` disagree on the same consensus rule: `SIGHASH_SINGLE` must fail when the input index has no corresponding output. `zcashd` treats this as consensus-invalid under ZIP-244, while `Zebra`\u0027s transparent verification path computes a digest for the missing-output case instead of failing.\n\nThe result is a direct block-validity split. A malformed V5 transparent transaction can be accepted by `Zebra`, retained in `Zebra`\u0027s mempool, selected into `Zebra` `getblocktemplate`, mined into a block, and then rejected by `zcashd`.\n\n### Details\nValidated code revisions used during analysis:\n\n- `zcashd`: `2c63e9aa08cb170b0feb374161bea94720c3e1f5`\n- `Zebra`: `a905fa19e3a91c7b4ead331e2709e6dec5db12cb`\n\nScope note:\n\n- earlier triage material grouped pre-V5 and V5 behavior together;\n- re-execution on the pinned revisions did not reproduce the claimed pre-V5 / V4 reject-side behavior;\n- this advisory therefore covers the V5+ / ZIP-244 variant only.\n\n`zcashd` side:\n\n- Transparent scripts in blocks are checked through `TransactionSignatureChecker::CheckSig()` and `SignatureHash()`: [`zcash/src/script/interpreter.cpp`](https://github.com/zcash/zcash/blob/2c63e9aa08cb170b0feb374161bea94720c3e1f5/src/script/interpreter.cpp#L1386-L1407).\n- In the ZIP-244 branch, `SignatureHash()` explicitly throws when `SIGHASH_SINGLE` or `SIGHASH_SINGLE|ANYONECANPAY` is used with `nIn \u003e= txTo.vout.size()`: [`zcash/src/script/interpreter.cpp`](https://github.com/zcash/zcash/blob/2c63e9aa08cb170b0feb374161bea94720c3e1f5/src/script/interpreter.cpp#L1221-L1259).\n- `CheckSig()` catches that exception and returns `false`, causing the transparent script to fail.\n\n`Zebra` side:\n\n- V5 transparent inputs route into the same FFI-based transparent script verifier used for block validation: [`zebra/zebra-consensus/src/transaction.rs`](https://github.com/ZcashFoundation/zebra/blob/a905fa19e3a91c7b4ead331e2709e6dec5db12cb/zebra-consensus/src/transaction.rs#L989-L1098).\n- `Zebra` converts the decoded hash type and asks its Rust sighash engine for a digest without adding the corresponding-output pre-check that `zcashd` enforces first: [`zebra/zebra-script/src/lib.rs`](https://github.com/ZcashFoundation/zebra/blob/a905fa19e3a91c7b4ead331e2709e6dec5db12cb/zebra-script/src/lib.rs#L160-L175), [`zebra/zebra-chain/src/primitives/zcash_primitives.rs`](https://github.com/ZcashFoundation/zebra/blob/a905fa19e3a91c7b4ead331e2709e6dec5db12cb/zebra-chain/src/primitives/zcash_primitives.rs#L307-L343).\n- `Zebra` forwards canonical `SIGHASH_SINGLE` into the Rust ZIP-244 implementation.\n- In that implementation, when `input.index() \u003e= bundle.vout.len()`, the code uses `transparent_outputs_hash::\u003cTxOut\u003e(\u0026[])` instead of erroring: [`zcash_primitives/src/transaction/sighash_v5.rs`](https://github.com/zcash/librustzcash/blob/c3425f9c3c7f6deb20720bb78b18f35fbbed8edd/zcash_primitives/src/transaction/sighash_v5.rs#L101-L107), [`zcash_primitives/src/transaction/sighash_v5.rs`](https://github.com/zcash/librustzcash/blob/c3425f9c3c7f6deb20720bb78b18f35fbbed8edd/zcash_primitives/src/transaction/sighash_v5.rs#L131-L139).\n\nWhy this is exploitable:\n\n- the malformed transaction only needs fewer transparent outputs than inputs;\n- the attacker signs the digest that `Zebra` computes for the missing-output case;\n- `Zebra` then sees a valid transparent signature, while `zcashd` never reaches the same digest because it fails first.\n\nOrdinary path viability:\n\n- `zcashd` ordinary mempool admission is not the practical trigger path, because the same ZIP-244 `SignatureHash()` checks fail there first: [`zcash/src/main.cpp`](https://github.com/zcash/zcash/blob/2c63e9aa08cb170b0feb374161bea94720c3e1f5/src/main.cpp#L1981-L1995), [`zcash/src/script/interpreter.cpp`](https://github.com/zcash/zcash/blob/2c63e9aa08cb170b0feb374161bea94720c3e1f5/src/script/interpreter.cpp#L1221-L1259).\n- `Zebra` ordinary mempool admission is viable because `Zebra` uses the same transparent verifier for mempool and block validation and does not have a separate \"one output per input\" standardness rule here: [`zebra/zebra-consensus/src/transaction.rs`](https://github.com/ZcashFoundation/zebra/blob/a905fa19e3a91c7b4ead331e2709e6dec5db12cb/zebra-consensus/src/transaction.rs#L414-L519), [`zebra/zebrad/src/components/mempool/storage.rs`](https://github.com/ZcashFoundation/zebra/blob/a905fa19e3a91c7b4ead331e2709e6dec5db12cb/zebrad/src/components/mempool/storage.rs#L255-L376).\n- `Zebra` is a block-template producer, so the realistic stock path is `Zebra` mempool -\u003e `Zebra` `getblocktemplate` -\u003e external miner: [`zebra/zebra-rpc/src/methods/types/get_block_template/zip317.rs`](https://github.com/ZcashFoundation/zebra/blob/a905fa19e3a91c7b4ead331e2709e6dec5db12cb/zebra-rpc/src/methods/types/get_block_template/zip317.rs#L72-L105).\n\n### PoC\nValidated commits:\n\n- `zcashd`: `2c63e9aa08cb170b0feb374161bea94720c3e1f5`\n- `Zebra`: `a905fa19e3a91c7b4ead331e2709e6dec5db12cb`\n\nManual reproduction steps:\n\n1. Build an otherwise-valid V5 transaction with at least two transparent inputs and only one transparent output.\n2. Sign input `0` normally.\n3. Sign input `1` with canonical `SIGHASH_SINGLE` or `SIGHASH_SINGLE|ANYONECANPAY`.\n4. Use the digest returned by `Zebra`\u0027s ZIP-244 path, where the missing output contributes `transparent_outputs_hash([])`.\n5. Submit the transaction to `Zebra` and to `zcashd`.\n6. Observe:\n   - `Zebra` accepts it into the mempool;\n   - `Zebra` selects it into `getblocktemplate`;\n   - `Zebra` can mine and accept a block containing it;\n   - `zcashd` rejects it in the ordinary mempool path.\n\n### Impact\nThis is a direct V5+ transparent consensus split.\n\nWho can trigger it:\n\n- an ordinary transaction author can craft the malformed V5 transparent transaction;\n- the accept-side stock path is `Zebra`\u0027s mempool and block-template path;\n- an external miner still has to include the transaction in a block for the split to materialize.\n\nWho is impacted:\n\n- `Zebra` can accept and template a transaction / block that `zcashd` rejects;\n- this makes the issue both a consensus-divergence problem and a practical `Zebra` block-template safety problem.",
  "id": "GHSA-cwfq-rfcr-8hmp",
  "modified": "2026-05-07T21:02:30Z",
  "published": "2026-05-07T21:02:30Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/ZcashFoundation/zebra/security/advisories/GHSA-cwfq-rfcr-8hmp"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/ZcashFoundation/zebra"
    },
    {
      "type": "WEB",
      "url": "https://github.com/ZcashFoundation/zebra/releases/tag/v4.4.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:H/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Zebra\u0027s Transparent SIGHASH_SINGLE Handling Diverges from zcashd for Corresponding Outputs"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…