Skip to content

Type-unsafe handling of block attributes and global styles values can cause fatal TypeErrors #80494

Description

@ramonjd

Summary

Some block supports and the block library consume block attributes and theme.json/global styles values as if their PHP types were guaranteed.

They are not!

block.json type/default and theme.json schema are not enforced at render time for hand-edited, imported, or AI-generated serialized content.

A wrong-typed value survives deserialization and reaches a strict operation, producing a fatal TypeError on the front end.

Reported in the wild (AI site generation is one known source):

Uncaught TypeError: preg_match(): Argument #2 ($subject) must be of type string, array given in lib/block-supports/layout.php

This keeps happening, and we keep fixing it one crash at a time.

Recent reactive fixes, all merged (several backported to core):

This issue proposes auditing and hardening the remaining sites proactively rather than waiting for each one to surface as a production fatal.

Progress

Scope note

Only fatal operations are listed. sprintf('%s'), str_replace, preg_replace, and "{$array}" interpolation only emit warnings (garbage CSS, not a crash) and are excluded.

Fatal-throwing sinks include preg_match/preg_quote/explode/trim/ltrim/rtrim/strlen/substr/strtolower/str_contains/str_starts_with/str_ends_with/parse_url/tag_escape/is_email/addcslashes/esc_url on an array, and array/array-typed values used as array offsets in isset()/array_key_exists()/[].

Correction: esc_attr(), esc_html(), and wp_strip_all_tags() are not fatal sinks. esc_attr/esc_html run the value through wp_check_invalid_utf8(), which does (string) $value — an array becomes the string "Array" with a warning, not a crash. wp_strip_all_tags() self-guards non-scalar input since WP 6.7 (returns '' with an E_USER_WARNING); Gutenberg requires WP 6.9. Only esc_url() among the escaping functions fatals on an array (via ltrim). An earlier version of this issue over-listed esc_attr/wp_strip_all_tags sites as fatal; they are excluded or dropped below.

Two complementary defense layers

  1. Render-time guards (the bulk of this issue): guard before the strict op and treat a wrong-typed value as absent. This is the Block Supports: Improve handling of block class name to avoid fatal #80214 / Elements: Guard against non-string className in render filter #78841 / Block Supports: Handle nested array block gap values properly #80464 idiom.
  2. Save / REST-time validation (the Global Styles: Reject non-string custom CSS in the REST controller #80338 idiom): for values persisted through a REST route (global styles, custom CSS), add a schema type and a validate_callback that returns WP_Error so bad data is rejected with a 400 and never stored. Preferred where a save boundary exists, because it stops the value at the door instead of guarding every downstream reader.

Target areas

Block supports:

  • lib/block-supports/layout.php:551-552 -> explode() on layout.contentSize / wideSize
  • lib/block-supports/layout.php:1450,1479 -> preg_quote() on attrs.tagName (Group)
  • lib/block-supports/layout.php:750,757,770,781 -> array_key_exists() with justifyContent/verticalAlignment as key
  • lib/block-supports/layout.php:409,1110,1114,1123 -> minimumColumnWidth / orientation / type
  • lib/block-supports/typography.php:230 -> str_contains() on style.typography.* (6 callers: fontFamily, fontStyle, fontWeight, textDecoration, textTransform, letterSpacing)
  • lib/block-supports/block-style-variations.php:63 -> explode() on variation ref; guard exists but the invalid-ref branch is missing a continue

Block library:

  • button/index.php:68,71,93 -> str_starts_with/substr/str_ends_with on style.dimensions.width
  • breadcrumbs/index.php:188 -> addcslashes() on separator
  • tag-cloud/index.php:19 -> preg_match() on smallestFontSize
  • navigation-link/index.php:154 -> parse_url() on url
  • social-link/index.php:39,47,121 -> is_email/parse_url/service offset
  • rss/index.php:18,36 -> untrailingslashit(feedURL), trim(rel)
  • template-part/index.php:155 -> tag_escape(tagName)
  • form/index.php:25,58 -> action (experiment-flagged)
  • latest-posts/index.php:62 -> array_column(categories)
  • playlist / playlist-track -> track attrs to esc_url
  • navigation/index.php:621, page-list/index.php:319, pattern/index.php:43,47 -> attribute-as-array-offset
  • media-text/index.php:34, cover/index.php:137 -> focalPoint['x']/['y'] arithmetic (fatal when focalPoint is a string, or x/y is an array)
  • query/index.php:121,124,145 -> queryId used as an array key in $dirty_enhanced_queries[...] (only when enhancedPagination is true)

Excluded — not fatal on WP >= 6.9 (see Scope note), cosmetic only:

  • table-of-contents/index.php:27, tab-list/index.php:26, search/index.php:151 -> wp_strip_all_tags(ariaLabel/buttonText). The wrapper self-guards; guarding them only avoids an E_USER_WARNING / supplies a fallback label.

Proposed fix

Apply the established idiom already used in the PRs above and in sibling code (anchor.php:45; dimensions.php gutenberg_is_explicit_aspect_ratio_value): guard with is_string()/is_array()/is_scalar() (and/or an explicit cast) before each strict operation, treating a wrong-typed value as absent. Land as small, per-file PRs; all are core backport candidates.

Non-goal / related work

Generation-time validation for AI agents (e.g. a WordPress Abilities API style validator that hands agents the theme's constraints and checks their output before save) would reduce the rate of bad input, but does not remove the need for these guards: malformed content already exists in databases and will keep arriving from tools, imports, and hand-editing that don't validate.

Prevention and hardening are complementary. This issue covers hardening.

Metadata

Metadata

Assignees

No one assigned

    Labels

    [Status] In ProgressTracking issues with work in progress[Type] Code QualityIssues or PRs that relate to code quality

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions