Skip to content

permessage-deflate accepts continuation frames with RSV1 set #1720

Description

@caveeroo

Summary

websockets==16.0 appears to accept a permessage-deflate continuation frame when the RSV1 bit is set.

My reading of RFC 7692 is that RSV1 marks the first frame of a compressed message. It should not be set on continuation frames. If a continuation frame has RSV1 set, the connection should fail with a protocol error.

Reproduction

This small test shows the behavior at the extension layer:

import zlib

import pytest

from websockets.exceptions import ProtocolError
from websockets.extensions.permessage_deflate import PerMessageDeflate
from websockets.frames import Frame, OP_CONT, OP_TEXT


def compressed_message(data: bytes) -> bytes:
    compressor = zlib.compressobj(wbits=-15)
    compressed = compressor.compress(data) + compressor.flush(zlib.Z_SYNC_FLUSH)
    return compressed[:-4]


def test_permessage_deflate_rejects_rsv1_on_continuation():
    extension = PerMessageDeflate(False, False, 15, 15)

    compressed = compressed_message(b'{"cmd":"admin"}')
    split = max(1, len(compressed) // 2)

    extension.decode(Frame(OP_TEXT, compressed[:split], fin=False, rsv1=True))

    with pytest.raises(ProtocolError):
        extension.decode(Frame(OP_CONT, compressed[split:], fin=True, rsv1=True))

Expected behavior

The second frame should raise ProtocolError because it is a continuation frame with RSV1=1.

Actual behavior

The second frame is accepted and decompressed.

I also reproduced this end to end with permessage-deflate negotiated:

  1. Send a first text frame with FIN=0 and RSV1=1.
  2. Send a final continuation frame with FIN=1 and RSV1=1.

The message is delivered to the application as:

{"cmd":"admin"}

Possible cause

In PerMessageDeflate.decode(), the continuation-frame branch checks whether continuation data should be decoded, but it does not appear to reject frame.rsv1. The returned frame then has rsv1=False, so later frame validation no longer sees the RSV1 bit from the original continuation frame.

Possible fix

One possible fix is to reject RSV1 before decoding continuation-frame extension data:

diff --git a/src/websockets/extensions/permessage_deflate.py b/src/websockets/extensions/permessage_deflate.py
@@
         if frame.opcode is frames.OP_CONT:
+            if frame.rsv1:
+                raise ProtocolError("reserved bits must be 0")
             if not self.decode_cont_data:
                 return frame
             if frame.fin:
                 self.decode_cont_data = False

References

RFC 7692, section 6.2:
https://datatracker.ietf.org/doc/html/rfc7692#section-6.2

RFC 7692, section 7.2.2:
https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions