Re: Prague side meeting: HTTP/2 concurrency and request cancellation (CVE-2023-44487)

Willy Tarreau <w@1wt.eu> Wed, 11 October 2023 09:46 UTC

Return-Path: <ietf-http-wg-request+bounce-httpbisa-archive-bis2juki=ietf.org@listhub.w3.org>
X-Original-To: ietfarch-httpbisa-archive-bis2Juki@ietfa.amsl.com
Delivered-To: ietfarch-httpbisa-archive-bis2Juki@ietfa.amsl.com
Received: from localhost (localhost [127.0.0.1]) by ietfa.amsl.com (Postfix) with ESMTP id C1227C151545 for <ietfarch-httpbisa-archive-bis2Juki@ietfa.amsl.com>; Wed, 11 Oct 2023 02:46:12 -0700 (PDT)
X-Virus-Scanned: amavisd-new at amsl.com
X-Spam-Flag: NO
X-Spam-Score: -7.657
X-Spam-Level:
X-Spam-Status: No, score=-7.657 tagged_above=-999 required=5 tests=[BAYES_00=-1.9, HEADER_FROM_DIFFERENT_DOMAINS=0.249, MAILING_LIST_MULTI=-1, RCVD_IN_DNSWL_HI=-5, RCVD_IN_MSPIKE_H5=0.001, RCVD_IN_MSPIKE_WL=0.001, RCVD_IN_ZEN_BLOCKED_OPENDNS=0.001, SPF_PASS=-0.001, T_SCC_BODY_TEXT_LINE=-0.01, URIBL_DBL_BLOCKED_OPENDNS=0.001, URIBL_ZEN_BLOCKED_OPENDNS=0.001] autolearn=ham autolearn_force=no
Received: from mail.ietf.org ([50.223.129.194]) by localhost (ietfa.amsl.com [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id nbiBS6qhPhxa for <ietfarch-httpbisa-archive-bis2Juki@ietfa.amsl.com>; Wed, 11 Oct 2023 02:46:07 -0700 (PDT)
Received: from lyra.w3.org (lyra.w3.org [128.30.52.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange ECDHE (P-256) server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by ietfa.amsl.com (Postfix) with ESMTPS id A6C99C15109C for <httpbisa-archive-bis2Juki@ietf.org>; Wed, 11 Oct 2023 02:46:07 -0700 (PDT)
Received: from lists by lyra.w3.org with local (Exim 4.94.2) (envelope-from <ietf-http-wg-request@listhub.w3.org>) id 1qqVkC-000h6S-Vi for ietf-http-wg-dist@listhub.w3.org; Wed, 11 Oct 2023 09:43:08 +0000
Resent-Date: Wed, 11 Oct 2023 09:43:08 +0000
Resent-Message-Id: <E1qqVkC-000h6S-Vi@lyra.w3.org>
Received: from mimas.w3.org ([128.30.52.79]) by lyra.w3.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from <w@1wt.eu>) id 1qqVkB-000h5R-C5 for ietf-http-wg@listhub.w3.org; Wed, 11 Oct 2023 09:43:07 +0000
Received: from ded1.1wt.eu ([163.172.96.212] helo=1wt.eu) by mimas.w3.org with esmtp (Exim 4.94.2) (envelope-from <w@1wt.eu>) id 1qqVk8-0096c2-HI for ietf-http-wg@w3.org; Wed, 11 Oct 2023 09:43:07 +0000
Received: (from willy@localhost) by mail.home.local (8.17.1/8.17.1/Submit) id 39B9gsGQ024461; Wed, 11 Oct 2023 11:42:54 +0200
Date: Wed, 11 Oct 2023 11:42:54 +0200
From: Willy Tarreau <w@1wt.eu>
To: Mark Nottingham <mnot@mnot.net>
Cc: HTTP Working Group <ietf-http-wg@w3.org>
Message-ID: <ZSZuHu5iWdh5Rl3b@1wt.eu>
References: <997813D6-7A5B-49E2-A2C3-FCD1B2D5F5BC@mnot.net>
MIME-Version: 1.0
Content-Type: text/plain; charset="us-ascii"
Content-Disposition: inline
In-Reply-To: <997813D6-7A5B-49E2-A2C3-FCD1B2D5F5BC@mnot.net>
Received-SPF: pass client-ip=163.172.96.212; envelope-from=w@1wt.eu; helo=1wt.eu
X-W3C-Hub-Spam-Status: No, score=-7.9
X-W3C-Hub-Spam-Report: BAYES_00=-1.9, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001, W3C_AA=-1, W3C_IRA=-1, W3C_IRR=-3, W3C_WL=-1
X-W3C-Scan-Sig: mimas.w3.org 1qqVk8-0096c2-HI d9999ada91af81253280265e36f0f098
X-Original-To: ietf-http-wg@w3.org
Subject: Re: Prague side meeting: HTTP/2 concurrency and request cancellation (CVE-2023-44487)
Archived-At: <https://www.w3.org/mid/ZSZuHu5iWdh5Rl3b@1wt.eu>
Resent-From: ietf-http-wg@w3.org
X-Mailing-List: <ietf-http-wg@w3.org> archive/latest/51452
X-Loop: ietf-http-wg@w3.org
Resent-Sender: ietf-http-wg-request@w3.org
Precedence: list
List-Id: <ietf-http-wg.w3.org>
List-Help: <https://www.w3.org/email/>
List-Post: <mailto:ietf-http-wg@w3.org>
List-Unsubscribe: <mailto:ietf-http-wg-request@w3.org?subject=unsubscribe>

Hi Mark,

On Wed, Oct 11, 2023 at 10:45:13AM +1100, Mark Nottingham wrote:
> Martin has already drafted one proposal:
>   https://martinthomson.github.io/h2-stream-limits/draft-thomson-httpbis-h2-stream-limits.html

I find that there are interesting points here. Yesterday my coworker
Amaury who works on H3 explained to me how QUIC streams are flow-
controlled and I found the principle much safer. I thought we could
slightly emulate it by sending lots of SETTINGS frames and keeping
track of which one advertised what so that we know what the client
ACKs but that would be totally ugly and complicated.

I do like Martin's idea above however. The only thing is that it will
not protect servers against older clients, hence attackers would just
act like an older client. And if for safety we decide to start lower
with a small MAX_CONCURRENT_STREAMS then it would penalize modern
clients during the first round trip. Or maybe we'd advertise both a
low value for MAX_CONCURRENT_STREAMS for legacy clients immediately
followed by a MAX_STREAMS frame advertising a larger one ? If so we
still need to make sure that it will not cause massive RSTs from
clients between the two :-/ Or maybe something like this could work:

   MAX_CONCURRENT_STREAMS = 100
   MAX_STREAMS = 100
   MAX_CONCURRENT_STREAMS = 10

Older clients would learn 100 then 10, possibly dropping excess
streams, while new clients would learn MAX_STREAMS=100 and from
that point ignore MAX_CONCURRENT_STREAMS=10.

> Other discussions might touch on whether there are other measures (protocol
> mechanisms or otherwise) that clients and servers can take, how concurrency
> is exposed to calling code, and whether we can give better guidance about how
> concurrency violations should be handled.

A few of these were discussed already in the thread below opened by Cory
4 years ago, where it was discussed how to count streams vs limit, and
where I even mentioned this exact method of attack consisting in sending
HEADERS followed by RST_STREAM that would not change the total stream
count from the protocol perspective:

  https://lists.w3.org/Archives/Public/ietf-http-wg/2019JanMar/0131.html

We already faced that issue long ago when multiple haproxy instances
were stacked on top of each other over H2 and too short timeouts on the
front would cause long series of HEADERS+RST_STREAM on the back before
the request had a chance to be processed due to the second layer being
configured with a nice value making it slower than the first one. What
we've been doing in haproxy against this is that instead of counting the
streams at the protocol level, we count attached ones at the application
layer: these are created at the same moment, but they're released once
they're aware of the close. And we stop processing new streams once the
configured limit is passed. This means that for a limit of 100 streams
for example, if we receive 100 HEADERS and their 100 respective
RST_STREAM, it will indeed create 100 streams that are immediately
orphaned (and closed from an H2 perspective), but the 101th HEADERS
frame will interrupt processing until some of these streams are
effectively closed and freed (and not just at the protocol layer).

And from what I've read below from Maxim Dounin, it seems like nginx
applies a very similar strategy (they use x2 margin instead of +1 but
the principle is the same, let streams finish first):

  https://mailman.nginx.org/pipermail/nginx-devel/2023-October/S36Q5HBXR7CAIMPLLPRSSSYR4PCMWILK.html

As such, I suspect that these approaches might be much more common than
what sensationalist mass media want us to believe and that the issue is
more a matter of implementation choices (i.e. resource management) than
of the protocol. We know that the protocol has some deficiencies that make
naive implementations easily attackable (this one, setting stream windows
of 1 byte during transfers, long series of 1-byte HEADERS+CONTINUATION,
various forms of HOL blocking such as zero-windows, unlimited number of
PUSH_PROMISE etc) but I think that once you care about your resource
usage you have to make some reasonable compromises on all of these.

With that said I'd love to plug that hole with an elegant mechanism
involving just an optional new frame ;-)

Cheers,
Willy