June 29, 2023·4 min read·engineeringrubysecurityuriredos

Ruby's URI follow-up fix stops whitespace backtracking

CVE-2023-36617 follows an incomplete earlier fix and changes whitespace matching in both URI parser implementations, with patched gems for older Ruby series.

A parser fix can remove one expensive path while leaving another way to make invalid input consume excessive processing time. Ruby's June URI advisory is explicit that the earlier correction was incomplete. The follow-up is useful to examine because its source change shows how a small quantifier adjustment changes the search performed during parsing.

The June 29 CVE-2023-36617 advisory releases uri 0.12.2 and 0.10.3. It identifies increased execution time for invalid URLs containing particular characters in rfc2396_parser.rb and rfc3986_parser.rb. The issue follows an incomplete fix for CVE-2023-28755.

I find Ruby's readable syntax useful, but readability at the method level can conceal the work inside a regular expression. Here, the important property is what the matcher can reconsider after a later part of the pattern fails.

Both parser implementations change

The comparison between uri 0.12.1 and 0.12.2 shows modifications in both named parser files. In the RFC2396 parser, the absolute and relative URI expressions change the leading whitespace quantifier from an ordinary repeated match to a possessive one.

The RFC3986 parser makes a corresponding change in the port expression. Leading whitespace precedes optional digits and trailing whitespace. Making the leading repetition possessive prevents it from yielding characters back when a later match fails.

That is a local interpretation of the patch, not a claim that every URL operation now has a particular complexity bound. The advisory establishes the vulnerability and affected versions. The source comparison establishes where the correction changes matching behavior.

The presence of two parser files also matters for understanding completeness. A library can expose multiple parsing implementations or paths. Reviewing only one expression cannot establish that all paths named in an advisory have received the same correction. In this comparison, the older parser changes both its absolute and relative expressions, while the newer parser changes its port check. Those are distinct expressions receiving the same restriction on leading-whitespace backtracking, rather than one shared matcher automatically covering every case.

Possessive matching removes a kind of reconsideration

Ruby's regular-expression documentation in the 3.2 source explains possessive quantifiers. A quantifier followed by a plus sign consumes input like a greedy quantifier but will not give up the characters it matched during backtracking.

That means an ordinary zero-or-more whitespace matcher and its possessive equivalent differ when the surrounding pattern cannot complete. A successful simple input can produce the same result under both. The failed path is where the refusal to backtrack changes the work.

The following small patterns illustrate the syntax, without reproducing the vulnerability's crafted input.

ordinary = /\A\s*\d+\z/
possessive = /\A\s*+\d+\z/

p ordinary.match?(" 42")
p possessive.match?(" 42")

Both accept the ordinary numeric example. The possessive repetition specifically prevents later parts of the match from making the initial whitespace matcher surrender characters. Ruby's documentation also shows cases where possessive matching changes whether a whole pattern succeeds.

That last point is essential. Possessive quantifiers are not a general instruction to make every expression faster without reconsidering its language. They remove a form of search, so a valid use depends on whether the rest of the pattern should ever need characters consumed by that repetition.

In the URI patch, the change applies to leading whitespace before the remaining URI or port structure. The source makes that boundary visible. Understanding it is more useful than copying a possessive quantifier into unrelated application expressions.

Invalid input still reaches meaningful work

The advisory concerns parsing strings into URI objects. An application may plan to reject an invalid URL, but it has already asked the parser to inspect the string. The rejection decision follows the work needed to discover invalidity.

That gives performance on malformed input a different role from ordinary throughput. A parser can handle normal URLs acceptably and still have an expensive failure path. A test containing only successful examples does not exercise the property identified by the advisory.

This is the part of the disclosure I consider most instructive. Correct rejection and affordable rejection are separate requirements. A method that eventually raises an error can still spend too much time reaching that error.

The advisory does not describe code execution or data disclosure. Its stated impact is a regular-expression denial-of-service condition through increased parsing time. Keeping that scope specific explains the risk without attributing other consequences that the source does not establish.

The earlier fix does not cover this advisory

The announcement identifies every uri release through 0.12.1 as affected. A dependency updated for the earlier CVE may therefore still fall within the new affected range. The earlier update's presence cannot stand in for checking this advisory's version guidance.

The project recommends uri 0.12.2 generally. For Ruby 3.0, it provides uri 0.10.3 as a compatibility-preserving alternative. Ruby 3.1 and Ruby 3.2 users can use the 0.12.2 correction.

The documented direct update command targets the library itself. That is the component for which the fixed versions are published.

gem update uri

For an application using the newer compatible series under Bundler, the advisory gives a dependency requirement. The older Ruby 3.0 path needs its corresponding compatible release instead.

gem "uri", ">= 0.12.2"

A requirement permits selection of the fixed release, while the resolved dependency determines what the application actually loads. The compatibility exception is therefore part of the remediation, not an incidental detail to omit from a copied example.

The patch and advisory answer different questions

The advisory credits ooooooo_q for discovery and nobu for the fix. It supplies affected versions, fixed versions, and the relationship to the earlier incomplete correction. Those are the facts an application maintainer needs to identify the relevant update.

The source comparison supplies the mechanism. Leading whitespace no longer participates in the same backtracking search at the modified boundaries. Together, those sources explain why a dependency already updated for March's URI issue needs another correction in June, and why the appropriate gem release depends on the Ruby series running the application.