November 18, 2021·4 min read·engineeringdockersecurityociimages

Docker rejects image documents with ambiguous types

Engine 20.10.11 mitigates ambiguous OCI manifest parsing. Identical document bytes can have different meanings when the HTTP response chooses which structure a client interprets.

A digest can identify the exact bytes of an image document while leaving its interpretation ambiguous. The Moby advisory published November 18 describes this problem in OCI manifest parsing. Before Engine 20.10.11, Docker trusts the HTTP Content-Type header to choose how to deserialize a document. If that header changes between pulls, the same document digest can lead to different interpretations.

The November 17 Engine release contains the mitigation for CVE-2021-41190 and updates containerd to 1.4.12. Docker now rejects an image manifest containing a manifests field and an index containing a layers field. That prevents the contradictory shapes described by the advisory from being accepted through the affected path.

I value content addressed images because the artifact can be identified independently of a mutable name. This issue interests me because the digest calculation can be entirely correct. The missing constraint concerns which kind of object those verified bytes represent.

A manifest and an index describe different objects

The existing OCI image manifest specification defines a manifest for a single image and platform. It references a configuration object and an ordered set of filesystem layers. The base layer appears first, with subsequent layers applied in stack order. Interpreting those references produces the contents of an image.

An OCI image index is a higher-level object. Its manifests array references other manifests, potentially for different platforms. Entries can describe the operating system and CPU architecture required by the image. Choosing an entry leads to a manifest, which then describes the image configuration and layers.

These are different operations on different reference structures. Processing layers means handling the filesystem components of one image. Processing manifests means following references to other image documents. A JSON object containing both structures leaves a client needing a rule about which one has authority.

Both specifications use schemaVersion with the value 2, so that property alone cannot distinguish the object types. The older documents also describe a top-level mediaType property as reserved for compatibility. A descriptor's media type and a document's own media type have different positions and roles. Merely observing that a type-related field exists somewhere is insufficient to resolve an inconsistent document.

The manifest specification also assigns required media types to the referenced configuration and layers. Those fields describe the child objects, rather than selecting the type of their containing document. Validation therefore has to consider where a field appears. A correct layer descriptor does not settle whether a document containing an additional manifest collection should be accepted as an image manifest.

The transport header supplied the interpretation

The OCI Distribution advisory from November 17 identifies the specification-level ambiguity. Distribution Specification 1.0.0 and earlier use the HTTP content type to determine the document type during push and pull. A document containing both relevant structures can be read as either a manifest or an index without that accompanying header.

This creates a mismatch between two sources of information. A digest commits to the response body. The HTTP header controls how a consumer interprets that body. Changing only the header leaves the hashed bytes unchanged while changing which structure the consumer follows. The weakness therefore does not require a hash collision or replacement of the document under the same content digest.

The advisory proposes requiring a document's mediaType, when present, to match the HTTP content type. It also describes rejecting ambiguous combinations of fields as a client workaround. Docker's own advisory specifies the rejection conditions implemented in Engine 20.10.11. The immediate repair is consequently a constraint on accepted documents.

This is a useful distinction from trying to guess which structure an author meant. A preference rule could make one client deterministic while allowing another client to choose differently. Rejecting the inconsistent structure removes that document from the accepted input set. The caller receives a failure instead of an image whose meaning depends on which interpretation won.

Platform selection makes the distinction consequential

The index specification gives each manifest entry descriptor information such as digest and size. Its platform object can specify architecture and operating system, with optional requirements including an operating system version. Those values participate in selecting an appropriate image for an execution environment.

Once an entry is selected, the image manifest's layer order determines how its filesystem is assembled. Moving between those two interpretations changes the reference relationships the consumer traverses. That is why the distinction concerns the contents of the image eventually selected, rather than simply how a metadata object is displayed.

The specification's allowance for an empty manifests array provides another useful detail. Presence and non-emptiness are different validation questions. A parser checking only whether an array has entries can miss that an incompatible field is present at all. Docker's advisory describes the repair in terms of fields contained in the documents.

I regard explicit rejection as appropriate here because image identity is supposed to remain understandable across distribution. A registry and several clients should agree on what a referenced object is. Accepting contradictory structures makes that agreement depend on metadata outside the hashed document and on the particular consumer's parsing rules.

The mitigation belongs in each consumer

The Engine release notes update containerd as well as Docker's own distribution handling. Those entries matter because the same specification ambiguity can affect more than one implementation. The notes identify containerd 1.4.12 as part of the mitigation supplied in this release, without implying that every independent image consumer has received the same repair.

The Moby advisory's workaround is to pull only from trusted sources. That reduces exposure where an update cannot yet be applied, but it does not redefine the document grammar. Engine 20.10.11 supplies a specific rule that rejects the incompatible structures. Its effect is to keep a digest-addressed document from selecting different image meanings through a change in the response type.