From d4b2126f07818e094c6423a3229151f1d27c74db Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Tue, 2 Feb 2021 20:53:53 +0000 Subject: [PATCH] Security: add Content-Security-Policy script loaders. Add new functions `wp_get_script_tag`, `wp_print_script_tag`, `wp_print_inline_script_tag` and `wp_get_inline_script_tag` that support script attributes. Enables passing attributes such as `async` or `nonce`, creating a path forward for enabling a Content-Security-Policy in core, plugins and themes. Props tomdxw, johnbillion, jadeddragoon, jrchamp, mallorydxw, epicfaace, alinod, enricocarraro, ocean90. Fixes #39941. git-svn-id: https://develop.svn.wordpress.org/trunk@50167 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/functions.php | 120 ++++++++++++++++ .../tests/functions/wpInlineScriptTag.php | 119 ++++++++++++++++ .../functions/wpSanitizeScriptAttributes.php | 130 ++++++++++++++++++ tests/phpunit/tests/functions/wpScriptTag.php | 94 +++++++++++++ 4 files changed, 463 insertions(+) create mode 100644 tests/phpunit/tests/functions/wpInlineScriptTag.php create mode 100644 tests/phpunit/tests/functions/wpSanitizeScriptAttributes.php create mode 100644 tests/phpunit/tests/functions/wpScriptTag.php diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 5935f781fb..56f50ce2de 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -7866,3 +7866,123 @@ function is_php_version_compatible( $required ) { function wp_fuzzy_number_match( $expected, $actual, $precision = 1 ) { return abs( (float) $expected - (float) $actual ) <= $precision; } + +/** + * Sanitizes an attributes array into an attributes string to be placed inside a `\n", wp_sanitize_script_attributes( $attributes ) ); +} + +/** + * Prints formatted `\n", wp_sanitize_script_attributes( $attributes ), $javascript ); +} + +/** + * Prints inline JavaScript wrapped in `\n", + wp_get_inline_script_tag( + $this->event_handler, + array( + 'type' => 'application/javascript', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + + remove_theme_support( 'html5' ); + + $this->assertSame( + '\n", + wp_get_inline_script_tag( + $this->event_handler, + array( + 'type' => 'application/javascript', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + } + + public function test_get_inline_script_tag_type_not_set() { + add_theme_support( 'html5', array( 'script' ) ); + + $this->assertSame( + "\n", + wp_get_inline_script_tag( + $this->event_handler, + array( + 'async' => false, + 'nomodule' => true, + ) + ) + ); + + remove_theme_support( 'html5' ); + } + + public function test_get_inline_script_tag_unescaped_src() { + add_theme_support( 'html5', array( 'script' ) ); + + $this->assertSame( + "\n", + wp_get_inline_script_tag( $this->event_handler ) + ); + + remove_theme_support( 'html5' ); + } + + public function test_print_script_tag_prints_get_inline_script_tag() { + add_filter( + 'wp_inline_script_attributes', + function ( $attributes ) { + if ( isset( $attributes['id'] ) && 'utils-js-extra' === $attributes['id'] ) { + $attributes['async'] = true; + } + return $attributes; + } + ); + + add_theme_support( 'html5', array( 'script' ) ); + + $attributes = array( + 'id' => 'utils-js-before', + 'nomodule' => true, + ); + + $this->assertSame( + wp_get_inline_script_tag( $this->event_handler, $attributes ), + get_echo( + 'wp_print_inline_script_tag', + array( + $this->event_handler, + $attributes, + ) + ) + ); + + remove_theme_support( 'html5' ); + + $this->assertSame( + wp_get_inline_script_tag( $this->event_handler, $attributes ), + get_echo( + 'wp_print_inline_script_tag', + array( + $this->event_handler, + $attributes, + ) + ) + ); + } +} diff --git a/tests/phpunit/tests/functions/wpSanitizeScriptAttributes.php b/tests/phpunit/tests/functions/wpSanitizeScriptAttributes.php new file mode 100644 index 0000000000..dd060ebd5d --- /dev/null +++ b/tests/phpunit/tests/functions/wpSanitizeScriptAttributes.php @@ -0,0 +1,130 @@ +assertSame( + ' type="application/javascript" src="https://DOMAIN.TLD/PATH/FILE.js" nomodule', + wp_sanitize_script_attributes( + array( + 'type' => 'application/javascript', + 'src' => 'https://DOMAIN.TLD/PATH/FILE.js', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + + remove_theme_support( 'html5' ); + + $this->assertSame( + ' src="https://DOMAIN.TLD/PATH/FILE.js" type="application/javascript" nomodule="nomodule"', + wp_sanitize_script_attributes( + array( + 'src' => 'https://DOMAIN.TLD/PATH/FILE.js', + 'type' => 'application/javascript', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + } + + function test_sanitize_script_attributes_type_not_set() { + add_theme_support( 'html5', array( 'script' ) ); + + $this->assertSame( + ' src="https://DOMAIN.TLD/PATH/FILE.js" nomodule', + wp_sanitize_script_attributes( + array( + 'src' => 'https://DOMAIN.TLD/PATH/FILE.js', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + + remove_theme_support( 'html5' ); + + $this->assertSame( + ' src="https://DOMAIN.TLD/PATH/FILE.js" nomodule="nomodule"', + wp_sanitize_script_attributes( + array( + 'src' => 'https://DOMAIN.TLD/PATH/FILE.js', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + } + + + function test_sanitize_script_attributes_no_attributes() { + add_theme_support( 'html5', array( 'script' ) ); + + $this->assertSame( + '', + wp_sanitize_script_attributes( array() ) + ); + + remove_theme_support( 'html5' ); + } + + function test_sanitize_script_attributes_relative_src() { + add_theme_support( 'html5', array( 'script' ) ); + + $this->assertSame( + ' src="PATH/FILE.js" nomodule', + wp_sanitize_script_attributes( + array( + 'src' => 'PATH/FILE.js', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + + remove_theme_support( 'html5' ); + } + + + function test_sanitize_script_attributes_only_false_boolean_attributes() { + add_theme_support( 'html5', array( 'script' ) ); + + $this->assertSame( + '', + wp_sanitize_script_attributes( + array( + 'async' => false, + 'nomodule' => false, + ) + ) + ); + + remove_theme_support( 'html5' ); + } + + function test_sanitize_script_attributes_only_true_boolean_attributes() { + add_theme_support( 'html5', array( 'script' ) ); + + $this->assertSame( + ' async nomodule', + wp_sanitize_script_attributes( + array( + 'async' => true, + 'nomodule' => true, + ) + ) + ); + + remove_theme_support( 'html5' ); + } + +} diff --git a/tests/phpunit/tests/functions/wpScriptTag.php b/tests/phpunit/tests/functions/wpScriptTag.php new file mode 100644 index 0000000000..3c451f5359 --- /dev/null +++ b/tests/phpunit/tests/functions/wpScriptTag.php @@ -0,0 +1,94 @@ +assertSame( + '' . "\n", + wp_get_script_tag( + array( + 'type' => 'application/javascript', + 'src' => 'https://localhost/PATH/FILE.js', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + + remove_theme_support( 'html5' ); + + $this->assertSame( + '' . "\n", + wp_get_script_tag( + array( + 'src' => 'https://localhost/PATH/FILE.js', + 'type' => 'application/javascript', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + } + + function test_get_script_tag_type_not_set() { + add_theme_support( 'html5', array( 'script' ) ); + + $this->assertSame( + '' . "\n", + wp_get_script_tag( + array( + 'src' => 'https://localhost/PATH/FILE.js', + 'async' => false, + 'nomodule' => true, + ) + ) + ); + + remove_theme_support( 'html5' ); + } + + function test_print_script_tag_prints_get_script_tag() { + add_filter( + 'wp_script_attributes', + function ( $attributes ) { + if ( isset( $attributes['id'] ) && 'utils-js-extra' === $attributes['id'] ) { + $attributes['async'] = true; + } + return $attributes; + } + ); + + add_theme_support( 'html5', array( 'script' ) ); + + $attributes = array( + 'src' => 'https://localhost/PATH/FILE.js', + 'id' => 'utils-js-extra', + 'nomodule' => true, + ); + + $this->assertSame( + wp_get_script_tag( $attributes ), + get_echo( + 'wp_print_script_tag', + array( $attributes ) + ) + ); + + remove_theme_support( 'html5' ); + + $this->assertSame( + wp_get_script_tag( $attributes ), + get_echo( + 'wp_print_script_tag', + array( $attributes ) + ) + ); + } +}