Classic theme security standards
Skill Lonsdale201/wp-agent-skills/theme-development/classic-theme-security-standards
A community-maintained collection of agent skills for WordPress plugin and theme development.
npx -y skills add Lonsdale201/wp-agent-skills --skill classic-theme-security-standardsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Write or audit secure modern classic PHP WordPress theme code for WP 7.0. Covers template output escaping, `esc_html`, `esc_attr`, `esc_url`, `wp_kses_post`, `wp_json_encode`, translation escaping, `wp_unslash` + sanitize input, validation, nonces and capability checks for theme forms, safe `template_include`/`get_template_part`, enqueued assets instead of inline tags, `$wpdb->prepare`, WPCS naming/filenames, namespacing/prefixing, no shorthand PHP tags, and no closing PHP tag. Use when reviewing `functions.php`, `header.php`, `page.php`, `404.php`, template parts, search/comment forms, or any classic theme code touching request data or dynamic output.
SKILL.md
8.7 KB, as published. Nobody here has run it
Classic Theme Security Standards
Use this when writing or reviewing PHP in a classic theme. Themes output most of the HTML on a site, so their main security failure mode is unsafe dynamic output.
This skill complements wp-security-audit; it is theme-specific and focused on templates, functions.php, template parts, and theme forms.
When to Use This Skill
- Reviewing
header.php,footer.php,index.php,page.php,single.php,archive.php,404.php,comments.php, ortemplate-parts/*.php. - Reviewing
functions.phporinc/*.phpin a theme. - Theme code reads
$_GET,$_POST,$_REQUEST,$_COOKIE, or custom query vars. - Theme code prints custom fields, options, term/user meta, search values, image URLs, classes, or inline JS.
- Theme code has a custom form, AJAX handler,
template_includeoverride, or direct SQL.
Escape on Output
Escape at the last possible moment, based on context.
| Output context | Use |
|---|---|
| Text node | esc_html( $value ) |
| HTML attribute | esc_attr( $value ) |
| URL | esc_url( $url ) |
| Textarea | esc_textarea( $value ) |
| CSS class fragment | sanitize_html_class( $class ), then esc_attr() if composing attributes |
| Limited trusted HTML | wp_kses_post( $html ) or wp_kses( $html, $allowed_html ) |
| JavaScript data | wp_json_encode( $data ) inside wp_add_inline_script() |
| Translation in text | esc_html__( 'Text', 'mytheme' ) or esc_html_e() |
| Translation in attribute | esc_attr__( 'Text', 'mytheme' ) or esc_attr_e() |
<a class="card-link" href="<?php echo esc_url( get_permalink() ); ?>">
<?php echo esc_html( get_the_title() ); ?>
</a>
Do not escape full editor content with esc_html(). For normal post content, use the_content() and let WordPress content filters run. For custom HTML fields, use wp_kses_post() or a narrower allowlist.
Sanitize Input, Validate Meaning
Request data is slashed. Unslash first, then sanitize, then validate.
$raw_layout = isset( $_GET['layout'] )
? wp_unslash( $_GET['layout'] )
: 'grid';
$layout = sanitize_key( $raw_layout );
if ( ! in_array( $layout, array( 'grid', 'list' ), true ) ) {
$layout = 'grid';
}
Sanitization changes shape; validation decides whether the value is allowed. Do both when values control template branches, queries, file choices, classes, or external URLs.
Forms Need Nonces
Themes should rarely mutate data. If a theme includes a form that changes state, use a nonce and capability check where relevant.
<form method="post">
<?php wp_nonce_field( 'mytheme_profile_action', 'mytheme_profile_nonce' ); ?>
<input type="text" name="display_name" value="">
<button type="submit"><?php esc_html_e( 'Save', 'mytheme' ); ?></button>
</form>
add_action( 'template_redirect', 'mytheme_handle_profile_form' );
function mytheme_handle_profile_form(): void {
if ( empty( $_POST['mytheme_profile_nonce'] ) ) {
return;
}
$nonce = sanitize_text_field( wp_unslash( $_POST['mytheme_profile_nonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'mytheme_profile_action' ) ) {
wp_die( esc_html__( 'Invalid request.', 'mytheme' ), '', array( 'response' => 403 ) );
}
if ( ! is_user_logged_in() ) {
wp_die( esc_html__( 'You must be logged in.', 'mytheme' ), '', array( 'response' => 403 ) );
}
if ( ! current_user_can( 'edit_user', get_current_user_id() ) ) {
wp_die( esc_html__( 'You cannot edit this profile.', 'mytheme' ), '', array( 'response' => 403 ) );
}
$display_name = isset( $_POST['display_name'] )
? sanitize_text_field( wp_unslash( $_POST['display_name'] ) )
: '';
wp_update_user(
array(
'ID' => get_current_user_id(),
'display_name' => $display_name,
)
);
}
If the form is core search or comments, prefer get_search_form(), comment_form(), and core comment APIs instead of custom handlers.
Safe Template Loading
Never include a PHP file based on request input.
// Wrong: request-controlled file path.
include get_template_directory() . '/views/' . $_GET['view'] . '.php';
Use a fixed map or locate_template():
$view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'grid';
$templates = array(
'grid' => 'template-parts/archive-grid.php',
'list' => 'template-parts/archive-list.php',
);
get_template_part(
str_replace( '.php', '', $templates[ $view ] ?? $templates['grid'] )
);
For full-template overrides, return a controlled path from template_include; do not include-and-exit from template_redirect.
Assets and Inline Data
Do not hardcode <script> or <link> tags in templates. Use enqueues.
wp_enqueue_script(
'mytheme-main',
get_theme_file_uri( 'assets/js/main.js' ),
array(),
MYTHEME_VERSION,
array(
'strategy' => 'defer',
'in_footer' => true,
)
);
For inline configuration:
wp_add_inline_script(
'mytheme-main',
'window.MyTheme = ' . wp_json_encode(
array(
'homeUrl' => home_url( '/' ),
'isRtl' => is_rtl(),
)
) . ';',
'before'
);
Do not pass <script> tags to wp_add_inline_script(); WordPress strips and warns.
Database and Queries
Prefer WordPress APIs over SQL:
WP_Query,get_posts(),get_terms(),get_users().get_post_meta(),get_term_meta(),get_user_meta().update_post_meta()only when the theme truly owns presentation-specific meta.
If SQL is unavoidable, use $wpdb->prepare():
global $wpdb;
$count = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
'post',
'publish'
)
);
Do not build SQL with concatenated request data.
Coding Standards Baseline
- Use full PHP tags:
<?php, never short tags. - Do not close PHP-only files with
?>. - Prefix global functions/hooks with the theme slug, or use a unique namespace for non-template code.
- Use lowercase, hyphenated file names.
- Use
require_oncefor mandatory includes. - Text domain must match the
Text Domainheader. - Escape translated strings with the context-specific helper.
- Keep feature code that should survive a theme switch in a plugin.
Theme Review Checklist
- Every
echo,printf, attribute interpolation, and custom field output has context-appropriate escaping. - Request data uses
wp_unslash()before sanitization. - Values controlling templates/classes/query args are allowlisted.
- Mutating forms have nonce checks and permission checks.
template_includereturns only controlled, existing theme paths.- Assets are enqueued; inline data uses
wp_json_encode(). - No direct SQL without
$wpdb->prepare(). header.phphaswp_head()andwp_body_open();footer.phphaswp_footer().
Cross-References
- Broader WP security checklist:
wp-security-audit - Secrets/key handling:
wp-security-secrets - Theme structure and asset placement:
classic-theme-structure - Template file selection:
classic-template-hierarchy
References
- Official documentation: https://developer.wordpress.org/themes/advanced-topics/security/
- Official documentation: https://developer.wordpress.org/apis/security/
- Official documentation: https://developer.wordpress.org/apis/security/escaping/
- Official documentation: https://developer.wordpress.org/apis/security/sanitizing/
- Official documentation: https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/
- Verified source paths:
wp-includes/template-loader.phpwp-includes/template.phpwp-includes/general-template.phpwp-includes/functions.wp-scripts.phpwp-includes/formatting.phpwp-content/themes/storefront/404.phpwp-content/themes/generatepress/header.php