March 21, 2024·4 min read·engineeringrubyrdocsecurityserialization

RDoc restricts object restoration from options and documentation caches

CVE-2024-27281 crosses the boundary between documentation data and Ruby objects. The corrected releases limit what the options and cache loaders can restore.

A documentation tool can execute consequential code before it begins rendering any documentation. RDoc reads local configuration and cached documentation, and those inputs can contain serialized Ruby objects. Allowing unrestricted object restoration turns files that appear to be passive project data into an execution boundary.

The March 21 advisory for CVE-2024-27281 identifies object injection and possible remote code execution through .rdoc_options and a crafted documentation cache. Corrected RDoc releases are 6.3.4.1, 6.4.1.1, 6.5.1.1 and 6.6.3.1. The notice specifically warns that the corresponding versions without the final .1 have an incorrect fix.

I find Ruby's consistent object model easier to follow than the conventions I came from in Perl. This issue shows the other side of treating data as objects. A loader needs to control which classes it can restore, because obtaining an instance is more consequential than reading a string or a number.

Configuration loading happens before type validation

The advisory names .rdoc_options as a YAML configuration file. The vulnerability allows unrestricted classes to be restored while parsing it. That operation happens before the program can decide whether the resulting value is a suitable set of RDoc options.

The corrected options loader in RDoc 6.6.3.1 makes the distinction visible. It first checks whether the file exists. When it does, the loader uses YAML.safe_load with permitted classes limited to RDoc::Options and Symbol beyond the loader's ordinary data support.

Only afterward does it check that the result is an RDoc::Options instance or a hash. A hash is used to construct the options object. An empty file falls back to new default options. These steps preserve useful configuration forms while restricting the restoration step itself.

Checking the resulting object's class would be insufficient on its own. If unrestricted deserialization has already created unexpected objects, the dangerous behavior can precede the final validation. The security boundary belongs at the point that input data requests object construction, which is why the safe loader's allowed classes matter.

This does not make every YAML document a program. The risk comes from a deserializer with permission to restore classes beyond the simple configuration data the application needs. The fix changes that permission while retaining the specific objects the RDoc options format uses.

The cache uses another serialization mechanism

The same advisory identifies a crafted documentation cache as another route to object injection. Its presence matters because correcting YAML configuration alone would leave a separate loader that consumes external file contents. A tool can have several deserialization boundaries even when only one configuration filename attracts attention.

The corrected store implementation uses a marshal_load helper for its cache. This helper opens the file in binary mode and supplies MarshalFilter to Marshal.load. The filter recognizes ordinary data types and selected documentation-related objects.

For other objects, the implementation checks the class name and raises a TypeError unless it begins with RDoc::. This is a concrete restriction on restored cache contents. It should not be generalized into advice that arbitrary Marshal.load input becomes safe merely by attaching any callback.

The two loaders therefore use different mechanisms for their respective formats. YAML options pass permitted classes to the safe loader. The documentation store applies its RDoc-specific filter during Marshal loading. Both changes concern the kinds of objects file contents may introduce into the running documentation process.

A local file can still be untrusted input

The relevant path need not begin with a network request to a running RDoc server. The advisory describes files the tool reads. If a project or cache contains attacker-supplied serialized contents, processing that material invokes the affected loading code.

That is why calling documentation generation a read-only activity does not resolve the trust question. The intended output may be documentation files, but the process performing the work has the permissions of its execution environment. Deserialization decides what code-level objects that process accepts before it gets to ordinary rendering.

The public notice does not give an application-independent exploit chain for every Ruby installation. It names the vulnerable input paths and reports possible code execution through object injection. A precise account should retain those conditions, without implying that simply having RDoc installed executes a malicious file automatically.

The distinction is particularly useful for build tooling. A repository can contain source text and tool configuration consumed by different parsers. Treating everything under the working directory as equally passive loses the distinction between reading language text and restoring arbitrary runtime objects from a tool's configuration.

Corrected versions differ by Ruby series

The advisory recommends RDoc 6.6.3.1 or later, with compatible corrected versions for older interpreter lines. Ruby 3.0 users can use 6.3.4.1, Ruby 3.1 users 6.4.1.1, and Ruby 3.2 users 6.5.1.1. It also gives a Bundler requirement for the newer corrected release.

gem "rdoc", ">= 6.6.3.1"

That declaration belongs in a Gemfile for an environment compatible with the selected release. The advisory's branch-specific alternatives remain relevant when an older Ruby distribution needs the corresponding RDoc line. Updating the documentation tool is distinct from assuming an interpreter's broad major version determines its loaded gem version.

The warning about the initial incorrect fixes is equally specific. Versions 6.3.4, 6.4.1, 6.5.1 and 6.6.3 should not be substituted for the listed corrected .1 releases. A dependency rule that stops at one of those earlier patch numbers does not express the advisory's recommendation.

My interest in this correction is its placement at the actual loading boundary. RDoc needs to restore its own options and documentation structures, but it does not need unrestricted class restoration to perform that work. The corrected loaders retain the expected formats while limiting the objects those inputs can introduce.