Make WordPress Core

Opened 2 months ago

Closed 2 months ago

Last modified 2 months ago

#65441 closed defect (bug) (duplicate)

Formatting: esc_url() triggers PHP 8.1 deprecation when passed null

Reported by: dervishov Owned by:
Priority: normal Milestone:
Component: Formatting Version:
Severity: normal Keywords: has-patch has-unit-tests php81
Cc: Focuses:

Description (last modified by sabernhardt)

esc_url() triggers a PHP 8.1+ deprecation notice when null is passed as the $url parameter:

Deprecated: ltrim(): Passing null to parameter #1 ($string) of type string is deprecated in wp-includes/formatting.php on line 4545                                                                                                   

Problem

In esc_url(), the function checks for an empty string on line 4541:

  if ( '' === $url ) {                                                                                                                                                                                                                  
      return $url;                                                                                                                                                                                                                    
  }                                                                                                                                                                                                                                     

However, this does not account for null. When null passes through, it reaches line 4545:

  $url = str_replace( ' ', '%20', ltrim( $url ) );                                                                                                                                                                                      

Since PHP 8.1, passing null to ltrim() (which expects a string parameter) triggers an E_DEPRECATED notice. On PHP 8.2+, this will become a TypeError fatal error, breaking sites entirely.

Why this matters

esc_url() is one of the most heavily used escaping functions in WordPress. It is called thousands of times across core, themes, and plugins. Multiple core functions that are commonly passed to esc_url() can return null:

  • get_edit_post_link() — returns null when the post type has no editing UI, the post doesn't exist, or the user lacks edit_post capability. Used in:
    • wp-admin/includes/meta-boxes.php:275
    • wp-admin/edit-form-comment.php:186
    • wp-admin/comment.php:207
    • wp-admin/edit.php:464
    • wp-admin/async-upload.php:68
  • get_edit_comment_link() — returns null when the comment doesn't exist or user lacks edit_comment capability
  • get_edit_bookmark_link() — returns null when user lacks manage_links capability
  • get_previous_posts_page_link() — returns null on the first page (fixed individually in [62034], but the root cause in esc_url() was not addressed)

Beyond core, thousands of third-party plugins and themes call esc_url() with values that may be null. Hardening esc_url() itself provides defense in depth for the entire ecosystem.

Proposed fix

Add null === $url to the existing empty-value check:

  if ( '' === $url || null === $url ) {                                                                                                                                                                                                 
      return '';
  }                                                                                                                                                                                                                                     

This:

  • Matches the pre-PHP 8.1 behavior exactly (where ltrim(null) silently returned '', and esc_url(null) returned '')
  • Introduces no behavioral change for any other input type
  • Protects all current and future callers without requiring individual null guards at each call site
  • Follows the same defensive pattern applied in [62034] for previous_posts()

Tests included

Two new unit tests added to tests/phpunit/tests/formatting/escUrl.php:

  • test_null_input_returns_empty_string — verifies esc_url( null ) returns ''
  • test_sanitize_url_null_input_returns_empty_string — verifies sanitize_url( null ) returns '' (since sanitize_url() calls esc_url() internally)

Full PHPUnit suite passes: 29,457 tests, 0 failures.
PHPCS passes with no violations.

References

Change History (3)

#1 @knutsp
2 months ago

  • Keywords close added

Since null is not a string, passing null should give a warning. Removing that is not hardening anything, it's the opposite - it's hiding developer errors.

This ticket was mentioned in PR #12132 on WordPress/wordpress-develop by @dervishov.


2 months ago
#2

## Summary

Fix a PHP 8.1+ deprecation notice in esc_url() when null is passed as the $url parameter.

On PHP 8.1+, passing null to esc_url() triggers:

Deprecated: ltrim(): Passing null to parameter #1 ($string) of type string is deprecated

This occurs because esc_url() calls ltrim( $url ) on line 4545 without first checking for null. Multiple core functions that return string|null (such as get_edit_post_link(), get_edit_comment_link(), get_edit_bookmark_link()) have their return values passed directly to esc_url() throughout the codebase.

## Fix

Add null === $url to the existing empty check, returning an empty string. This matches the pre-PHP 8.1 behavior where ltrim(null) silently returned ''.

Follow-up to [r62034] which fixed the same pattern in previous_posts().

## Tests

  • test_null_input_returns_empty_string — verifies esc_url( null ) returns ''
  • test_sanitize_url_null_input_returns_empty_string — verifies sanitize_url( null ) returns ''

#3 @sabernhardt
2 months ago

  • Description modified (diff)
  • Keywords close removed
  • Milestone Awaiting Review
  • Resolutionduplicate
  • Status newclosed
  • Version trunk

This is already reported on #61268.

The esc_url() function expects a string, and developers who misuse functions should be notified. When esc_url() returns an empty string, the markup could have an empty href="" instead of a usable target URL. #61268 also has an example of plugins that use an echoing function inside esc_url(), which then prints the output without escaping anything.

On the other hand, passing a null value was already silenced, in older PHP versions and/or before [47808] used strict comparison.

Last edited 2 months ago by sabernhardt (previous) (diff)
Note: See TracTickets for help on using tickets.