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:
- Send a first text frame with
FIN=0 and RSV1=1.
- Send a final continuation frame with
FIN=1 and RSV1=1.
The message is delivered to the application as:
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
Summary
websockets==16.0appears to accept apermessage-deflatecontinuation 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:
Expected behavior
The second frame should raise
ProtocolErrorbecause it is a continuation frame withRSV1=1.Actual behavior
The second frame is accepted and decompressed.
I also reproduced this end to end with
permessage-deflatenegotiated:FIN=0andRSV1=1.FIN=1andRSV1=1.The message is delivered to the application as:
Possible cause
In
PerMessageDeflate.decode(), the continuation-frame branch checks whether continuation data should be decoded, but it does not appear to rejectframe.rsv1. The returned frame then hasrsv1=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:
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