Validation, Sanitization, and Escaping in a WordPress Plugin

Validation, sanitization, and escaping solve different problems in a WordPress plugin. Validation decides whether input is acceptable. Sanitization converts allowed input into a safer canonical form. Escaping makes a value safe for its exact output context. Applying all three indiscriminately is not extra security; applying each at the correct boundary is. This guide follows an external documentation URL from an admin field into post metadata and back into HTML, so the distinctions stay concrete. The example also shows where authorization fits beside these data controls.
Write the data contract first
The field accepts an optional HTTPS URL no longer than 2,048 characters. It must not accept relative paths, JavaScript schemes, arrays, or malformed URLs. On the front end, it appears in an anchor’s href attribute. That short contract determines every later choice.
Without a contract, “sanitize this value” becomes guesswork. A generic text sanitizer could leave a syntactically plausible string that is still not a usable URL. Validation should therefore decide whether the value belongs in the system.
Register metadata with a sanitization callback
Registering metadata documents the type and ensures updates through supported APIs use the same callback. Authentication and authorization remain separate concerns.
add_action( 'init', function () {
register_post_meta( 'post', 'adz_docs_url', array(
'type' => 'string',
'single' => true,
'default' => '',
'show_in_rest' => false,
'sanitize_callback' => 'adz_sanitize_docs_url',
'auth_callback' => function ( $allowed, $meta_key, $post_id ) {
return current_user_can( 'edit_post', $post_id );
},
) );
} );
Expose metadata through REST only when a real client requires it. If you enable show_in_rest, treat its schema and permissions as a public interface that needs tests.
Validate before normalizing
Accept an empty string because the field is optional. Reject non-strings and overly long input. Then use WordPress URL helpers and require HTTPS.
function adz_sanitize_docs_url( $value ) {
if ( ! is_string( $value ) ) {
return '';
}
$value = trim( $value );
if ( '' === $value ) {
return '';
}
if ( strlen( $value ) > 2048 ) {
return '';
}
$url = esc_url_raw( $value, array( 'https' ) );
if ( '' === $url || 'https' !== wp_parse_url( $url, PHP_URL_SCHEME ) ) {
return '';
}
return $url;
}
esc_url_raw() prepares a URL for database or redirect use; despite its name, it is not the final output escape. The explicit scheme check makes the business rule readable.
Save only in the intended editor flow
A classic meta-box handler must verify the nonce, reject autosaves and revisions, check the post type, and authorize the specific post.
function adz_save_docs_url( $post_id ) {
if ( ! isset( $_POST['adz_docs_nonce'] )
|| ! is_string( $_POST['adz_docs_nonce'] )
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['adz_docs_nonce'] ) ), 'adz_save_docs_url' )
) {
return;
}
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( 'post' !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$raw = isset( $_POST['adz_docs_url'] ) && is_string( $_POST['adz_docs_url'] )
? wp_unslash( $_POST['adz_docs_url'] )
: '';
update_post_meta( $post_id, 'adz_docs_url', $raw );
}
add_action( 'save_post_post', 'adz_save_docs_url' );
The registered callback sanitizes the value when metadata is updated. Keeping the handler’s responsibility limited makes its authorization path easier to review.
Escape for each output context
The same stored value needs different escaping depending on where it appears.
$url = get_post_meta( get_the_ID(), 'adz_docs_url', true );
if ( $url ) {
printf(
'<a href="%1$s" rel="noopener noreferrer">%2$s</a>',
esc_url( $url ),
esc_html__( 'Read the external documentation', 'adz-docs' )
);
}
For an editable input, use esc_attr( $url ) inside the value attribute. For visible text, use esc_html(). For structured HTML intentionally supplied by trusted users, use a narrowly defined wp_kses() allowlist. Output context—not the database column—chooses the escape function.
Avoid the most common category errors
- Escaping before storage: encoded entities can be encoded again later and corrupt data.
- Sanitizing instead of rejecting: silently changing an invalid identifier or date can create the wrong record.
- Trusting database values: imports and older code can bypass today’s write path.
- Using one helper everywhere: HTML text, attributes, URLs, JavaScript, and SQL have different contexts.
- Forgetting prepared SQL: output escaping does not protect a custom database query; use
$wpdb->prepare().
Test the contract, not just the callback
- Save a valid HTTPS URL and confirm it round-trips unchanged.
- Try HTTP, relative, malformed, and
javascript:values. - Submit an array and an overlong string.
- Attempt an update without permission or a valid nonce.
- Inject a hostile value directly into metadata and confirm front-end output remains safe.
These tests cover both entry and exit boundaries. A unit test for one sanitizer cannot prove that a render callback escaped the result.
When invalid input needs to preserve the user’s original entry for correction, do not store the invalid value in production metadata. Return a structured error to the form or API client and redisplay the request value only after escaping it for the form control. This separates error recovery from canonical storage. It also prevents a temporary validation failure from becoming data that another output path assumes has already passed the contract.
Use a boundary checklist during review
At every input, ask what exact values the feature accepts, who may submit them, and how invalid data is reported. At every output, identify the HTML, URL, JavaScript, or SQL context and escape or prepare at the last responsible moment. WordPress summarizes the core mindset as never trusting data and escaping late; the official security guidance is worth keeping beside your plugin review checklist. Clear data contracts turn those principles into code another developer can verify.
Photo by Jakub Zerdzicki on Pexels.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


