July 16, 2024·4 min read·engineeringrubyxmlrexmlsecurity

REXML 3.3.2 avoids repeatedly matching incomplete XML constructs

The denial-of-service correction addresses expensive parsing of repeated characters. Reading through construct-specific terminators and replacing a broad reference scan reduce repeated work.

An XML parser can spend excessive time discovering that it has not yet read enough input to finish a construct. If it reads a little more and repeats the same unsuccessful match, a simple repeated character can make the parser revisit an increasingly large buffer. REXML 3.3.2 changes several of these reading and matching paths.

The July 16 advisory for CVE-2024-39908 identifies long parsing times for XML containing many selected characters, including <, 0 and %>. It recommends REXML 3.3.2 or later and lists 3.3.1 and earlier as affected. The issue is in the gem, so an interpreter's broad Ruby version does not fully identify the parser an application loads.

I appreciate Ruby's readable collection operations, but repeated scanning is a reminder that readable source can still hide expensive traversal. The relevant question is how much input a loop revisits when a match fails. This release contains concrete changes to that work.

Matching needs the right amount of buffered input

The 3.3.2 release notes identify several parsing-performance changes. One processing-instruction correction adds a terminator argument to matching. The parser can ask the source to read through ?> before retrying the complete processing-instruction pattern. Supplying that information at the call site lets the shared source implementation serve several distinct grammar contexts without guessing their terminators.

Previously, the source's matching loop could read again after an unsuccessful scan without that construct-specific information. Repeated delimiter characters could cause many read-and-match cycles before the parser reached the actual end of the instruction. Supplying the terminator changes where the source obtains enough input for the next meaningful match attempt.

The implementation adds an optional term: keyword to the source matching API. When the scan needs more input, that value passes to the read operation. This preserves the distinction between finding enough bytes and recognizing the complete XML grammar of the construct.

That distinction is useful because reading through a delimiter does not by itself validate everything before it. The complete pattern still decides whether the buffered construct has the required form. The improvement concerns how often the parser attempts that decision while the necessary input is still incomplete.

The same pattern appears in other XML constructs

The comment correction supplies the comment terminator to the same source API. CDATA handling receives its own terminator, while document-type handling supplies information appropriate to the internal subset. A further change applies comment-aware reading inside a document-type declaration.

These are related implementation changes, but each grammar context needs its own endpoint. A processing instruction ends differently from a comment or CDATA section. Treating a single greater-than character as sufficient buffering information for every context can force unnecessary retries when that character is allowed inside the larger construct.

The regression tests exercise repeated greater-than characters inside those contexts. The document-type tests also include repeated %> in malformed entity material. That keeps the performance obligation attached to inputs that expose the repeated work, including ones the parser will ultimately reject.

The advisory itself describes a broader set of selected characters. The individual patches explain several concrete paths behind the release. They should not be reduced to a claim that the parser simply rejects every long run of one character, since legitimate constructs can contain repeated characters too.

Character references get a more explicit scan

Another included correction addresses very long character references containing repeated zeros. Its description identifies the regular-expression denial of service on Ruby 3.1 and earlier. That runtime qualification applies to this particular path, rather than establishing that every issue in the July advisory disappears on newer interpreters.

The change replaces a broad context-sensitive scan in Text.check with an explicit loop. The loop locates the next less-than sign or ampersand, finds the reference boundary and checks the enclosed value. Numeric references receive validation appropriate to decimal or hexadecimal notation before conversion and character validation.

The parser then advances its position beyond the checked reference. This makes progress through the input visible in the implementation. It also separates locating a reference from validating its contents, reducing the responsibilities assigned to a single scan pattern.

The tests include long runs of leading zeros in a numeric reference. Their concern is the relationship between input size and parsing time. They do not imply that numeric references with leading zeros should all be forbidden. The implementation still needs to obtain and validate their represented character.

Performance is part of rejecting input safely

The added tests use the project's linear-performance assertion across increasing input sizes. That is a different property from checking that a document produces a particular tree or exception. A parser can return the correct error and still spend enough time getting there to create a denial-of-service problem.

These upstream performance tests are evidence about the intended regression coverage. The advisory gives no universal request-duration figure for a deployed service. The effect on an application depends on which XML parser it uses and whether external callers can supply inputs reaching these paths.

Other changes in REXML 3.3.2 tighten document handling. The release notes describe exceptions for extra content after the root element and misplaced XML declarations. Those are compatibility-relevant parser corrections separate from the timing fixes, so an upgrade can change which malformed documents an application accepts.

The corrected dependency is the gem

The advisory recommends updating REXML itself. A dependency declaration can state the corrected lower bound for an application that manages the parser through Bundler.

gem "rexml", ">= 3.3.2"

The selected and deployed gem must then satisfy that requirement. I find the release most useful as an example of parser work whose cost depends on the reading strategy around a regular expression. REXML now supplies more precise terminators to buffering and performs character-reference checks in explicit stages, preserving parsing behavior while reducing the repeated work identified in these corrections.