Penligent Header

CVE-2023-44487 PoC: Safe HTTP/2 Rapid Reset Validation

A CVE-2023-44487 PoC result is meaningful only when it connects HTTP/2 stream cancellation to work inside the receiving server. Rapid Reset exploits a mismatch between those lifetimes: a client can close streams quickly while the server continues paying for requests already admitted. A low open-stream count can therefore coexist with growing application work.

The attacks publicly disclosed in October 2023 used repeated request creation and cancellation to impose disproportionate processing costs, as documented in Cloudflare’s original technical analysis. The practical assessment turns on the receiving implementation, its work-admission policy, and the traffic path. A client timeout or connection reset alone cannot distinguish an exposed server from one actively rejecting the traffic.

A closed stream can leave unfinished work

HTTP/2 multiplexes streams over a connection. Resetting one stream is a normal protocol operation: a client may abandon a response because a user navigated elsewhere or an operation is no longer needed. RFC 9113 defines RST_STREAM as terminating the affected stream. It does not give the client a mechanism to undo CPU time already spent by the receiver.

A request passes through several stages before a response exists: protocol parsing, request validation, route selection, handler admission, and perhaps an upstream call or database operation. Resetting the client-facing stream can happen between any two of them. The receiver has already spent some resources and may have transferred responsibility for others to a separate component.

The ordering determines what cancellation can save. A request still waiting for a handler may be discarded before application code runs. Once a handler has started, the implementation must signal cancellation and wait for that work to finish or cooperate. If a proxy has dispatched another request upstream, there is a second connection and another lifecycle to inspect.

This also explains why ordinary access logs can be incomplete witnesses. A log written only after a response completes may omit requests canceled earlier in processing. An application counter cannot account for protocol parsing that was rejected before the application started. Choose observations at the layer being investigated rather than assuming every request will appear in one familiar log.

Concurrent streams and admitted work are different quantities

Google’s technical analysis explains the central limit mismatch: closing streams quickly permits further requests without keeping the same number of streams open simultaneously. The receiver can incur work for many requests while its instantaneous open-stream count remains within the negotiated limit. Limiting the number present at one moment does not, by itself, limit how quickly they are replaced.

Imagine a request whose stream closes while its handler waits on a slow dependency. The stream slot can become available before the handler returns. If the next request starts another handler solely because that slot is free, the dependency can accumulate work even though the protocol’s concurrent-stream limit is being respected. This is an illustrative ordering, not a measured traffic trace.

Keep three quantities separate during assessment: streams currently open, handlers currently executing, and downstream operations still outstanding. A stream limit bounds the first quantity. To bound the second, accounting must follow handler completion. The third depends on how work is dispatched and canceled across application and dependency boundaries.

This is why a lower concurrency setting needs an implementation-specific explanation. It may reduce admitted work in a fixed server, but its name alone says nothing about how canceled streams are charged. Read the vendor’s remedy as a change to resource accounting, not merely as a smaller number in a configuration file.

What the Go handler scheduler changes

The Go vulnerability record GO-2023-2102 gives a concrete implementation example. It tracks the related Go issue under CVE-2023-39325 and explains that handlers could continue executing after their streams were reset. The described fix bounds simultaneously executing handlers, queues additional requests, and closes a connection when the pending queue becomes excessive. CVE-2023-39325 and CVE-2023-44487 should remain distinct identifiers in an inventory.

A pinned Go HTTP/2 source snapshot makes that accounting concrete. Its scheduleHandler checks running-handler capacity before launching application work and otherwise places the request in a bounded pending path. In handlerDone, completion frees capacity; queued entries whose streams have already disappeared are skipped. This snapshot is the implementation examined here, rather than an asserted identification of the original 2023 patch commit.

The following is a reading aid, not executable code or a reproduction of the original patch:

When a request is ready:
    Start its handler only if running-handler capacity remains.
    Otherwise, apply the pending-work limit and queue it.

When a handler actually finishes:
    Release its running-handler capacity.
    Discard queued requests whose streams have already disappeared.
    Start eligible queued work while capacity remains.

The critical event is handler completion. A peer resetting a stream no longer means that the associated executing handler has finished. Pending requests also need a separate guard: moving excess work into a queue would otherwise exchange an execution problem for a queue-growth problem. Skipping abandoned entries saves application work that has not yet started, while the connection-level guard limits the pending path.

Those bounds apply to this connection’s scheduler. They are not a global service budget: a deployment must still consider the aggregate work admitted across connections and instances. Nor does an application inherit the inspected behavior merely because it is written in Go. Establish whether its serving binary uses this implementation, another HTTP/2 stack, or a maintained fork.

The same source includes the internal label too_many_early_resets and a connection error using ENHANCE_YOUR_CALM. Do not turn the label into a universal logging query: a product must actually expose that internal event for an operator to observe it. The protocol error code alone also cannot identify the source branch or the reason a particular deployment closed a connection.

Conceptual Rapid Reset mechanism connecting stream creation and cancellation to server work and resource pressure.
The stream and the work it initiated can have different lifetimes. Inspect the implementation’s admission and completion accounting.

Cancellation must reach the work that can stop

Go’s Request.Context documentation says an incoming request’s context is canceled when the client connection closes, when an HTTP/2 request is canceled, or when the handler returns. That is an application-visible signal. It is not a promise that every goroutine or dependency immediately releases its resources.

The context package describes carrying cancellation and deadlines through API boundaries. Application code and its dependencies must participate in that propagation. A handler that starts downstream work with an unrelated background context has broken the request’s cancellation chain. A long CPU-bound loop that never checks for cancellation can keep running after its result is no longer useful.

Some requests intentionally create work that survives disconnection, such as an accepted background job. That work needs its own admission limit, ownership, and deadline. Detaching a goroutine from the incoming context supplies independence, but no queue budget or overload policy. Review the handoff as a deliberate change in resource ownership.

Review one expensive handler from request entry through its dependency calls. Identify where it passes the incoming context, where it creates an unrelated one, and where a library defines its own timeout behavior. An ordinary cancellation test in an isolated application can then check that path without attempting to reproduce Rapid Reset. No live cancellation experiment or attack client was executed for this article.

The protocol fix bounds admission in the affected server; application cooperation determines how promptly some admitted work can stop. Both matter, but they operate at different points. Review resource release at the expensive dependency rather than assuming that a canceled handler context means every operation has already ended.

Determine which server the client is actually testing

In a deployment with a CDN, load balancer, reverse proxy, and application, a public HTTP/2 connection terminates at a particular hop. The next hop may use a different connection and even a different protocol. The client-visible stream identifier therefore does not automatically identify an application request or a backend connection.

Trace the owner-approved path from client to HTTP/2 terminator and onward to the application. For each hop, record the implementation, negotiated protocol, and the event that ends its request. A correlation identifier can connect logs, but it does not make the connection lifetimes identical. The client-facing reset and an upstream completion are distinct observations.

This matters in both directions. A protected edge can reject traffic before the origin sees it, so an origin with no corresponding requests may simply be unreachable through that test path. An origin reachable through another address can have a different exposure from the public route. Assess known owner-approved origin paths explicitly; do not infer their state from the edge response.

Conversely, an edge that accepts a request and quickly dispatches upstream can move part of the cost into the application tier. A client observing a reset learns little about whether the origin’s database query completed. The useful telemetry follows the request through admission, cancellation, completion, and resource release at the affected hops.

Observation pointQuestion it can answerQuestion it leaves open
Test clientWhich protocol was negotiated and what response or connection event occurred?Which internal resource did the receiver consume?
HTTP/2 termination pointWere streams admitted, reset, queued, or rejected by that implementation?Did previously dispatched downstream work finish?
Application handlerDid the request start, see cancellation, and return?Did a detached operation or dependency keep running?
Downstream dependencyWas the operation canceled, completed, or retained in a queue?Did another tier remain overloaded?

Read a PoC result as an experiment with competing explanations

Read a third-party PoC’s success condition before running it. A counter of frames written locally measures client activity. A socket closure measures a connection event. An application health check measures one functional outcome at a particular time. None of those, by itself, reveals how many handlers were admitted or whether the implementation enforced the relevant bound.

A timeout is especially ambiguous. The client may have exhausted its own sockets or CPU. A firewall may have dropped the connection. A protective edge may have rate-limited it. An application dependency may already have been slow. A test with no visibility into these alternatives cannot attribute the delay to the HTTP/2 scheduling issue.

For an owner-approved isolated evaluation, select an unchanged legitimate request as a control and collect the server’s relevant admission, cancellation, completion, and resource observations. Record a healthy baseline first. Set stop conditions from that lab’s capacity and recovery requirements; there is no portable request rate or duration that makes a disruptive test safe or its conclusion valid.

Review existing vendor tests and regression material before adding load of your own. If a transport-level test is necessary, run it where failure cannot interrupt unrelated tenants or production dependencies. The experiment should answer a bounded question about one identified implementation and configuration, rather than using an outage to stand in for root-cause analysis.

Compare the sequence after the test stops as carefully as the peak. Does pending work drain? Do executing handlers finish? Does the legitimate control recover? A patched admission path can reject new work while an application dependency remains saturated by earlier requests. That is a different repair question from failure to enforce the protocol-layer bound, and the evidence should preserve the distinction.

Verify the running artifact and the configuration together

The Go advisory lists historical fixes in Go 1.20.10, Go 1.21.3, and golang.org/x/net/http2 v0.17.0. These identify the 2023 remediation boundary for the affected branches; they are not runtime recommendations for a deployment in October 2026. Use the product’s currently supported release and security guidance, including any vendor-maintained backport.

Build provenance can connect that guidance to an executable. For a Go binary that exposes build information, the documented go version command supports:

go version -m /path/to/the/server-binary

The path must refer to the artifact that actually runs. Inspecting the developer workstation’s Go version says nothing about a service built months earlier in another pipeline. A module listing is useful evidence, but forks, replaced dependencies, stripped metadata, and vendor backports can require additional build records. Tie the binary or container digest to the running workload, not merely to a repository manifest.

Configuration is equally important. The historical NGINX Plus release notes qualify their Rapid Reset update discussion with the case of keepalive_requests configured substantially above its default. That is a product-specific condition, not a general prescription for every HTTP/2 server. Read the guidance for the exact product, release, and active configuration rather than copying a tuning value from another implementation.

After an update, verify that the new artifact is serving traffic and that the intended configuration was loaded. A successful package installation does not prove an old process was replaced. In a rolling deployment, one checked instance does not establish that every serving instance changed. Include edge-managed protections and remaining origin paths in the same deployment record.

Rapid Reset remediation workflow connecting implementation inventory, supported updates, configuration review, and controlled verification.
Verification connects the serving artifact and active configuration to the traffic path under assessment.

An assessment is ready to close when the serving artifact and active configuration match the supported remedy, the relevant admission behavior is understood, and the deployment’s legitimate traffic remains usable. Keep unresolved application cancellation or origin-exposure questions explicit. The goal is to account for work from admission to release; a passing PoC message cannot do that accounting for the operator.

Share the Post:
Related Posts
en_USEnglish