Personal notes on making things Programming ✳ Technology ✳ Life
Back to the notebook
WordPress / STORY 4 MIN READ

Escape WordPress output where it is printed

Build a small resource card that keeps labels as text, contains quotation marks, and refuses unwanted URL protocols.

Keep text as text
THE IDEA, THEN THE DETAILS
In this piece

A resource card looks harmless: a label, a link, and a tooltip. Then someone enters a label containing quotation marks or HTML. If the template prints those values directly, text can become markup. I want the label to stay text, even when it looks like code.

Let’s build one small renderer and check the exact HTML it returns. This is a standalone teaching function, not a plugin to activate. Use a disposable WordPress installation with WP-CLI; the checks below were run on WordPress 7.1.2 and PHP 8.4.3.

The short version

  • Choose escaping for the place where a value is printed.
  • Keep quotation marks around attribute values.
  • Test the rejected URL path as well as an ordinary link.

Give each output position its own check

The label appears twice, but those positions have different jobs. Between the anchor tags it is visible text, so I use esc_html(). Inside the quoted title attribute I use esc_attr(). Encoding the quotation marks keeps them inside that attribute instead of letting them introduce another one.

The href is a URL context. esc_url() handles URL output and can take an explicit protocol list. This example permits HTTP and HTTPS protocols. If it returns an empty string, the renderer produces a paragraph instead of a link that points nowhere.

Save a tiny renderer

Create escape-card.php outside the public web directory. WordPress must be loaded before this file runs; WP-CLI does that for the test. Nothing here saves settings or changes posts.

escape-card.php
<?php
function notebook_resource_card(string $label, string $url): string {
    $href = esc_url($url, ['https', 'http']);
    if ($href === '') {
        return '<p>' . esc_html($label) . '</p>';
    }
    return '<a href="' . $href . '" title="'
        . esc_attr($label) . '">'
        . esc_html($label) . '</a>';
}

I keep the escaping next to the string assembly so a reviewer can see each boundary. The database is not an HTML context: storing a value earlier does not tell us how it will be used later. WordPress recommends this approach in its escaping guidance.

This function deliberately treats the label as plain text. If the product requires formatted labels, define a small allowed HTML vocabulary and use an appropriate KSES policy. Do not remove escaping merely because one label needs bold text.

Save the following beside the renderer, then run wp eval-file /absolute/path/check-escaping.php from the disposable installation. These expected strings are written out so a broken renderer cannot manufacture its own expected answer. In the double-quoted PHP strings, \x26 means an ampersand; this keeps entity spellings intact when copying the test through an editor.

check-escaping.php
<?php
require __DIR__ . '/escape-card.php';
$cases = [
    ['Read <b>carefully</b>', 'https://example.org/guide',
     "<a href=\"https://example.org/guide\" title=\"Read \x26lt;b\x26gt;carefully\x26lt;/b\x26gt;\">Read \x26lt;b\x26gt;carefully\x26lt;/b\x26gt;</a>"],
    ['" onmouseover="alert(1)', 'https://example.org/',
     "<a href=\"https://example.org/\" title=\"\x26quot; onmouseover=\x26quot;alert(1)\">\x26quot; onmouseover=\x26quot;alert(1)</a>"],
    ['Open guide', 'javascript:alert(1)', '<p>Open guide</p>'],
    ['Email', 'mailto:reader@example.org', '<p>Email</p>'],
    ['Empty', '', '<p>Empty</p>'],
];
foreach ($cases as [$label, $url, $expected]) {
    $actual = notebook_resource_card($label, $url);
    if ($actual !== $expected) {
        throw new RuntimeException('Unexpected markup: ' . $actual);
    }
}
echo "PASS: 5 rendering cases\n";

The expected result is PASS: 5 rendering cases. The apparent event handler remains part of the title and visible label; it never becomes a new attribute. The JavaScript URL, email URL, and empty URL produce plain paragraphs. A browser check also confirmed that the quote case creates only href and title attributes, with no nested HTML in the label.

Keep the promise narrow

Escaping a URL does not establish that its destination is trustworthy. This helper is not an external-domain allowlist, and it does not require an absolute URL; relative paths are a separate policy decision. It also does not authorize a user to create or edit a resource. That belongs in the request handler, as explained in the nonce and permissions tutorial.

Before using the renderer in a real template, keep a copy of that template and test the replacement on staging. Roll back by restoring the previous file. The standalone checks themselves have no database changes to undo.

Try it yourself

Add a label containing A & B. Write the expected HTML before running the test: both label positions should contain A &amp; B in the HTML source, while the browser displays one ampersand. Then remove esc_attr() temporarily; the quotation-mark case should fail. Restore it and rerun.

END OF STORY

KEEP WANDERING

A WordPress nonce is only one part of the check

Build a small REST endpoint and follow the boundaries between authentication, post permissions, input validation, and a successful save.

Read the next story
YOUR READING LIST

Saved for later.

Saved in this browser only.