tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

decompression-corrupt-input.any.js (8026B)


      1 // META: global=window,worker,shadowrealm
      2 // META: script=resources/decompression-input.js
      3 
      4 // This test checks that DecompressionStream behaves according to the standard
      5 // when the input is corrupted. To avoid a combinatorial explosion in the
      6 // number of tests, we only mutate one field at a time, and we only test
      7 // "interesting" values.
      8 
      9 'use strict';
     10 
     11 // The many different cases are summarised in this data structure.
     12 const expectations = [
     13  {
     14    format: 'deflate',
     15 
     16    // Decompresses to 'expected output'.
     17    baseInput: deflateChunkValue,
     18 
     19    // See RFC1950 for the definition of the various fields used by deflate:
     20    // https://tools.ietf.org/html/rfc1950.
     21    fields: [
     22      {
     23        // The function of this field. This matches the name used in the RFC.
     24        name: 'CMF',
     25 
     26        // The offset of the field in bytes from the start of the input.
     27        offset: 0,
     28 
     29        // The length of the field in bytes.
     30        length: 1,
     31 
     32        cases: [
     33          {
     34            // The value to set the field to. If the field contains multiple
     35            // bytes, all the bytes will be set to this value.
     36            value: 0,
     37 
     38            // The expected result. 'success' means the input is decoded
     39            // successfully. 'error' means that the stream will be errored.
     40            result: 'error'
     41          }
     42        ]
     43      },
     44      {
     45        name: 'FLG',
     46        offset: 1,
     47        length: 1,
     48 
     49        // FLG contains a 4-bit checksum (FCHECK) which is calculated in such a
     50        // way that there are 4 valid values for this field.
     51        cases: [
     52          {
     53            value: 218,
     54            result: 'success'
     55          },
     56          {
     57            value: 1,
     58            result: 'success'
     59          },
     60          {
     61            value: 94,
     62            result: 'success'
     63          },
     64          {
     65            // The remaining 252 values cause an error.
     66            value: 157,
     67            result: 'error'
     68          }
     69        ]
     70      },
     71      {
     72        name: 'DATA',
     73        // In general, changing any bit of the data will trigger a checksum
     74        // error. Only the last byte does anything else.
     75        offset: 18,
     76        length: 1,
     77        cases: [
     78          {
     79            value: 4,
     80            result: 'success'
     81          },
     82          {
     83            value: 5,
     84            result: 'error'
     85          }
     86        ]
     87      },
     88      {
     89        name: 'ADLER',
     90        offset: -4,
     91        length: 4,
     92        cases: [
     93          {
     94            value: 255,
     95            result: 'error'
     96          }
     97        ]
     98      }
     99    ]
    100  },
    101  {
    102    format: 'gzip',
    103 
    104    // Decompresses to 'expected output'.
    105    baseInput: gzipChunkValue,
    106 
    107    // See RFC1952 for the definition of the various fields used by gzip:
    108    // https://tools.ietf.org/html/rfc1952.
    109    fields: [
    110      {
    111        name: 'ID',
    112        offset: 0,
    113        length: 2,
    114        cases: [
    115          {
    116            value: 255,
    117            result: 'error'
    118          }
    119        ]
    120      },
    121      {
    122        name: 'CM',
    123        offset: 2,
    124        length: 1,
    125        cases: [
    126          {
    127            value: 0,
    128            result: 'error'
    129          }
    130        ]
    131      },
    132      {
    133        name: 'FLG',
    134        offset: 3,
    135        length: 1,
    136        cases: [
    137          {
    138            value: 1, // FTEXT
    139            result: 'success'
    140          },
    141          {
    142            value: 2, // FHCRC
    143            result: 'error'
    144          }
    145        ]
    146      },
    147      {
    148        name: 'MTIME',
    149        offset: 4,
    150        length: 4,
    151        cases: [
    152          {
    153            // Any value is valid for this field.
    154            value: 255,
    155            result: 'success'
    156          }
    157        ]
    158      },
    159      {
    160        name: 'XFL',
    161        offset: 8,
    162        length: 1,
    163        cases: [
    164          {
    165            // Any value is accepted.
    166            value: 255,
    167            result: 'success'
    168          }
    169        ]
    170      },
    171      {
    172        name: 'OS',
    173        offset: 9,
    174        length: 1,
    175        cases: [
    176          {
    177            // Any value is accepted.
    178            value: 128,
    179            result: 'success'
    180          }
    181        ]
    182      },
    183      {
    184        name: 'DATA',
    185 
    186        // The last byte of the data is the most interesting.
    187        offset: 26,
    188        length: 1,
    189        cases: [
    190          {
    191            value: 3,
    192            result: 'error'
    193          },
    194          {
    195            value: 4,
    196            result: 'success'
    197          }
    198        ]
    199      },
    200      {
    201        name: 'CRC',
    202        offset: -8,
    203        length: 4,
    204        cases: [
    205          {
    206            // Any change will error the stream.
    207            value: 0,
    208            result: 'error'
    209          }
    210        ]
    211      },
    212      {
    213        name: 'ISIZE',
    214        offset: -4,
    215        length: 4,
    216        cases: [
    217          {
    218            // A mismatch will error the stream.
    219            value: 1,
    220            result: 'error'
    221          }
    222        ]
    223      }
    224    ]
    225  },
    226  {
    227    format: 'brotli',
    228 
    229    // Decompresses to 'expected output'.
    230    baseInput: brotliChunkValue,
    231 
    232    fields: []
    233  }
    234 ];
    235 
    236 async function tryDecompress(input, format) {
    237  const ds = new DecompressionStream(format);
    238  const reader = ds.readable.getReader();
    239  const writer = ds.writable.getWriter();
    240  writer.write(input).catch(() => {});
    241  writer.close().catch(() => {});
    242  let out = [];
    243  while (true) {
    244    try {
    245      const { value, done } = await reader.read();
    246      if (done) {
    247        break;
    248      }
    249      out = out.concat(Array.from(value));
    250    } catch (e) {
    251      if (e instanceof TypeError) {
    252        return { result: 'error' };
    253      } else {
    254        return { result: e.name };
    255      }
    256    }
    257  }
    258  const expectedOutput = 'expected output';
    259  if (out.length !== expectedOutput.length) {
    260    return { result: 'corrupt' };
    261  }
    262  for (let i = 0; i < out.length; ++i) {
    263    if (out[i] !== expectedOutput.charCodeAt(i)) {
    264      return { result: 'corrupt' };
    265    }
    266  }
    267  return { result: 'success' };
    268 }
    269 
    270 function corruptInput(input, offset, length, value) {
    271  const output = new Uint8Array(input);
    272  if (offset < 0) {
    273    offset += input.length;
    274  }
    275  for (let i = offset; i < offset + length; ++i) {
    276    output[i] = value;
    277  }
    278  return output;
    279 }
    280 
    281 for (const { format, baseInput, fields } of expectations) {
    282  promise_test(async () => {
    283    const { result } = await tryDecompress(baseInput, format);
    284    assert_equals(result, 'success', 'decompression should succeed');
    285  }, `the unchanged input for '${format}' should decompress successfully`);
    286 
    287  promise_test(async () => {
    288    const truncatedInput = baseInput.subarray(0, -1);
    289    const { result } = await tryDecompress(truncatedInput, format);
    290    assert_equals(result, 'error', 'decompression should fail');
    291  }, `truncating the input for '${format}' should give an error`);
    292 
    293  promise_test(async () => {
    294    const extendedInput = new Uint8Array([...baseInput, 0]);
    295    const { result } = await tryDecompress(extendedInput, format);
    296    assert_equals(result, 'error', 'decompression should fail');
    297  }, `trailing junk for '${format}' should give an error`);
    298 
    299  for (const { name, offset, length, cases } of fields) {
    300    for (const { value, result } of cases) {
    301      promise_test(async () => {
    302        const corruptedInput = corruptInput(baseInput, offset, length, value);
    303        const { result: actual } =
    304              await tryDecompress(corruptedInput, format);
    305        assert_equals(actual, result, 'result should match');
    306      }, `format '${format}' field ${name} should be ${result} for ${value}`);
    307    }
    308  }
    309 }
    310 
    311 promise_test(async () => {
    312  // Data generated in Python:
    313  // ```py
    314  // h = b"thequickbrownfoxjumped\x00"
    315  // words = h.split()
    316  // zdict = b''.join(words)
    317  // co = zlib.compressobj(zdict=zdict)
    318  // cd = co.compress(h) + co.flush()
    319  // ```
    320  const { result } = await tryDecompress(new Uint8Array([
    321    0x78, 0xbb, 0x74, 0xee, 0x09, 0x59, 0x2b, 0xc1, 0x2e, 0x0c, 0x00, 0x74, 0xee, 0x09, 0x59
    322  ]), 'deflate');
    323  assert_equals(result, 'error', 'Data compressed with a dictionary should throw TypeError');
    324 }, 'the deflate input compressed with dictionary should give an error')