← Chrome — 2026-W05

9e619333d25e5fd2ae1f9b9a3eacb35a95cb10f3  Only support range syntax for range type features

severity low class InputValidation CVSS 3.1 confidence 0.4 signal score 4.5 third_party/blink/renderer AI review CONFIRMED
Primitive: Range operators (>, <, =) were incorrectly accepted for non-range media/container features (e.g. overflow-inline > scroll), being matched as if ':'.
Triage note: CSS parser input-validation/spec-conformance fix with no memory-safety consequence. Web-reachable but only tightens grammar acceptance; kept as low-severity hardening of parser input handling.

The bug at a glance

This is a CSS parser correctness bug, not a memory-safety bug. Before the patch, the media/container query parser accepted range comparison operators (>, <, >=, <=, =) against feature names that are not range-type features - e.g. @media (overflow-inline > scroll) - and then treated the comparison as if it had been an equality/: match. The consequence is that malformed queries parsed as valid and were evaluated with attacker-unintended semantics, so a page could get a media/container query to match (or not match) in a way the author did not write and the spec does not permit. There is no corruption, no cross-origin leak, and no privilege boundary crossed; the worst realistic outcome is incorrect style application or a subtle interop/compat divergence that content could rely on to fingerprint engine behaviour or smuggle a query past a naive CSS sanitizer that only allow-lists :-form features. Hence low / CVSS 3.1. It is flagged security-relevant mainly because permissive parsing of untrusted CSS is a supply-line concern, and because the change also tightens which feature names may appear in range position, closing a small parser-differential gap.

The fix reframes the model from ‘does this feature set support ranges at all?’ (a set-wide SupportsRange() bool) to ‘is this specific feature a Type:range feature?’ (a per-feature IsRangeTypeFeature(name)). That distinction is the whole bug: the old code let any non-min-/max-/-- feature slip through in range context as long as the set supported ranges, so discrete features like overflow-inline/overflow-block were parseable with </>. The angle worth noting for auditors is the parser differential it created - Blink would accept and (mis)evaluate (overflow-inline > scroll) while spec-conformant engines reject it - and that the same refactor also relocated block-size/inline-size out of the plain media allow-list, so the surface of which names are legal where shifted in two files at once.

Root cause

The FeatureSet virtual SupportsRange() is deleted and replaced by IsRangeTypeFeature(const AtomicString& feature), implemented per feature set. In MediaQueryParser::ConsumeAllowedNameInRange, the logic previously was: consume the name; if null return null; if it starts with min-/max-/-- return null (those can’t be range-compared); otherwise return the name (accept it in range position). That final ‘otherwise accept’ is the defect - it accepted every remaining feature, including discrete ones. The new logic returns the name only if !name.IsNull() && feature_set.IsRangeTypeFeature(name), and returns g_null_atom otherwise, so a discrete feature in range position is now rejected outright. Correspondingly, ConsumeFeature drops the old if (!feature_set.SupportsRange()) return nullptr; gate and the CHECK(!feature_set.SupportsRange()) guard on the style-range branch, because range eligibility is now decided per name inside the range-name consumer. Each feature set implements IsRangeTypeFeature: the media set enumerates the true Type:range media features (width/height, device-width/height, aspect-ratio, resolution, color, color-index, monochrome, device-pixel-ratio, viewport-segments, etc.); the container size set enumerates width/height/inline-size/block-size/aspect-ratio/orientation; style/state container sets return false. Note media’s plain ConsumeAllowedName list also had block-size/inline-size removed - those belong to container size queries, not top-level media - narrowing the media name surface.

Key code

ConsumeAllowedNameInRange now admits a name only if it is a range-type feature

AtomicString name = ConsumeAllowedName(stream, feature_set);
if (!name.IsNull() && feature_set.IsRangeTypeFeature(name)) {
  return name;
}
return g_null_atom;

// FeatureSet API change:
// - virtual bool SupportsRange() const = 0;               // removed
// + virtual bool IsRangeTypeFeature(const AtomicString&) const = 0;  // added

Patch walkthrough

  • third_party/blink/renderer/core/css/parser/media_query_parser.h — Replaces the set-wide virtual SupportsRange() with per-feature IsRangeTypeFeature(feature) on the FeatureSet interface, and changes MediaQueryFeatureSet to declare it out-of-line rather than returning a constant true.
  • third_party/blink/renderer/core/css/parser/media_query_parser.cc — Implements MediaQueryFeatureSet::IsRangeTypeFeature enumerating the genuine Type:range media features. Rewrites ConsumeAllowedNameInRange to accept a name only when IsRangeTypeFeature(name) is true (previously it accepted any non-min-/max-/– name). Removes the SupportsRange() short-circuit in ConsumeFeature and the CHECK(!SupportsRange()) guard on the style-range path. Also removes block-size/inline-size from the plain media ConsumeAllowedName allow-list.
  • third_party/blink/renderer/core/css/parser/container_query_parser.cc — Updates the size-query feature set to implement IsRangeTypeFeature (true only for width/height/inline-size/block-size/aspect-ratio/orientation, with the note that min-/max- prefixed variants still can’t be range-matched), and the style/state container feature sets to return false, replacing their old SupportsRange() overrides.
  • third_party/blink/renderer/core/css/parser/container_query_parser.h — Mirrors the interface change for the style-query feature set, returning false from IsRangeTypeFeature while keeping SupportsStyleRange() true.
  • third_party/blink/renderer/core/css/parser/container_query_parser_test.cc — Updates the test feature-set stub to override IsRangeTypeFeature (true) in place of SupportsRange().
  • third_party/blink/web_tests/external/wpt/css/mediaqueries/test_media_queries.html — Adds regression assertions that overflow-block = scroll and overflow-block < scroll are unknown (rejected), plus a batch of semicolon cleanups. This is the observable behaviour the fix enforces.

Background

mf-range / range syntax — Media Queries Level 4 range context: (width > 600px), (400px <= width <= 800px). Only ‘Type:range’ features (those with an orderable value) may use comparison operators; discrete features (overflow-block, overflow-inline, scan, grid, …) may not.

Type:range vs discrete feature — A per-feature CSS spec property. Range features have a min-/max- form and support <, <=, >, >=, =. Discrete features only support plain feature or feature: value.

container / style / state queries — Container queries reuse the media-query feature-set machinery; style container queries (style(--x > --y)) have their own range handling, which is why the sets distinguish SupportsStyleRange() from ordinary range eligibility.

parser differential — When one engine accepts syntax others reject, content can branch on it. Permissive acceptance of range operators on discrete features is exactly such a divergence.

Vulnerability window

  1. report — Bug 425248661 notes Blink incorrectly accepting range syntax on non-range features (e.g. overflow-inline > scroll), evaluating them as :.
  2. fix — 2026-01-28: Rune Lillesveen adds IsRangeTypeFeature to the FeatureSet API and gates range-name consumption on it, landing WPT coverage. Merged as #1575976.

Proof of concept

The diff itself supplies the regression cases as WPT assertions - these are legitimate reproduction inputs (not exploit code). Pre-patch, @media (overflow-block < scroll) parsed and matched as if written overflow-block: scroll; post-patch it is rejected as an unknown expression. This is a parser-behaviour demonstration only; there is no memory-safety effect to weaponize.

// From test_media_queries.html (regression assertions added by the patch):
expression_should_be_unknown("overflow-block = scroll");
expression_should_be_unknown("overflow-block < scroll");

/* Illustrative content that behaved wrongly pre-patch: */
@media (overflow-inline > scroll) { /* was incorrectly matched as ':' */ }

Exploitation

  1. reach — Any untrusted stylesheet or inline reaches the media/container query parser; no special privilege is needed to submit CSS.
  2. effect — Pre-patch, a discrete feature written with a range operator would parse and evaluate as an equality match, so authored CSS could apply styles under conditions the syntax should have rejected - a correctness/interop divergence, not a memory bug.
  3. ceiling — Maximum practical impact is incorrect styling, engine fingerprinting via the differential, or bypassing a naive CSS filter that only reasons about :-form features. No corruption, disclosure, or sandbox effect.

Detection & hunting

For defenders and detection engineers:

  • stylesheets using comparison operators on discrete features — Static CSS scans for overflow-block/overflow-inline/scan/grid etc. adjacent to <, >, or = flag content that relied on the old permissive behaviour.
  • WPT css/mediaqueries results — The added expression_should_be_unknown cases fail on pre-patch builds; use them to confirm whether a given build carries the fix.

Audit directions

  • FeatureSet implementations — Verify every feature set’s IsRangeTypeFeature list matches the CSS spec’s Type:range designation; an over-broad list re-opens the same permissive-parsing gap, an under-broad one rejects legitimate range queries.
  • ConsumeAllowedName allow-lists — Confirm the media vs container size-query name partitions are correct after block-size/inline-size were moved; a name legal in the wrong context is another parser differential.
  • range vs style-range branching — Re-check ConsumeFeature now that SupportsRange()/its CHECK are gone - ensure style-range and ordinary-range paths remain mutually exclusive per feature set.

Before / after

Loading diff…