Decodehextx scripts sanity check #10481

pull achow101 wants to merge 2 commits into bitcoin:master from achow101:decodehextx-sanity changing 4 files +59 −3
  1. achow101 commented at 11:33 PM on May 30, 2017: member

    This PR adds some sanity checking to DecodeHexTx to make sure that the scripts contain valid opcodes. The main purpose of doing this is to avoid decoding witness transactions as 0-input 1-output non-witness transactions. The scripts are likely to be garbage and contain invalid opcodes when decoded as non-witness.

    There is an issue where other invalid opcodes (not just OP_INVALIDOPCODE) are not handled but I am not sure how to go about doing that.

    This is related to #9522.

  2. in src/core_read.cpp:95 in d1fb4337b5 outdated
      87 | @@ -88,18 +88,38 @@ CScript ParseScript(const std::string& s)
      88 |      return result;
      89 |  }
      90 |  
      91 | +// Check that all of the input and output scripts of a transaction contains valid opcodes
      92 | +bool CheckTxScriptsSanity(CMutableTransaction& tx)
      93 | +{
      94 | +    // Check input scripts
      95 | +    for(unsigned int i = 0; i < tx.vin.size(); i++) {
    


    sipa commented at 11:46 PM on May 30, 2017:

    You should probably skip this loop for coinbase transactions, as those don't have meaningful input scripts.

    Is testing the inputs even useful, as the function of this test is to distinguish between transactions that may errorneously be decoded as 0-input transaction?

    Nit: space after for (and elsewhere).

  3. achow101 force-pushed on May 31, 2017
  4. achow101 force-pushed on May 31, 2017
  5. dcousens approved
  6. in src/core_read.cpp:92 in 6909865869 outdated
      87 | @@ -88,18 +88,40 @@ CScript ParseScript(const std::string& s)
      88 |      return result;
      89 |  }
      90 |  
      91 | +// Check that all of the input and output scripts of a transaction contains valid opcodes
      92 | +bool CheckTxScriptsSanity(CMutableTransaction& tx)
    


    sipa commented at 2:52 AM on May 31, 2017:

    Mark the tx argument const?


    achow101 commented at 7:02 PM on June 5, 2017:

    done

  7. jonasschnelli added the label RPC/REST/ZMQ on May 31, 2017
  8. sipa commented at 6:14 PM on June 3, 2017: member

    utACK 6909865869d9f2582e33e936e770538dfc541cf7

  9. sipa commented at 11:55 PM on June 4, 2017: member

    Something else you could consider is checking whether the total script is not too large (MAX_SCRIPT_SIZE) or the stack elements aren't too large (MAX_SCRIPT_ELEMENT_SIZE).

  10. achow101 force-pushed on Jun 5, 2017
  11. achow101 commented at 7:02 PM on June 5, 2017: member

    addressed @sipa's comments.

  12. in src/script/script.cpp:277 in e21a9f48fe outdated
     272 | +{
     273 | +    CScript::const_iterator it = begin();
     274 | +    while (it < end()) {
     275 | +        opcodetype opcode;
     276 | +        CScript::const_iterator prev_it = it;
     277 | +        if (!GetOp(it, opcode) || opcode > 0xb9 || it - prev_it > MAX_SCRIPT_ELEMENT_SIZE) {
    


    sipa commented at 9:54 PM on June 5, 2017:

    I think this isn't actually correct. MAX_SCRIPT_ELEMENT_SIZE is the maximum length of the data, but its encoding as a CScript push may be up to 2 bytes more. Perhaps you can use GetOp2, which returns the data being pushed?


    achow101 commented at 10:31 PM on June 5, 2017:

    done

  13. achow101 force-pushed on Jun 5, 2017
  14. in src/core_read.cpp:104 in dc95ca642e outdated
      99 | +            }
     100 | +        }
     101 | +    }
     102 | +    // Check output scripts
     103 | +    for (unsigned int i = 0; i < tx.vout.size(); i++) {
     104 | +        if (!tx.vout[0].scriptPubKey.HasValidOps() || tx.vout[0].scriptPubKey.size() > MAX_SCRIPT_SIZE) {
    


    sipa commented at 10:35 PM on June 5, 2017:

    vout[i]

  15. achow101 force-pushed on Jun 5, 2017
  16. achow101 force-pushed on Jun 5, 2017
  17. in src/core_read.cpp:97 in a3c9b93b18 outdated
      92 | +bool CheckTxScriptsSanity(const CMutableTransaction& tx)
      93 | +{
      94 | +    // Check input scripts for non-coinbase txs
      95 | +    if (!CTransaction(tx).IsCoinBase()) {
      96 | +        for (unsigned int i = 0; i < tx.vin.size(); i++) {
      97 | +            if (!tx.vin[i].scriptSig.HasValidOps()) {
    


    sipa commented at 7:01 AM on June 6, 2017:

    No MAX_SCRIPT_SIZE test here?


    achow101 commented at 5:47 PM on June 6, 2017:

    done

  18. achow101 force-pushed on Jun 6, 2017
  19. sipa commented at 9:41 PM on June 6, 2017: member

    reutACK e76d516a19f09f546d6fb5f16f8b4c758c5287ed

  20. Add a valid opcode sanity check to CScript
    Added a function in CScript that checks if the script contains valid opcodes.
    
    Add a test for that function
    5b75c47784
  21. Sanity check transaction scripts in DecodeHexTx
    Make sure that the scripts of decoded transactions are valid scripts.
    ac4e438229
  22. in src/script/script.cpp:277 in e76d516a19 outdated
     272 | +{
     273 | +    CScript::const_iterator it = begin();
     274 | +    while (it < end()) {
     275 | +        opcodetype opcode;
     276 | +        std::vector<unsigned char> item;
     277 | +        if (!GetOp(it, opcode, item) || opcode > 0xb9 || item.size() > MAX_SCRIPT_ELEMENT_SIZE) {
    


    laanwj commented at 5:59 PM on June 7, 2017:

    Let's define a constant for 0xb9, this is kind of a magic number here. OP_NOP10 would work, but maybe it's better to explicitly mark what the maximum valid opcode is.


    achow101 commented at 9:08 PM on June 7, 2017:

    Done

  23. achow101 force-pushed on Jun 7, 2017
  24. laanwj merged this on Jun 8, 2017
  25. laanwj closed this on Jun 8, 2017

  26. laanwj referenced this in commit 9c248e39f2 on Jun 8, 2017
  27. achow101 deleted the branch on Jun 8, 2017
  28. roconnor-blockstream commented at 4:10 PM on August 26, 2017: contributor

    This sanity check doesn't work. invalid opcodes are not illegal, and can even appear in real transactions. For an example see transaction 95f7832b6449348bd1f3bc2e6eb110785248427bbe13853f2d61da9a6e810800 on testnet.

  29. sipa commented at 7:35 PM on August 26, 2017: member

    @roconnor-blockstream This test is only a heuristic to determine whether to prefer a non-witness interpretation of a transaction over a witness interpretation when it has 0 inputs. It's also only relevant for interpreting inputs to decoderawtransaction and fundrawtransaction (which are the only things that accept transactions with 0 inputs in the first place).

  30. roconnor-blockstream commented at 4:50 AM on August 27, 2017: contributor

    We cannot use heuristics for deciding how to interpret ambiguous parameters to API calls such as fundrawtransaction. It is a security hazard.

    Imagine someone is using Bitcoin Core as part of some protocol. They have assembled data from various parties, some of them generated adversarially. The protocol safely validates the adversarial inputs and composes a valid, partial transaction, and passes it to fundrawtransaction, then sends that output to signrawtransaction and then passes it around or even broadcasts it with sendrawtransaction.

    The system has validated the construction of their transaction, and the designers have tested it many times and believes it work. However, it isn't secure. After deployment an adversary tickles their data, perhaps by creating an output with invalid opcodes. These adversarially provided output scripts are legal and pass protocol's validation engine. However this causes the data passed to fundrawtransaction to be interpreted differently, either as a segwit transaction when the protocol thinks it is generating a legacy transaction, or vice versa. Now fundrawtransaciton is generating a transaction that is different than the protocol intends, and it is probably a transaction sending the funds somewhere else. Nobody expected that fine details about the contents of output scripts to change how the transaction being passed is parsed, that would be very surprising, so the software just assumes fundrawtransaction output is the transaction it thinks it has created, and blindly sends it onto signrawtransaction. With that transaction now signed, who knows where the money will end up.

  31. sipa commented at 12:42 AM on August 28, 2017: member

    @roconnor-blockstream Then what do you suggest? The only full solution is not using the same format for partial transactions (something that @achow101's PSBT format accomplishes). Anything else will inherently risk being ambiguous.

  32. roconnor-blockstream commented at 2:21 AM on August 28, 2017: contributor

    Yes, ultimately this format for partial transactions must be retired.

    In the mean time I suggest either adding a, preferably mandatory, flag to fundrawtransaction to tell it whether the parameter is a partially signed segwit transaction or a partially signed legacy transaction. If you choose to make the flag optional, then, when no flag is given, you must parse the parameter both ways and fail of both parses are successful. For consistency, and safety, decoderawtransaction should operate the same way.

    (instead of a flag, you could instead create two new API calls for fundrawlegacytransaction and fundrawsegwittransaction).

    Perhaps there are some other ideas.

  33. laanwj referenced this in commit fee0370fd6 on Dec 19, 2017
  34. virtload referenced this in commit 8c83d4407f on Apr 4, 2018
  35. PastaPastaPasta referenced this in commit b72ddddfca on Jul 5, 2019
  36. PastaPastaPasta referenced this in commit 38204b0fa5 on Aug 6, 2019
  37. PastaPastaPasta referenced this in commit a42b58da61 on Aug 7, 2019
  38. PastaPastaPasta referenced this in commit a6d37544ca on Aug 8, 2019
  39. PastaPastaPasta referenced this in commit 3a6b2ce274 on Aug 12, 2019
  40. barrystyle referenced this in commit 80e37d1c1c on Jan 22, 2020
  41. DrahtBot locked this on Sep 8, 2021

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-13 15:15 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me