wordpress-security-validation by bobmatnyc/claude-mpm-skills
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill wordpress-security-validation版本: 1.0.0 目标: WordPress 6.7+ | PHP 8.3+ 技能等级: 中级到高级
在 WordPress 开发中,安全不是可选项——它是基础。本技能教授三层安全模型,通过适当的输入清理、业务逻辑验证和输出转义,防止 XSS、CSRF、SQL 注入和其他常见的 Web 漏洞。
黄金法则: "输入时清理,逻辑上验证,输出时转义。"
每年都有成千上万的 WordPress 网站因插件和主题中的安全漏洞而受到攻击。这些攻击大多利用了以下三种弱点之一:
本技能提供了完整的、可用于生产环境的模式,以防止所有这三种攻击向量。
WordPress 安全遵循深度防御策略,包含三个不同的层次:
用户输入 → [1. 清理] → [2. 验证] → 处理 → [3. 转义] → 输出
目的: 移除危险字符并规范化数据格式 时机: 接收到用户输入后立即进行 示例: sanitize_text_field($_POST['username'])
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
目的: 确保数据满足业务需求 时机: 清理之后,处理之前 示例: if (!is_email($email)) { /* 错误 */ }
目的: 通过编码特殊字符来防止 XSS 时机: 每次向浏览器输出数据时 示例: echo esc_html($user_input);
关键区别:
Nonces(一次性数字)是加密令牌,用于验证请求源自您的站点,而非恶意的外部来源。它们防止跨站请求伪造(CSRF) 攻击。
CSRF 攻击如何运作:
<!-- 攻击者的恶意站点:evil.com -->
<img src="https://yoursite.com/wp-admin/admin.php?action=delete_user&id=1">
<!-- 如果用户已登录 yoursite.com,此操作将被执行! -->
Nonces 如何防止 CSRF:
<!-- 带有 nonce 的合法请求 -->
<form action="admin.php?action=delete_user&id=1" method="POST">
<?php wp_nonce_field('delete_user_1', 'delete_nonce'); ?>
<button>删除用户</button>
</form>
<!-- 攻击者无法生成有效的 nonce(与用户会话绑定) -->
之前(易受攻击):
// 易受攻击的表单处理
if (isset($_POST['submit'])) {
$user_id = absint($_POST['user_id']);
delete_user($user_id); // ⚠️ CSRF 漏洞!
}
之后(安全):
// 在表单中生成 nonce
<form method="post" action="">
<?php wp_nonce_field('delete_user_action', 'delete_user_nonce'); ?>
<input type="hidden" name="user_id" value="42">
<button type="submit" name="submit">删除用户</button>
</form>
// 提交时验证 nonce
if (isset($_POST['submit'])) {
// 安全检查 #1:验证 nonce
if (!isset($_POST['delete_user_nonce']) ||
!wp_verify_nonce($_POST['delete_user_nonce'], 'delete_user_action')) {
wp_die('安全检查失败:无效的 nonce');
}
// 安全检查 #2:能力检查
if (!current_user_can('delete_users')) {
wp_die('您没有删除用户的权限');
}
// 现在可以安全处理
$user_id = absint($_POST['user_id']);
wp_delete_user($user_id);
}
关键函数:
wp_nonce_field($action, $name) - 生成隐藏的 nonce 字段wp_verify_nonce($nonce, $action) - 验证 nonce 有效性用例: 删除/移至回收站链接,管理操作
// 生成 nonce URL
$delete_url = wp_nonce_url(
admin_url('admin.php?action=delete_post&post_id=123'),
'delete_post_123', // 操作(必须唯一)
'delete_nonce' // 查询参数名称
);
echo '<a href="' . esc_url($delete_url) . '">删除文章</a>';
// 在处理器中验证 nonce
add_action('admin_action_delete_post', 'handle_delete_post');
function handle_delete_post() {
// 验证 URL 中的 nonce
if (!isset($_GET['delete_nonce']) ||
!wp_verify_nonce($_GET['delete_nonce'], 'delete_post_123')) {
wp_die('无效的安全令牌');
}
// 验证能力
$post_id = absint($_GET['post_id']);
if (!current_user_can('delete_post', $post_id)) {
wp_die('您不能删除此文章');
}
// 删除文章
wp_delete_post($post_id, true); // true = 强制删除
// 重定向并显示成功消息
wp_redirect(add_query_arg('message', 'deleted', wp_get_referer()));
exit;
}
用例: 前端 AJAX 请求
之前(易受攻击):
// ⚠️ 易受攻击的 AJAX 请求
jQuery.post(ajaxurl, {
action: 'update_user_meta',
user_id: 42,
meta_key: 'favorite_color',
meta_value: 'blue'
}, function(response) {
console.log(response);
});
之后(安全):
PHP(使用 nonce 入队脚本):
add_action('wp_enqueue_scripts', 'enqueue_ajax_script');
function enqueue_ajax_script() {
wp_enqueue_script('my-ajax-script',
plugin_dir_url(__FILE__) . 'js/ajax.js',
['jquery'],
'1.0.0',
true
);
// 将 nonce 和 AJAX URL 传递给 JavaScript
wp_localize_script('my-ajax-script', 'myAjax', [
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_ajax_nonce'), // 生成 nonce
]);
}
// 带有 nonce 验证的 AJAX 处理器
add_action('wp_ajax_update_user_meta', 'handle_ajax_update');
function handle_ajax_update() {
// 验证 nonce
check_ajax_referer('my_ajax_nonce', 'nonce');
// 验证能力
if (!current_user_can('edit_users')) {
wp_send_json_error(['message' => '权限被拒绝']);
}
// 清理输入
$user_id = absint($_POST['user_id']);
$meta_key = sanitize_key($_POST['meta_key']);
$meta_value = sanitize_text_field($_POST['meta_value']);
// 更新元数据
update_user_meta($user_id, $meta_key, $meta_value);
wp_send_json_success(['message' => '更新成功']);
}
JavaScript(在 AJAX 中使用 nonce):
jQuery(document).ready(function($) {
$('#update-button').on('click', function() {
$.post(myAjax.ajaxurl, {
action: 'update_user_meta',
nonce: myAjax.nonce, // 包含 nonce
user_id: 42,
meta_key: 'favorite_color',
meta_value: 'blue'
}, function(response) {
if (response.success) {
console.log(response.data.message);
} else {
console.error(response.data.message);
}
});
});
});
关键函数:
wp_create_nonce($action) - 生成 nonce 令牌check_ajax_referer($action, $query_arg) - 验证 AJAX nonce(失败时终止)wp_send_json_success($data) - 发送 JSON 成功响应wp_send_json_error($data) - 发送 JSON 错误响应✅ 应该做:
delete_post_$post_id,而不仅仅是 delete)check_ajax_referer)❌ 不应该做:
Nonce 生命周期: 默认情况下,WordPress nonces 在 24 小时后过期(由于时间窗口,每个方向 12 小时)。
清理通过移除或编码危险字符来转换用户输入为安全格式。这是对抗恶意数据的第一道防线。
| 函数 | 用例 | 示例输入 | 输出 |
|---|---|---|---|
sanitize_text_field() | 单行文本(用户名、标题) | "Hello <script>alert('xss')</script>" | "Hello alert('xss')" |
sanitize_email() | 电子邮件地址 | "user@example.com<script>" | "user@example.com" |
sanitize_url() / esc_url_raw() | URL(用于存储) | "javascript:alert('xss')" | ""(被阻止) |
sanitize_key() | 数组键、元键 | "my key!" | "my_key" |
sanitize_file_name() | 文件上传 | "../../etc/passwd" | "..etcpasswd" |
absint() | 正整数 | "-5", "42abc" | 5, 42 |
intval() | 任何整数 | "-5", "42.7" | -5, 42 |
floatval() | 浮点数 | "3.14abc" | 3.14 |
wp_kses_post() | HTML 内容(允许安全标签) | "<p>Safe</p><script>Bad</script>" | "<p>Safe</p>" |
wp_kses() | 带有自定义允许标签的 HTML | 见下文 | 自定义过滤 |
sanitize_textarea_field() | 多行文本 | "Line 1\nLine 2<script>" | "Line 1\nLine 2" |
sanitize_title() | 文章别名 | "Hello World!" | "hello-world" |
// 单行文本(移除 HTML、换行符、额外空白)
$username = sanitize_text_field($_POST['username']);
// 输入:" John <b>Doe</b>\n"
// 输出:"John Doe"
// 多行文本(保留换行符,移除 HTML)
$bio = sanitize_textarea_field($_POST['bio']);
// 输入:"Line 1\nLine 2<script>alert('xss')</script>"
// 输出:"Line 1\nLine 2alert('xss')"
// 电子邮件(验证格式并移除无效字符)
$email = sanitize_email($_POST['email']);
// 输入:"user@EXAMPLE.com <script>"
// 输出:"user@example.com"
// URL(移除危险协议)
$website = esc_url_raw($_POST['website']);
// 输入:"javascript:alert('xss')"
// 输出:""(被阻止的协议)
// 输入:"http://example.com"
// 输出:"http://example.com"
// 仅限正整数(绝对值)
$post_id = absint($_POST['post_id']);
// 输入:"-5", "42", "123abc"
// 输出:5, 42, 123
// 任何整数(保留负数)
$temperature = intval($_POST['temperature']);
// 输入:"-5", "42.7", "99abc"
// 输出:-5, 42, 99
// 浮点数
$price = floatval($_POST['price']);
// 输入:"19.99", "20.5abc"
// 输出:19.99, 20.5
wp_kses_post() - 允许 WordPress 文章编辑器标签:
$content = wp_kses_post($_POST['content']);
// 允许:<p>, <a>, <strong>, <em>, <ul>, <ol>, <li>, <blockquote>, <img>, 等。
// 阻止:<script>, <iframe>, <object>, <embed>, <form>
// 输入:"<p>Safe content</p><script>alert('xss')</script>"
// 输出:"<p>Safe content</p>alert('xss')"
wp_kses() - 自定义允许的标签:
// 定义允许的标签和属性
$allowed_html = [
'a' => [
'href' => true,
'title' => true,
'target' => true,
],
'strong' => [],
'em' => [],
'br' => [],
];
$clean_html = wp_kses($_POST['content'], $allowed_html);
// 输入:"<a href='#' onclick='alert(1)'>Link</a><script>Bad</script>"
// 输出:"<a href='#'>Link</a>Bad"(onclick 被移除,script 被剥离)
剥离所有 HTML:
$plain_text = wp_strip_all_tags($_POST['content']);
// 输入:"<p>Hello <b>World</b></p>"
// 输出:"Hello World"
// 清理文件名(移除路径遍历、特殊字符)
$safe_filename = sanitize_file_name($_FILES['upload']['name']);
// 输入:"../../etc/passwd", "my file!.php"
// 输出:"..etcpasswd", "my-file.php"
// 完整的文件上传示例
if (isset($_FILES['user_avatar'])) {
// 首先验证 nonce!
if (!wp_verify_nonce($_POST['upload_nonce'], 'upload_avatar')) {
wp_die('安全检查失败');
}
// 清理文件名
$filename = sanitize_file_name($_FILES['user_avatar']['name']);
// 验证文件类型
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
$file_type = $_FILES['user_avatar']['type'];
if (!in_array($file_type, $allowed_types)) {
wp_die('无效的文件类型。仅允许 JPG、PNG、GIF。');
}
// 使用 WordPress 上传处理器(处理安全性)
$upload = wp_handle_upload($_FILES['user_avatar'], [
'test_form' => false,
'mimes' => [
'jpg|jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
],
]);
if (isset($upload['error'])) {
wp_die('上传失败:' . $upload['error']);
}
// 存储上传的文件 URL
$avatar_url = $upload['url'];
update_user_meta(get_current_user_id(), 'avatar_url', $avatar_url);
}
// 清理文本字段数组
$tags = array_map('sanitize_text_field', $_POST['tags']);
// 输入:['tag1', '<script>tag2</script>', 'tag3']
// 输出:['tag1', 'tag2', 'tag3']
// 清理整数数组
$ids = array_map('absint', $_POST['post_ids']);
// 输入:['1', '2abc', '-5']
// 输出:[1, 2, 5]
// 清理电子邮件数组
$emails = array_map('sanitize_email', $_POST['email_list']);
// 使用清理回调注册设置
register_setting('my_plugin_options', 'my_plugin_settings', [
'type' => 'array',
'sanitize_callback' => 'my_plugin_sanitize_settings',
]);
function my_plugin_sanitize_settings($input) {
$sanitized = [];
// 清理 API 密钥(仅字母数字)
if (isset($input['api_key'])) {
$sanitized['api_key'] = preg_replace('/[^a-zA-Z0-9]/', '', $input['api_key']);
}
// 清理布尔复选框
$sanitized['enable_feature'] = isset($input['enable_feature']) ? 1 : 0;
// 清理颜色(十六进制格式)
if (isset($input['primary_color'])) {
$color = sanitize_hex_color($input['primary_color']);
$sanitized['primary_color'] = $color ? $color : '#000000';
}
// 清理选择选项(白名单)
$allowed_modes = ['mode1', 'mode2', 'mode3'];
if (isset($input['mode']) && in_array($input['mode'], $allowed_modes)) {
$sanitized['mode'] = $input['mode'];
} else {
$sanitized['mode'] = 'mode1'; // 默认值
}
return $sanitized;
}
验证确保数据在清理后满足业务逻辑需求。与清理(转换数据)不同,验证返回 true/false。
| 函数 | 目的 | 示例 |
|---|---|---|
is_email($email) | 有效的电子邮件格式 | is_email('user@example.com') → true |
is_numeric($value) | 数字字符串 | is_numeric('42') → true |
is_int($value) | 整数类型 | is_int(42) → true |
is_array($value) | 数组类型 | is_array([1,2,3]) → true |
is_user_logged_in() | 用户认证 | is_user_logged_in() → true/false |
username_exists($user) | 用户名存在 | username_exists('admin') → user_id 或 null |
email_exists($email) | 电子邮件存在 | email_exists('user@example.com') → user_id 或 false |
$email = sanitize_email($_POST['email']);
// 验证格式
if (!is_email($email)) {
$errors[] = '无效的电子邮件地址格式';
}
// 验证唯一性(用于注册)
if (email_exists($email)) {
$errors[] = '电子邮件地址已注册';
}
$age = absint($_POST['age']);
// 验证范围
if ($age < 18 || $age > 100) {
$errors[] = '年龄必须在 18 到 100 之间';
}
// 验证正数
if ($quantity <= 0) {
$errors[] = '数量必须大于零';
}
$username = sanitize_text_field($_POST['username']);
// 验证最小长度
if (strlen($username) < 3) {
$errors[] = '用户名必须至少 3 个字符';
}
// 验证最大长度
if (strlen($username) > 20) {
$errors[] = '用户名不能超过 20 个字符';
}
// 检查字段是否存在且不为空
if (empty($_POST['title']) || trim($_POST['title']) === '') {
$errors[] = '标题是必填项';
}
// 替代方案:isset() + 非空检查
if (!isset($_POST['terms']) || $_POST['terms'] !== 'accepted') {
$errors[] = '您必须接受条款和条件';
}
$phone = sanitize_text_field($_POST['phone']);
// 验证电话格式(美国格式:(555) 123-4567)
if (!preg_match('/^\(\d{3}\) \d{3}-\d{4}$/', $phone)) {
$errors[] = '电话必须为格式:(555) 123-4567';
}
// 验证仅字母数字
$product_code = sanitize_text_field($_POST['product_code']);
if (!preg_match('/^[a-zA-Z0-9]+$/', $product_code)) {
$errors[] = '产品代码只能包含字母和数字';
}
function validate_registration_form($data) {
$errors = [];
// 电子邮件验证
$email = sanitize_email($data['email']);
if (!is_email($email)) {
$errors['email'] = '无效的电子邮件地址';
} elseif (email_exists($email)) {
$errors['email'] = '电子邮件已注册';
}
// 用户名验证
$username = sanitize_text_field($data['username']);
if (strlen($username) < 3) {
$errors['username'] = '用户名太短(至少 3 个字符)';
} elseif (username_exists($username)) {
$errors['username'] = '用户名已被占用';
}
// 密码验证
if (strlen($data['password']) < 8) {
$errors['password'] = '密码必须至少 8 个字符';
}
// 密码确认
if ($data['password'] !== $data['password_confirm']) {
$errors['password_confirm'] = '密码不匹配';
}
// 年龄验证
$age = absint($data['age']);
if ($age < 18) {
$errors['age'] = '您必须年满 18 岁才能注册';
}
return empty($errors) ? true : $errors;
}
// 用法
$result = validate_registration_form($_POST);
if ($result === true) {
// 处理注册
} else {
// 显示错误
foreach ($result as $field => $error) {
echo "<p class='error'>$error</p>";
}
}
// 验证 URL 是否来自允许的域名
function validate_allowed_domain($url) {
$allowed_domains = ['example.com', 'wordpress.org'];
$host = parse_url($url, PHP_URL_HOST);
return in_array($host, $allowed_domains);
}
// 验证日期格式和范围
function validate_date($date_string) {
$date = DateTime::createFromFormat('Y-m-d', $date_string);
if (!$date) {
return false; // 无效格式
}
// 检查日期不在过去
$now = new DateTime();
if ($date < $now) {
return false;
}
return true;
}
// 验证信用卡(Luhn 算法)
function validate_credit_card($number) {
$number = preg_replace('/\D/', '', $number); // 移除非数字
if (strlen($number) < 13 || strlen($number) > 19) {
return false;
}
$sum = 0;
$double = false;
for ($i = strlen($number) - 1; $i >= 0; $i--) {
$digit = (int) $number[$i];
if ($double) {
$digit *= 2;
if ($digit > 9) {
$digit -= 9;
}
}
$sum += $digit;
$double = !$double;
}
return ($sum % 10) === 0;
}
转义通过在输出前编码特殊字符来防止XSS(跨站脚本攻击)。这是最后的安全层。
| 函数 | 上下文 | 转义 | 示例用法 |
|---|---|---|---|
esc_html() | HTML 内容 | < > & " ' | echo esc_html($user_input); |
esc_attr() | HTML 属性 | < > & " ' | <input value="<?php echo esc_attr($value); ?>"> |
esc_url() | HTML href/src | 危险协议 | <a href="<?php echo esc_url($link); ?>"> |
esc_js() | JavaScript 字符串 | ' " \ / | <script>var msg = '<?php echo esc_js($message); ?>';</script> |
esc_sql() | 已弃用(使用 $wpdb->prepare()) | SQL 特殊字符 | ❌ 不要使用 |
esc_textarea() | 文本区域内容 | < > & | <textarea><?php echo esc_textarea($content); ?></textarea> |
// 转义 HTML 内容(将特殊字符转换为实体)
$user_comment = "<script>alert('XSS')</script>Hello";
echo esc_html($user_comment);
// 输出:<script>alert('XSS')</script>Hello
// 浏览器显示:<script>alert('XSS')</script>Hello(作为文本,而非代码)
// 错误:无转义
echo $user_comment; // ⚠️ 执行 JavaScript!
// 转义属性值
$title = 'My "Awesome" Title';
?>
<input type="text"
value="<?php echo esc_attr($title); ?>"
placeholder="<?php echo esc_attr($placeholder); ?>">
<!-- 输出:value="My "Awesome" Title" -->
<!-- 错误:无转义 -->
<input value="<?php echo $title; ?>">
<!-- 输出:<input value="My "Awesome" Title">(破坏 HTML!) -->
// 转义 URL(阻止危险协议)
$user_url = "javascript:alert('XSS')";
echo '<a href="' . esc_url($user_url) . '">链接</a>';
// 输出:<a href="">链接</a>(javascript: 协议被阻止)
// 安全 URL
$safe_url = "https://example.com";
echo '<a href="' . esc_url($safe_url) . '">链接</a>';
// 输出:<a href="https://example.com">链接</a>
// 错误:无转义
echo '<a href="' . $user_url . '">链接</a>'; // ⚠️ XSS 漏洞!
// 转义 JavaScript 字符串
$user_message = "It's \"dangerous\" to trust user input";
?>
<script>
var message = '<?php echo esc_js($user_message); ?>';
alert(message);
</script>
<!-- 输出:var message = 'It\'s \"dangerous\" to trust user input'; -->
<!-- 错误:无转义 -->
<script>
var message = '<?php echo $user_message; ?>'; // ⚠️ 破坏 JavaScript!
</script>
// 转义文本区域内容
$bio = "Line 1\nLine 2 <script>alert('XSS')</script>";
?>
<textarea><?php echo esc_textarea($bio); ?></textarea>
<!-- 输出保留换行符,转义 HTML -->
<!-- 错误:在 textarea 中使用 esc_html() -->
<textarea><?php echo esc_html($bio); ?></textarea>
<!-- ⚠️ 换行符转换为 <br>(无法正确显示) -->
// 段落内容
echo '<p>' . esc_html($user_content) . '</p>';
// 链接文本
echo '<a href="' . esc_url($url) . '">' . esc_html($link_text) . '</a>';
// 图片替代文本
echo '<img src="' . esc_url($image_url) . '" alt="' . esc_attr($alt_text) . '">';
// 数据属性
echo '<div data-user-id="' . esc_attr($user_id) . '"
data-username="' . esc_attr($username) . '"></div>';
// 类名(使用 sanitize_html_class)
echo '<div class="' . esc_attr(sanitize_html_class($class)) . '"></div>';
// 样式属性(危险 - 尽可能避免)
$safe_color = sanitize_hex_color($user_color); // 首先验证
echo '<div style="color: ' . esc_attr($safe_color) . ';"></div>';
// 内联 JavaScript(尽可能避免,改用 wp_localize_script)
<script>
var config = {
username: '<?php echo esc_js($username); ?>',
apiUrl: '<?php echo esc_js(admin_url('admin-ajax.php')); ?>'
};
</script>
// 更好:使用 wp_localize_script
wp_localize_script('my-script', 'myConfig', [
'username' => $username, // 自动 JSON 编码
'apiUrl' => admin_url('admin-ajax.php'),
]);
// 翻译并转义
echo esc_html__('欢迎用户', 'my-plugin');
// 翻译带变量,然后转义
$message = sprintf(
__('你好 %s,您有 %d 条新消息', 'my-plugin'),
esc_html($username),
absint($message_count)
);
echo $message;
// 转义可翻译属性
<input placeholder="<?php echo esc_attr__('输入您的姓名', 'my-plugin'); ?>">
// 允许翻译中的 HTML(使用 wp_kses_post)
$welcome_html = __('欢迎使用 <strong>我的插件</strong>!', 'my-plugin');
echo wp_kses_post($welcome_html);
❌ 错误:
// 双重转义(向用户显示 HTML 实体)
echo esc_html(esc_html($content)); // ⚠️ 显示 &lt;script&gt;
// 上下文使用错误函数
echo '<a href="' . esc_html($url) . '">链接</a>'; // ⚠️ 使用 esc_url()
// JavaScript 中无转义
echo "<script>var x = '$user_input';</script>"; // ⚠️ 使用 esc_js()
// 存储前转义(存储原始数据,输出时转义)
update_option('setting', esc_html($value)); // ⚠️ 输出时转义,而非输入时
✅ 正确:
// 输出时转义一次
echo esc_html($content);
// 为上下文使用正确的函数
echo '<a href="' . esc_url($url) . '">' . esc_html($text) . '</a>';
// 正确转义 JavaScript
wp_localize_script('script', 'data', ['value' => $user_input]);
// 存储原始数据,输出时转义
update_option('setting', $value); // 存储原始数据
echo esc_html(get_option('setting')); // 输出时转义
能力检查确保用户有权限执行操作。始终与 nonce 验证结合使用。
| 能力 | 描述 | 默认角色 |
|---|---|---|
read | 查看内容 | 所有已登录用户 |
edit_posts | 创建/编辑自己的文章 | 作者、编辑、管理员 |
edit_published_posts | 编辑已发布的文章 | 编辑、管理员 |
delete_posts | 删除自己的文章 | 作者、编辑、管理员 |
manage_options | 管理站点设置 | 仅管理员 |
upload_files | 上传媒体 | 作者、编辑、管理员 |
edit_users | 编辑用户账户 | 仅管理员 |
delete_users | 删除用户 | 仅管理员 |
install_plugins | 安装/激活插件 | 仅管理员 |
switch_themes | 切换主题 | 仅管理员 |
// 检查用户是否已登录
if (!is_user_logged_in()) {
wp_die('您必须登录才能访问此页面');
}
// 检查用户是否具有能力
if (!current_user_can('manage_options')) {
wp_die('您没有管理设置的权限');
}
// 检查用户是否可以编辑特定文章
$post_id = absint($_GET['post_id']);
if (!current_user_can('edit_post', $post_id)) {
wp_die('您不能编辑此文章');
}
add_action('admin_post_update_settings', 'handle_settings_update');
function handle_settings_update() {
// 1. 检查用户是否已登录
if (!is_user_logged_in()) {
wp_die('您必须登录');
}
// 2. 验证 nonce
if (!isset($_POST['settings_nonce']) ||
!wp_verify_nonce($_POST['settings_nonce'], 'update_settings')) {
wp_die('安全检查失败');
}
// 3. 检查用户能力
if (!current_user_can('manage_options')) {
wp_die('您没有更新设置的权限');
}
// 4. 清理输入
$api_key = sanitize_text_field($_POST['api_key']);
$enable_feature = isset($_POST['enable_feature']) ? 1 : 0;
// 5. 验证
Version: 1.0.0 Target: WordPress 6.7+ | PHP 8.3+ Skill Level: Intermediate to Advanced
Security is not optional in WordPress development—it's fundamental. This skill teaches the three-layer security model that prevents XSS, CSRF, SQL injection, and other common web vulnerabilities through proper input sanitization, business logic validation, and output escaping.
The Golden Rule: "Sanitize on input, validate for logic, escape on output."
Every year, thousands of WordPress sites are compromised due to security vulnerabilities in plugins and themes. Most of these attacks exploit one of three weaknesses:
This skill provides complete, production-ready patterns for preventing all three attack vectors.
WordPress security follows a defense-in-depth strategy with three distinct layers:
User Input → [1. SANITIZE] → [2. VALIDATE] → Process → [3. ESCAPE] → Output
Purpose: Remove dangerous characters and normalize data format When: Immediately upon receiving user input Example: sanitize_text_field($_POST['username'])
Purpose: Ensure data meets business requirements When: After sanitization, before processing Example: if (!is_email($email)) { /* error */ }
Purpose: Prevent XSS by encoding special characters When: Every time you output data to browser Example: echo esc_html($user_input);
Critical Distinction:
Nonces (Numbers Used Once) are cryptographic tokens that verify a request originated from your site, not a malicious external source. They prevent Cross-Site Request Forgery (CSRF) attacks.
How CSRF Attacks Work:
<!-- Attacker's malicious site: evil.com -->
<img src="https://yoursite.com/wp-admin/admin.php?action=delete_user&id=1">
<!-- If user is logged into yoursite.com, this executes! -->
How Nonces Prevent CSRF:
<!-- Legitimate request with nonce -->
<form action="admin.php?action=delete_user&id=1" method="POST">
<?php wp_nonce_field('delete_user_1', 'delete_nonce'); ?>
<button>Delete User</button>
</form>
<!-- Attacker cannot generate valid nonce (tied to user session) -->
BEFORE (Vulnerable):
// Vulnerable form processing
if (isset($_POST['submit'])) {
$user_id = absint($_POST['user_id']);
delete_user($user_id); // ⚠️ CSRF vulnerable!
}
AFTER (Secure):
// Generate nonce in form
<form method="post" action="">
<?php wp_nonce_field('delete_user_action', 'delete_user_nonce'); ?>
<input type="hidden" name="user_id" value="42">
<button type="submit" name="submit">Delete User</button>
</form>
// Verify nonce on submission
if (isset($_POST['submit'])) {
// Security check #1: Verify nonce
if (!isset($_POST['delete_user_nonce']) ||
!wp_verify_nonce($_POST['delete_user_nonce'], 'delete_user_action')) {
wp_die('Security check failed: Invalid nonce');
}
// Security check #2: Capability check
if (!current_user_can('delete_users')) {
wp_die('You do not have permission to delete users');
}
// Now safe to process
$user_id = absint($_POST['user_id']);
wp_delete_user($user_id);
}
Key Functions:
wp_nonce_field($action, $name) - Generates hidden nonce fieldwp_verify_nonce($nonce, $action) - Verifies nonce validityUse Case: Delete/trash links, admin actions
// Generate nonce URL
$delete_url = wp_nonce_url(
admin_url('admin.php?action=delete_post&post_id=123'),
'delete_post_123', // Action (must be unique)
'delete_nonce' // Query parameter name
);
echo '<a href="' . esc_url($delete_url) . '">Delete Post</a>';
// Verify nonce in handler
add_action('admin_action_delete_post', 'handle_delete_post');
function handle_delete_post() {
// Verify nonce from URL
if (!isset($_GET['delete_nonce']) ||
!wp_verify_nonce($_GET['delete_nonce'], 'delete_post_123')) {
wp_die('Invalid security token');
}
// Verify capability
$post_id = absint($_GET['post_id']);
if (!current_user_can('delete_post', $post_id)) {
wp_die('You cannot delete this post');
}
// Delete post
wp_delete_post($post_id, true); // true = force delete
// Redirect with success message
wp_redirect(add_query_arg('message', 'deleted', wp_get_referer()));
exit;
}
Use Case: Frontend AJAX requests
BEFORE (Vulnerable):
// ⚠️ Vulnerable AJAX request
jQuery.post(ajaxurl, {
action: 'update_user_meta',
user_id: 42,
meta_key: 'favorite_color',
meta_value: 'blue'
}, function(response) {
console.log(response);
});
AFTER (Secure):
PHP (Enqueue script with nonce):
add_action('wp_enqueue_scripts', 'enqueue_ajax_script');
function enqueue_ajax_script() {
wp_enqueue_script('my-ajax-script',
plugin_dir_url(__FILE__) . 'js/ajax.js',
['jquery'],
'1.0.0',
true
);
// Pass nonce and AJAX URL to JavaScript
wp_localize_script('my-ajax-script', 'myAjax', [
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_ajax_nonce'), // Generate nonce
]);
}
// AJAX handler with nonce verification
add_action('wp_ajax_update_user_meta', 'handle_ajax_update');
function handle_ajax_update() {
// Verify nonce
check_ajax_referer('my_ajax_nonce', 'nonce');
// Verify capability
if (!current_user_can('edit_users')) {
wp_send_json_error(['message' => 'Permission denied']);
}
// Sanitize input
$user_id = absint($_POST['user_id']);
$meta_key = sanitize_key($_POST['meta_key']);
$meta_value = sanitize_text_field($_POST['meta_value']);
// Update meta
update_user_meta($user_id, $meta_key, $meta_value);
wp_send_json_success(['message' => 'Updated successfully']);
}
JavaScript (Use nonce in AJAX):
jQuery(document).ready(function($) {
$('#update-button').on('click', function() {
$.post(myAjax.ajaxurl, {
action: 'update_user_meta',
nonce: myAjax.nonce, // Include nonce
user_id: 42,
meta_key: 'favorite_color',
meta_value: 'blue'
}, function(response) {
if (response.success) {
console.log(response.data.message);
} else {
console.error(response.data.message);
}
});
});
});
Key Functions:
wp_create_nonce($action) - Generate nonce tokencheck_ajax_referer($action, $query_arg) - Verify AJAX nonce (dies on failure)wp_send_json_success($data) - Send JSON success responsewp_send_json_error($data) - Send JSON error response✅ DO:
delete_post_$post_id, not just delete)check_ajax_referer for AJAX)❌ DON'T:
Nonce Lifespan: WordPress nonces expire after 24 hours by default (12 hours in each direction due to time window).
Sanitization transforms user input into a safe format by removing or encoding dangerous characters. It's the first line of defense against malicious data.
| Function | Use Case | Example Input | Output |
|---|---|---|---|
sanitize_text_field() | Single-line text (usernames, titles) | "Hello <script>alert('xss')</script>" | "Hello alert('xss')" |
sanitize_email() | Email addresses | "user@example.com<script>" | "user@example.com" |
| / |
// Single-line text (removes HTML, line breaks, extra whitespace)
$username = sanitize_text_field($_POST['username']);
// Input: " John <b>Doe</b>\n"
// Output: "John Doe"
// Multi-line text (preserves line breaks, removes HTML)
$bio = sanitize_textarea_field($_POST['bio']);
// Input: "Line 1\nLine 2<script>alert('xss')</script>"
// Output: "Line 1\nLine 2alert('xss')"
// Email (validates format and removes invalid characters)
$email = sanitize_email($_POST['email']);
// Input: "user@EXAMPLE.com <script>"
// Output: "user@example.com"
// URL (removes dangerous protocols)
$website = esc_url_raw($_POST['website']);
// Input: "javascript:alert('xss')"
// Output: "" (blocked protocol)
// Input: "http://example.com"
// Output: "http://example.com"
// Positive integers only (absolute value)
$post_id = absint($_POST['post_id']);
// Input: "-5", "42", "123abc"
// Output: 5, 42, 123
// Any integer (preserves negative)
$temperature = intval($_POST['temperature']);
// Input: "-5", "42.7", "99abc"
// Output: -5, 42, 99
// Floating-point numbers
$price = floatval($_POST['price']);
// Input: "19.99", "20.5abc"
// Output: 19.99, 20.5
wp_kses_post() - Allow WordPress Post Editor Tags:
$content = wp_kses_post($_POST['content']);
// Allows: <p>, <a>, <strong>, <em>, <ul>, <ol>, <li>, <blockquote>, <img>, etc.
// Blocks: <script>, <iframe>, <object>, <embed>, <form>
// Input: "<p>Safe content</p><script>alert('xss')</script>"
// Output: "<p>Safe content</p>alert('xss')"
wp_kses() - Custom Allowed Tags:
// Define allowed tags and attributes
$allowed_html = [
'a' => [
'href' => true,
'title' => true,
'target' => true,
],
'strong' => [],
'em' => [],
'br' => [],
];
$clean_html = wp_kses($_POST['content'], $allowed_html);
// Input: "<a href='#' onclick='alert(1)'>Link</a><script>Bad</script>"
// Output: "<a href='#'>Link</a>Bad" (onclick removed, script stripped)
Strip All HTML:
$plain_text = wp_strip_all_tags($_POST['content']);
// Input: "<p>Hello <b>World</b></p>"
// Output: "Hello World"
// Sanitize filename (removes path traversal, special characters)
$safe_filename = sanitize_file_name($_FILES['upload']['name']);
// Input: "../../etc/passwd", "my file!.php"
// Output: "..etcpasswd", "my-file.php"
// Complete file upload example
if (isset($_FILES['user_avatar'])) {
// Verify nonce first!
if (!wp_verify_nonce($_POST['upload_nonce'], 'upload_avatar')) {
wp_die('Security check failed');
}
// Sanitize filename
$filename = sanitize_file_name($_FILES['user_avatar']['name']);
// Validate file type
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
$file_type = $_FILES['user_avatar']['type'];
if (!in_array($file_type, $allowed_types)) {
wp_die('Invalid file type. Only JPG, PNG, GIF allowed.');
}
// Use WordPress upload handler (handles security)
$upload = wp_handle_upload($_FILES['user_avatar'], [
'test_form' => false,
'mimes' => [
'jpg|jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
],
]);
if (isset($upload['error'])) {
wp_die('Upload failed: ' . $upload['error']);
}
// Store uploaded file URL
$avatar_url = $upload['url'];
update_user_meta(get_current_user_id(), 'avatar_url', $avatar_url);
}
// Sanitize array of text fields
$tags = array_map('sanitize_text_field', $_POST['tags']);
// Input: ['tag1', '<script>tag2</script>', 'tag3']
// Output: ['tag1', 'tag2', 'tag3']
// Sanitize array of integers
$ids = array_map('absint', $_POST['post_ids']);
// Input: ['1', '2abc', '-5']
// Output: [1, 2, 5]
// Sanitize array of emails
$emails = array_map('sanitize_email', $_POST['email_list']);
// Register setting with sanitization callback
register_setting('my_plugin_options', 'my_plugin_settings', [
'type' => 'array',
'sanitize_callback' => 'my_plugin_sanitize_settings',
]);
function my_plugin_sanitize_settings($input) {
$sanitized = [];
// Sanitize API key (alphanumeric only)
if (isset($input['api_key'])) {
$sanitized['api_key'] = preg_replace('/[^a-zA-Z0-9]/', '', $input['api_key']);
}
// Sanitize boolean checkbox
$sanitized['enable_feature'] = isset($input['enable_feature']) ? 1 : 0;
// Sanitize color (hex format)
if (isset($input['primary_color'])) {
$color = sanitize_hex_color($input['primary_color']);
$sanitized['primary_color'] = $color ? $color : '#000000';
}
// Sanitize select option (whitelist)
$allowed_modes = ['mode1', 'mode2', 'mode3'];
if (isset($input['mode']) && in_array($input['mode'], $allowed_modes)) {
$sanitized['mode'] = $input['mode'];
} else {
$sanitized['mode'] = 'mode1'; // Default
}
return $sanitized;
}
Validation ensures data meets business logic requirements after sanitization. Unlike sanitization (which transforms data), validation returns true/false.
| Function | Purpose | Example |
|---|---|---|
is_email($email) | Valid email format | is_email('user@example.com') → true |
is_numeric($value) | Numeric string | is_numeric('42') → true |
is_int($value) | Integer type |
$email = sanitize_email($_POST['email']);
// Validate format
if (!is_email($email)) {
$errors[] = 'Invalid email address format';
}
// Validate uniqueness (for registration)
if (email_exists($email)) {
$errors[] = 'Email address already registered';
}
$age = absint($_POST['age']);
// Validate range
if ($age < 18 || $age > 100) {
$errors[] = 'Age must be between 18 and 100';
}
// Validate positive number
if ($quantity <= 0) {
$errors[] = 'Quantity must be greater than zero';
}
$username = sanitize_text_field($_POST['username']);
// Validate minimum length
if (strlen($username) < 3) {
$errors[] = 'Username must be at least 3 characters';
}
// Validate maximum length
if (strlen($username) > 20) {
$errors[] = 'Username cannot exceed 20 characters';
}
// Check if field exists and is not empty
if (empty($_POST['title']) || trim($_POST['title']) === '') {
$errors[] = 'Title is required';
}
// Alternative: isset() + non-empty check
if (!isset($_POST['terms']) || $_POST['terms'] !== 'accepted') {
$errors[] = 'You must accept the terms and conditions';
}
$phone = sanitize_text_field($_POST['phone']);
// Validate phone format (US format: (555) 123-4567)
if (!preg_match('/^\(\d{3}\) \d{3}-\d{4}$/', $phone)) {
$errors[] = 'Phone must be in format: (555) 123-4567';
}
// Validate alphanumeric only
$product_code = sanitize_text_field($_POST['product_code']);
if (!preg_match('/^[a-zA-Z0-9]+$/', $product_code)) {
$errors[] = 'Product code must contain only letters and numbers';
}
function validate_registration_form($data) {
$errors = [];
// Email validation
$email = sanitize_email($data['email']);
if (!is_email($email)) {
$errors['email'] = 'Invalid email address';
} elseif (email_exists($email)) {
$errors['email'] = 'Email already registered';
}
// Username validation
$username = sanitize_text_field($data['username']);
if (strlen($username) < 3) {
$errors['username'] = 'Username too short (minimum 3 characters)';
} elseif (username_exists($username)) {
$errors['username'] = 'Username already taken';
}
// Password validation
if (strlen($data['password']) < 8) {
$errors['password'] = 'Password must be at least 8 characters';
}
// Password confirmation
if ($data['password'] !== $data['password_confirm']) {
$errors['password_confirm'] = 'Passwords do not match';
}
// Age validation
$age = absint($data['age']);
if ($age < 18) {
$errors['age'] = 'You must be 18 or older to register';
}
return empty($errors) ? true : $errors;
}
// Usage
$result = validate_registration_form($_POST);
if ($result === true) {
// Process registration
} else {
// Display errors
foreach ($result as $field => $error) {
echo "<p class='error'>$error</p>";
}
}
// Validate URL is from allowed domain
function validate_allowed_domain($url) {
$allowed_domains = ['example.com', 'wordpress.org'];
$host = parse_url($url, PHP_URL_HOST);
return in_array($host, $allowed_domains);
}
// Validate date format and range
function validate_date($date_string) {
$date = DateTime::createFromFormat('Y-m-d', $date_string);
if (!$date) {
return false; // Invalid format
}
// Check date is not in the past
$now = new DateTime();
if ($date < $now) {
return false;
}
return true;
}
// Validate credit card (Luhn algorithm)
function validate_credit_card($number) {
$number = preg_replace('/\D/', '', $number); // Remove non-digits
if (strlen($number) < 13 || strlen($number) > 19) {
return false;
}
$sum = 0;
$double = false;
for ($i = strlen($number) - 1; $i >= 0; $i--) {
$digit = (int) $number[$i];
if ($double) {
$digit *= 2;
if ($digit > 9) {
$digit -= 9;
}
}
$sum += $digit;
$double = !$double;
}
return ($sum % 10) === 0;
}
Escaping prevents XSS (Cross-Site Scripting) by encoding special characters before output. This is the final security layer.
| Function | Context | Escapes | Example Use |
|---|---|---|---|
esc_html() | HTML content | < > & " ' | echo esc_html($user_input); |
esc_attr() | HTML attributes | < > & " ' | <input value="<?php echo esc_attr($value); ?>"> |
esc_url() |
// Escape HTML content (converts special characters to entities)
$user_comment = "<script>alert('XSS')</script>Hello";
echo esc_html($user_comment);
// Output: <script>alert('XSS')</script>Hello
// Browser displays: <script>alert('XSS')</script>Hello (as text, not code)
// WRONG: No escaping
echo $user_comment; // ⚠️ Executes JavaScript!
// Escape attribute values
$title = 'My "Awesome" Title';
?>
<input type="text"
value="<?php echo esc_attr($title); ?>"
placeholder="<?php echo esc_attr($placeholder); ?>">
<!-- Output: value="My "Awesome" Title" -->
<!-- WRONG: No escaping -->
<input value="<?php echo $title; ?>">
<!-- Output: <input value="My "Awesome" Title"> (breaks HTML!) -->
// Escape URLs (blocks dangerous protocols)
$user_url = "javascript:alert('XSS')";
echo '<a href="' . esc_url($user_url) . '">Link</a>';
// Output: <a href="">Link</a> (javascript: protocol blocked)
// Safe URL
$safe_url = "https://example.com";
echo '<a href="' . esc_url($safe_url) . '">Link</a>';
// Output: <a href="https://example.com">Link</a>
// WRONG: No escaping
echo '<a href="' . $user_url . '">Link</a>'; // ⚠️ XSS vulnerability!
// Escape JavaScript strings
$user_message = "It's \"dangerous\" to trust user input";
?>
<script>
var message = '<?php echo esc_js($user_message); ?>';
alert(message);
</script>
<!-- Output: var message = 'It\'s \"dangerous\" to trust user input'; -->
<!-- WRONG: No escaping -->
<script>
var message = '<?php echo $user_message; ?>'; // ⚠️ Breaks JavaScript!
</script>
// Escape textarea content
$bio = "Line 1\nLine 2 <script>alert('XSS')</script>";
?>
<textarea><?php echo esc_textarea($bio); ?></textarea>
<!-- Output preserves line breaks, escapes HTML -->
<!-- WRONG: Using esc_html() in textarea -->
<textarea><?php echo esc_html($bio); ?></textarea>
<!-- ⚠️ Line breaks converted to <br> (not displayed correctly) -->
// Paragraph content
echo '<p>' . esc_html($user_content) . '</p>';
// Link text
echo '<a href="' . esc_url($url) . '">' . esc_html($link_text) . '</a>';
// Image alt text
echo '<img src="' . esc_url($image_url) . '" alt="' . esc_attr($alt_text) . '">';
// Data attributes
echo '<div data-user-id="' . esc_attr($user_id) . '"
data-username="' . esc_attr($username) . '"></div>';
// Class names (use sanitize_html_class)
echo '<div class="' . esc_attr(sanitize_html_class($class)) . '"></div>';
// Style attribute (dangerous - avoid if possible)
$safe_color = sanitize_hex_color($user_color); // Validate first
echo '<div style="color: ' . esc_attr($safe_color) . ';"></div>';
// Inline JavaScript (avoid if possible, use wp_localize_script instead)
<script>
var config = {
username: '<?php echo esc_js($username); ?>',
apiUrl: '<?php echo esc_js(admin_url('admin-ajax.php')); ?>'
};
</script>
// BETTER: Use wp_localize_script
wp_localize_script('my-script', 'myConfig', [
'username' => $username, // Automatically JSON-encoded
'apiUrl' => admin_url('admin-ajax.php'),
]);
// Translate and escape
echo esc_html__('Welcome User', 'my-plugin');
// Translate with variable, then escape
$message = sprintf(
__('Hello %s, you have %d new messages', 'my-plugin'),
esc_html($username),
absint($message_count)
);
echo $message;
// Escape translatable attributes
<input placeholder="<?php echo esc_attr__('Enter your name', 'my-plugin'); ?>">
// Allow HTML in translations (use wp_kses_post)
$welcome_html = __('Welcome to <strong>My Plugin</strong>!', 'my-plugin');
echo wp_kses_post($welcome_html);
❌ WRONG:
// Double-escaping (displays HTML entities to user)
echo esc_html(esc_html($content)); // ⚠️ Displays &lt;script&gt;
// Wrong function for context
echo '<a href="' . esc_html($url) . '">Link</a>'; // ⚠️ Use esc_url()
// No escaping in JavaScript
echo "<script>var x = '$user_input';</script>"; // ⚠️ Use esc_js()
// Escaping before storage (store raw, escape on output)
update_option('setting', esc_html($value)); // ⚠️ Escape on output, not input
✅ CORRECT:
// Escape once, on output
echo esc_html($content);
// Use correct function for context
echo '<a href="' . esc_url($url) . '">' . esc_html($text) . '</a>';
// Escape JavaScript properly
wp_localize_script('script', 'data', ['value' => $user_input]);
// Store raw, escape on output
update_option('setting', $value); // Store raw
echo esc_html(get_option('setting')); // Escape on output
Capability checks ensure users have permission to perform actions. Always combine with nonce verification.
| Capability | Description | Default Roles |
|---|---|---|
read | View content | All logged-in users |
edit_posts | Create/edit own posts | Author, Editor, Admin |
edit_published_posts | Edit published posts | Editor, Admin |
delete_posts | Delete own posts | Author, Editor, Admin |
manage_options | Manage site settings |
// Check if user is logged in
if (!is_user_logged_in()) {
wp_die('You must be logged in to access this page');
}
// Check if user has capability
if (!current_user_can('manage_options')) {
wp_die('You do not have permission to manage settings');
}
// Check if user can edit specific post
$post_id = absint($_GET['post_id']);
if (!current_user_can('edit_post', $post_id)) {
wp_die('You cannot edit this post');
}
add_action('admin_post_update_settings', 'handle_settings_update');
function handle_settings_update() {
// 1. Check if user is logged in
if (!is_user_logged_in()) {
wp_die('You must be logged in');
}
// 2. Verify nonce
if (!isset($_POST['settings_nonce']) ||
!wp_verify_nonce($_POST['settings_nonce'], 'update_settings')) {
wp_die('Security check failed');
}
// 3. Check user capability
if (!current_user_can('manage_options')) {
wp_die('You do not have permission to update settings');
}
// 4. Sanitize input
$api_key = sanitize_text_field($_POST['api_key']);
$enable_feature = isset($_POST['enable_feature']) ? 1 : 0;
// 5. Validate data
if (strlen($api_key) < 10) {
wp_die('API key must be at least 10 characters');
}
// 6. Update options
update_option('my_plugin_api_key', $api_key);
update_option('my_plugin_enable_feature', $enable_feature);
// 7. Redirect with success message
wp_redirect(add_query_arg('message', 'updated', wp_get_referer()));
exit;
}
// Check if user can edit specific post
$post_id = absint($_POST['post_id']);
if (!current_user_can('edit_post', $post_id)) {
wp_send_json_error(['message' => 'You cannot edit this post']);
}
// Check if user can delete specific post
if (!current_user_can('delete_post', $post_id)) {
wp_send_json_error(['message' => 'You cannot delete this post']);
}
// Check if user can publish posts
if (!current_user_can('publish_posts')) {
wp_send_json_error(['message' => 'You cannot publish posts']);
}
// Register custom role with custom capability
add_action('init', 'register_custom_role');
function register_custom_role() {
add_role('store_manager', 'Store Manager', [
'read' => true,
'edit_posts' => true,
'manage_products' => true, // Custom capability
]);
}
// Add custom capability to existing role
$role = get_role('editor');
$role->add_cap('manage_products');
// Check custom capability
if (current_user_can('manage_products')) {
// Allow product management
}
CRITICAL: Never trust user input in SQL queries. Always use $wpdb->prepare().
BEFORE (Vulnerable):
global $wpdb;
// ⚠️ CRITICAL VULNERABILITY - SQL INJECTION!
$user_id = $_GET['user_id'];
$results = $wpdb->get_results(
"SELECT * FROM {$wpdb->posts} WHERE post_author = $user_id"
);
// Attacker can inject SQL:
// ?user_id=1 OR 1=1 -- (returns all posts)
// ?user_id=1; DROP TABLE wp_posts; -- (deletes table!)
AFTER (Secure):
global $wpdb;
// ✅ SECURE - Using prepared statements
$user_id = absint($_GET['user_id']); // Sanitize first
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_author = %d",
$user_id
)
);
| Placeholder | Type | Example |
|---|---|---|
%s | String | "SELECT * FROM table WHERE name = %s" |
%d | Integer | "SELECT * FROM table WHERE id = %d" |
%f | Float | "SELECT * FROM table WHERE price = %f" |
global $wpdb;
$email = sanitize_email($_POST['email']);
// Prepared statement (prevents SQL injection)
$user = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$wpdb->users} WHERE user_email = %s",
$email
)
);
if ($user) {
echo "User found: " . esc_html($user->user_login);
}
global $wpdb;
// Use wpdb->insert() (automatically prepares)
$result = $wpdb->insert(
$wpdb->prefix . 'my_table',
[
'title' => sanitize_text_field($_POST['title']),
'content' => wp_kses_post($_POST['content']),
'user_id' => absint($_POST['user_id']),
'price' => floatval($_POST['price']),
'created_at' => current_time('mysql'),
],
['%s', '%s', '%d', '%f', '%s'] // Format specifiers
);
if ($result === false) {
wp_die('Database insert failed: ' . $wpdb->last_error);
}
$inserted_id = $wpdb->insert_id;
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'my_table',
[
'title' => sanitize_text_field($_POST['title']), // New values
'updated_at' => current_time('mysql'),
],
['id' => absint($_POST['id'])], // WHERE condition
['%s', '%s'], // Format for new values
['%d'] // Format for WHERE condition
);
global $wpdb;
$wpdb->delete(
$wpdb->prefix . 'my_table',
['id' => absint($_POST['id'])],
['%d']
);
global $wpdb;
$status = sanitize_text_field($_POST['status']);
$min_price = floatval($_POST['min_price']);
// Multiple placeholders
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}products
WHERE status = %s AND price >= %f
ORDER BY created_at DESC
LIMIT %d",
$status,
$min_price,
10 // LIMIT value
)
);
❌ WRONG:
// String concatenation (vulnerable!)
$sql = "SELECT * FROM table WHERE name = '" . $_POST['name'] . "'";
// Using esc_sql() (deprecated and insufficient)
$sql = "SELECT * FROM table WHERE name = '" . esc_sql($_POST['name']) . "'";
// Not using placeholders
$wpdb->query("DELETE FROM table WHERE id = $id"); // ⚠️ Vulnerable
✅ CORRECT:
// Always use $wpdb->prepare()
$wpdb->get_results($wpdb->prepare(
"SELECT * FROM table WHERE name = %s",
$_POST['name']
));
// Use wpdb methods (insert, update, delete)
$wpdb->insert('table', ['name' => $_POST['name']], ['%s']);
Attack Scenario:
// Vulnerable code
echo "Welcome, " . $_GET['username'];
// Attacker visits: ?username=<script>alert(document.cookie)</script>
// Browser executes JavaScript, stealing session cookies
Prevention:
// Escape output
echo "Welcome, " . esc_html($_GET['username']);
// Output: Welcome, <script>alert(document.cookie)</script>
Attack Scenario:
<!-- Attacker's site (evil.com) -->
<img src="https://yoursite.com/wp-admin/admin.php?action=delete_all_posts">
<!-- If admin is logged in, this executes without their knowledge! -->
Prevention:
// Require nonce verification
if (!wp_verify_nonce($_GET['nonce'], 'delete_all_posts')) {
wp_die('Invalid security token');
}
Attack Scenario:
// Vulnerable code
$wpdb->query("DELETE FROM posts WHERE id = " . $_GET['id']);
// Attacker visits: ?id=1 OR 1=1
// Deletes ALL posts!
Prevention:
// Use prepared statements
$wpdb->query($wpdb->prepare(
"DELETE FROM posts WHERE id = %d",
absint($_GET['id'])
));
Attack Scenario:
// Vulnerable code
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $_FILES['file']['name']);
// Attacker uploads: malicious.php
// Executes: https://yoursite.com/uploads/malicious.php
Prevention:
// Validate file type and use wp_handle_upload()
$allowed_types = ['image/jpeg', 'image/png'];
if (!in_array($_FILES['file']['type'], $allowed_types)) {
wp_die('Invalid file type');
}
$upload = wp_handle_upload($_FILES['file'], ['test_form' => false]);
Attack Scenario:
// Vulnerable code
include($_GET['template'] . '.php');
// Attacker visits: ?template=../../../../etc/passwd
Prevention:
// Whitelist allowed templates
$allowed_templates = ['template1', 'template2'];
$template = sanitize_file_name($_GET['template']);
if (in_array($template, $allowed_templates)) {
include($template . '.php');
}
<?php
/**
* Secure Form Handling Example
* Demonstrates all security layers: nonces, sanitization, validation, escaping
*/
// 1. Display Form (with nonce)
function display_user_profile_form() {
$user_id = get_current_user_id();
$user_data = get_user_meta($user_id, 'profile_data', true);
?>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<input type="hidden" name="action" value="update_user_profile">
<?php wp_nonce_field('update_profile_' . $user_id, 'profile_nonce'); ?>
<label>
Display Name:
<input type="text"
name="display_name"
value="<?php echo esc_attr($user_data['display_name'] ?? ''); ?>"
required>
</label>
<label>
Email:
<input type="email"
name="email"
value="<?php echo esc_attr($user_data['email'] ?? ''); ?>"
required>
</label>
<label>
Bio:
<textarea name="bio"><?php echo esc_textarea($user_data['bio'] ?? ''); ?></textarea>
</label>
<label>
Website:
<input type="url"
name="website"
value="<?php echo esc_attr($user_data['website'] ?? ''); ?>">
</label>
<button type="submit">Update Profile</button>
</form>
<?php
}
// 2. Process Form (with full security)
add_action('admin_post_update_user_profile', 'handle_profile_update');
function handle_profile_update() {
// SECURITY LAYER 1: Authentication
if (!is_user_logged_in()) {
wp_die('You must be logged in to update your profile');
}
$user_id = get_current_user_id();
// SECURITY LAYER 2: Nonce Verification
if (!isset($_POST['profile_nonce']) ||
!wp_verify_nonce($_POST['profile_nonce'], 'update_profile_' . $user_id)) {
wp_die('Security check failed: Invalid nonce');
}
// SECURITY LAYER 3: Capability Check
if (!current_user_can('edit_user', $user_id)) {
wp_die('You do not have permission to update this profile');
}
// SECURITY LAYER 4: Sanitization
$display_name = sanitize_text_field($_POST['display_name']);
$email = sanitize_email($_POST['email']);
$bio = sanitize_textarea_field($_POST['bio']);
$website = esc_url_raw($_POST['website']);
// SECURITY LAYER 5: Validation
$errors = [];
if (empty($display_name) || strlen($display_name) < 3) {
$errors[] = 'Display name must be at least 3 characters';
}
if (!is_email($email)) {
$errors[] = 'Invalid email address';
}
if (!empty($website) && !filter_var($website, FILTER_VALIDATE_URL)) {
$errors[] = 'Invalid website URL';
}
if (!empty($errors)) {
wp_die(implode('<br>', array_map('esc_html', $errors)));
}
// SECURITY LAYER 6: Process Data
$profile_data = [
'display_name' => $display_name,
'email' => $email,
'bio' => $bio,
'website' => $website,
];
update_user_meta($user_id, 'profile_data', $profile_data);
// SECURITY LAYER 7: Safe Redirect
wp_redirect(add_query_arg('message', 'profile_updated', wp_get_referer()));
exit;
}
// 3. Display Success Message (with escaping)
add_action('admin_notices', 'show_profile_update_notice');
function show_profile_update_notice() {
if (isset($_GET['message']) && $_GET['message'] === 'profile_updated') {
echo '<div class="notice notice-success is-dismissible">';
echo '<p>' . esc_html__('Profile updated successfully!', 'my-plugin') . '</p>';
echo '</div>';
}
}
Use this checklist for every WordPress feature you implement:
wp_verify_nonce())current_user_can())wp_handle_upload()esc_html(), esc_attr(), etc.esc_url()wp_localize_script() or esc_js()echo $_POST or echo $_GET$wpdb->prepare()$wpdb->insert(), $wpdb->update(), $wpdb->delete()$wpdb->prefixis_user_logged_in())current_user_can())eval(), assert(), or create_function()extract() on user inputWP_DEBUG = false)1. Test Nonce Expiration:
# Generate form with nonce, wait 25 hours, submit
# Expected: "Security check failed" error
2. Test CSRF Protection:
<!-- Create external form pointing to your site -->
<form action="https://yoursite.com/wp-admin/admin-post.php" method="POST">
<input name="action" value="your_action">
<button>Submit</button>
</form>
<!-- Expected: Nonce verification fails -->
3. Test XSS Prevention:
Input: <script>alert('XSS')</script>
Expected Output: <script>alert('XSS')</script> (as text)
4. Test SQL Injection:
Input: 1 OR 1=1
Expected: Treats as literal string, no SQL execution
5. Test Capability Bypass:
// Log in as subscriber (low-privilege user)
// Try to access admin-only features
// Expected: "You do not have permission" error
Install Security Scanner:
# WPScan (CLI tool)
gem install wpscan
wpscan --url https://yoursite.com --enumerate vp
# Sucuri Security Plugin
wp plugin install sucuri-scanner --activate
Run PHP Code Sniffer:
# Check for security issues
vendor/bin/phpcs --standard=WordPress-Extra,WordPress-VIP-Go
// ============================================
// NONCES (CSRF Protection)
// ============================================
// Forms
wp_nonce_field('action_name', 'nonce_field_name');
wp_verify_nonce($_POST['nonce_field_name'], 'action_name');
// URLs
wp_nonce_url($url, 'action_name', 'nonce_param');
wp_verify_nonce($_GET['nonce_param'], 'action_name');
// AJAX
wp_create_nonce('ajax_action');
check_ajax_referer('ajax_action', 'nonce');
// ============================================
// SANITIZATION (Input Cleaning)
// ============================================
sanitize_text_field() // Single-line text
sanitize_textarea_field()// Multi-line text
sanitize_email() // Email addresses
esc_url_raw() // URLs (for storage)
sanitize_file_name() // File names
absint() // Positive integers
wp_kses_post() // HTML content
// ============================================
// VALIDATION (Logic Checks)
// ============================================
is_email($email) // Valid email format
is_numeric($value) // Numeric value
strlen($str) >= 3 // Minimum length
preg_match($pattern) // Pattern matching
in_array($value, $allowed) // Whitelist check
// ============================================
// ESCAPING (Output Protection)
// ============================================
esc_html() // HTML content
esc_attr() // HTML attributes
esc_url() // URLs (output)
esc_js() // JavaScript strings
esc_textarea() // Textarea content
// ============================================
// CAPABILITIES (Authorization)
// ============================================
is_user_logged_in()
current_user_can('capability')
current_user_can('edit_post', $post_id)
// ============================================
// SQL INJECTION PREVENTION
// ============================================
$wpdb->prepare("SELECT * FROM table WHERE id = %d", $id);
$wpdb->insert($table, $data, $format);
$wpdb->update($table, $data, $where, $format, $where_format);
Remember: Security is not a feature—it's a requirement. Every line of code that handles user input or displays data must follow these principles. When in doubt, sanitize, validate, and escape.
Weekly Installs
110
Repository
GitHub Stars
18
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode86
gemini-cli83
codex82
claude-code80
github-copilot80
cursor71
Azure RBAC 权限管理工具:查找最小角色、创建自定义角色与自动化分配
127,200 周安装
iOS Core Location 问题诊断指南 - 位置更新、后台定位、授权问题排查
137 周安装
GRDB.swift SQLite 数据库开发指南 - 类型安全查询、迁移与响应式观察
137 周安装
iOS 存储决策指南:SwiftData、SQLite、文件与云端存储方案选择
137 周安装
CSV处理器:高级数据处理工具,支持解析、转换、清理和分析CSV文件
137 周安装
项目开发指南模板:Next.js + FastAPI + Claude AI 全栈架构最佳实践
137 周安装
ENA数据库API使用指南:查询DNA/RNA序列、基因组组装与生物信息学数据
137 周安装
sanitize_url()esc_url_raw()| URLs (for storage) |
"javascript:alert('xss')" |
"" (blocked) |
sanitize_key() | Array keys, meta keys | "my key!" | "my_key" |
sanitize_file_name() | File uploads | "../../etc/passwd" | "..etcpasswd" |
absint() | Positive integers | "-5", "42abc" | 5, 42 |
intval() | Any integer | "-5", "42.7" | -5, 42 |
floatval() | Floating-point numbers | "3.14abc" | 3.14 |
wp_kses_post() | HTML content (allows safe tags) | "<p>Safe</p><script>Bad</script>" | "<p>Safe</p>" |
wp_kses() | HTML with custom allowed tags | See below | Custom filtering |
sanitize_textarea_field() | Multi-line text | "Line 1\nLine 2<script>" | "Line 1\nLine 2" |
sanitize_title() | Post slugs | "Hello World!" | "hello-world" |
is_int(42) → true |
is_array($value) | Array type | is_array([1,2,3]) → true |
is_user_logged_in() | User authentication | is_user_logged_in() → true/false |
username_exists($user) | Username exists | username_exists('admin') → user_id or null |
email_exists($email) | Email exists | email_exists('user@example.com') → user_id or false |
| HTML href/src |
| Dangerous protocols |
<a href="<?php echo esc_url($link); ?>"> |
esc_js() | JavaScript strings | ' " \ / | <script>var msg = '<?php echo esc_js($message); ?>';</script> |
esc_sql() | DEPRECATED (use $wpdb->prepare()) | SQL special chars | ❌ Don't use |
esc_textarea() | Textarea content | < > & | <textarea><?php echo esc_textarea($content); ?></textarea> |
| Admin only |
upload_files | Upload media | Author, Editor, Admin |
edit_users | Edit user accounts | Admin only |
delete_users | Delete users | Admin only |
install_plugins | Install/activate plugins | Admin only |
switch_themes | Change themes | Admin only |