Skip to content

Commit 1cd9ce0

Browse files
authored
Reject client enable-push settings (#1318)
Co-authored-by: Miro <200482516+Mirochill@users.noreply.github.com>
1 parent dc5da5c commit 1cd9ce0

4 files changed

Lines changed: 52 additions & 2 deletions

File tree

‎CHANGELOG.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dev
2727
**Bugfixes**
2828

2929
- Fix to allow sending 0 bytes on a stream even if the flow control window is negative.
30+
- Reject non-zero ``SETTINGS_ENABLE_PUSH`` values received from servers.
3031

3132
4.3.0 (2025-08-23)
3233
------------------

‎src/h2/connection.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,6 +1777,8 @@ def _receive_settings_frame(self, frame: SettingsFrame) -> tuple[list[Frame], li
17771777
return [], events
17781778

17791779
# Add the new settings.
1780+
for setting, value in frame.settings.items():
1781+
self.remote_settings.validate_received_setting(setting, value)
17801782
self.remote_settings.update(frame.settings)
17811783
events.append(
17821784
RemoteSettingsChanged.from_settings(

‎src/h2/settings.py‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class Settings(MutableMapping[SettingCodes | int, int]):
126126
"""
127127

128128
def __init__(self, client: bool = True, initial_values: dict[SettingCodes, int] | None = None) -> None:
129+
self._client = client
130+
129131
# Backing object for the settings. This is a dictionary of
130132
# (setting: [list of values]), where the first value in the list is the
131133
# current value of the setting. Strictly this doesn't use lists but
@@ -287,6 +289,23 @@ def __setitem__(self, key: SettingCodes | int, value: int) -> None:
287289

288290
items.append(value)
289291

292+
def validate_received_setting(self, setting: SettingCodes | int, value: int) -> None:
293+
"""
294+
Validate a setting received from the peer that owns this Settings
295+
object.
296+
297+
Servers may advertise ``ENABLE_PUSH`` only as ``0`` in received
298+
SETTINGS frames.
299+
"""
300+
invalid = _validate_setting(setting, value, client=self._client)
301+
302+
if invalid != ErrorCodes.NO_ERROR:
303+
msg = f"Setting {setting} has invalid value {value}"
304+
raise InvalidSettingsValueError(
305+
msg,
306+
error_code=invalid,
307+
)
308+
290309
def __delitem__(self, key: SettingCodes | int) -> None:
291310
del self._settings[key]
292311

@@ -311,13 +330,23 @@ def __ne__(self, other: object) -> bool:
311330
__hash__ = MutableMapping.__hash__
312331

313332

314-
def _validate_setting(setting: SettingCodes | int, value: int) -> ErrorCodes:
333+
def _validate_setting(
334+
setting: SettingCodes | int,
335+
value: int,
336+
*,
337+
client: bool | None = None,
338+
) -> ErrorCodes:
315339
"""
316340
Confirms that a specific setting has a well-formed value. If the setting is
317341
invalid, returns an error code. Otherwise, returns 0 (NO_ERROR).
342+
343+
If ``client`` is set, the setting originated from a peer with that role.
318344
"""
319345
if setting == SettingCodes.ENABLE_PUSH:
320-
if value not in (0, 1):
346+
# RFC 9113 section 6.5.2: "A client MUST treat receipt of a
347+
# SETTINGS frame with SETTINGS_ENABLE_PUSH set to 1 as a connection
348+
# error (Section 5.4.1) of type PROTOCOL_ERROR."
349+
if value not in (0, 1) or (client is False and value != 0):
321350
return ErrorCodes.PROTOCOL_ERROR
322351
elif setting == SettingCodes.INITIAL_WINDOW_SIZE:
323352
if not 0 <= value <= 2147483647: # 2^31 - 1

‎tests/test_invalid_frame_sequences.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import h2.errors
1111
import h2.events
1212
import h2.exceptions
13+
import h2.settings
1314

1415

1516
class TestInvalidFrameSequences:
@@ -266,6 +267,23 @@ def test_reject_invalid_settings_values(self, frame_factory, settings) -> None:
266267
h2.errors.ErrorCodes.PROTOCOL_ERROR
267268
)
268269

270+
def test_reject_server_enable_push_updates(self, frame_factory) -> None:
271+
"""
272+
Clients reject servers that advertise non-zero SETTINGS_ENABLE_PUSH
273+
values in received SETTINGS frames.
274+
"""
275+
c = h2.connection.H2Connection(config=self.client_config)
276+
c.initiate_connection()
277+
278+
f = frame_factory.build_settings_frame(
279+
settings={h2.settings.SettingCodes.ENABLE_PUSH: 1},
280+
)
281+
282+
with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
283+
c.receive_data(f.serialize())
284+
285+
assert e.value.error_code == h2.errors.ErrorCodes.PROTOCOL_ERROR
286+
269287
@pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes])
270288
def test_invalid_frame_headers_are_protocol_errors(self, frame_factory, request_headers) -> None:
271289
"""

0 commit comments

Comments
 (0)