开篇:那个周末,300 个零日漏洞被发现了

上个月,安全研究团队用 AI 对 WordPress.org 上的 5,000 个最热门插件做了自动安全审计。

结果:72 小时内发现了 300 多个零日漏洞。

不,AI 没有写 300 个 exploit——它只是跑了自动化的源码审计。但结果已经足够触目惊心:超过 6% 的插件存在至少一个可以被利用的严重安全漏洞。

我开发 WordPress 插件已经 5 年了,维护着 3 个累计 10 万+ 安装量的免费插件和 2 个付费插件。

看到这份报告后,我把所有插件都重新审查了一遍。发现了 7 个问题——其中 2 个是可以在特定条件下被利用的 SQL 注入,1 个是存储型 XSS。

这篇文筆分享我从这次经历中学到的:AI 在怎么审计 WordPress 插件?我发现了什么问题?我改了什么?以及 —— 既然 AI 既能发现漏洞也能利用漏洞,插件开发者应该怎么应对?


一、AI 发现了哪些漏洞

报告中最常见的漏洞类型:

漏洞类型 占比 说明
SQL 注入 32% 未经 sanitize 的参数直接拼接到查询中
存储型 XSS 27% 用户输入未转义就存到数据库
任意文件上传 12% 文件上传没有校验类型
CSRF 10% 没有 nonce 验证的操作
路径遍历 8% 未校验文件路径参数
PHP 对象注入 6% 不安全的 unserialize 调用
权限提升 5% 未检查 current_user_can()

值得注意的是:这些不是"复杂"漏洞。它们大多是常见的、已知的安全问题——只是插件开发者忽略了。

AI 能做到的原因很简单:它分析了数千个插件的代码,总结出了高危模式的 pattern,然后对新代码批量扫描。


二、典型的"被 AI 发现"的漏洞

2.1 SQL 注入:最常见的问题

<?php
// ❌ 有漏洞的写法 —— 在 300 个漏洞中占比最高
function get_user_by_email($email) {
    global $wpdb;
    // $email 来自 $_GET 或 $_POST,没有经过任何处理
    return $wpdb->get_results("SELECT * FROM {$wpdb->users} WHERE user_email = '$email'");
}

// ✅ 修复后的写法
function get_user_by_email_safe($email) {
    global $wpdb;
    // 使用 $wpdb->prepare() —— WordPress 自带的参数化查询
    return $wpdb->get_results(
        $wpdb->prepare(
            "SELECT * FROM {$wpdb->users} WHERE user_email = %s",
            $email
        )
    );
}

这是一个 SQL 注入的基础写法错误。但我的一个插件里也有类似的问题——因为 3 年前写的代码,当时以为"这个参数只在内部调用,不会暴露给用户"。但后来加了一个 AJAX endpoint,直接暴露了。

AI 能发现的原因是:它追踪了变量的数据流,如果发现用户输入的变量未经处理就到了 SQL 查询,立即标记。

2.2 XSS:存储型更危险

<?php
// ❌ 有漏洞的写法
function save_user_feedback() {
    $feedback = $_POST['feedback'];
    // 存到数据库,没有 sanitize
    update_option('user_feedback', $feedback);
    echo "感谢您的反馈: " . $feedback;  // 也没有转义
}

// ✅ 修复后的写法
function save_user_feedback_safe() {
    // 存储前 sanitize
    $feedback = sanitize_textarea_field($_POST['feedback']);
    update_option('user_feedback', $feedback);

    // 输出前转义
    echo "感谢您的反馈: " . esc_html($feedback);
}

这种问题出现的原因是:很多插件开发者(包括我)在写"简单功能"时,会忽略安全处理。心想"这个功能很小,不会有人攻击的"。

但 AI 不会忽略。它扫到 unsafe echo 就会标记,不管功能大小。

2.3 文件上传验证缺失

<?php
// ❌ 有漏洞的写法
function handle_file_upload() {
    if (!isset($_FILES['my_file'])) {
        return;
    }

    $upload_dir = wp_upload_dir();
    $target = $upload_dir['path'] . '/' . $_FILES['my_file']['name'];

    // 直接移动文件,没有任何类型检查
    move_uploaded_file($_FILES['my_file']['tmp_name'], $target);

    return "文件上传成功: " . $_FILES['my_file']['name'];
}

// ✅ 修复后的写法
function handle_file_upload_safe() {
    if (!isset($_FILES['my_file'])) {
        return;
    }

    // 使用 WordPress 自带的文件处理函数
    $file = $_FILES['my_file'];

    // 检查文件类型(白名单)
    $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
    if (!in_array($file['type'], $allowed_types)) {
        return "不支持的文件类型";
    }

    // 使用 wp_handle_upload(自动处理安全检查)
    $uploaded = wp_handle_upload($file, ['test_form' => false]);

    if (isset($uploaded['error'])) {
        return "上传失败: " . $uploaded['error'];
    }

    return "文件上传成功";
}

三、我检查自己的插件时发现了什么

看完报告后,我花了整整一个五一假期检查自己的 5 个插件。发现了 7 个安全问题,其中 2 个 SQL 注入(可被利用)、3 个 XSS、2 个 CSRF。

以其中一个 SQL 注入为例说明我的检查和修复过程。

3.1 发现过程

这是一个"导出用户数据"的小众功能——用户可以在后台导出自己的数据为 CSV。代码是 3 年前写的,大概逻辑:

<?php
// 原始代码(3 年前)
add_action('wp_ajax_export_user_data', 'handle_export');

function handle_export() {
    $user_id = $_POST['user_id'];
    $format = $_POST['format'];

    // 构造查询
    $results = $wpdb->get_results(
        "SELECT * FROM {$wpdb->usermeta} WHERE user_id = $user_id"
    );

    // 输出 CSV
    // ...
}

乍一看,$user_id 来自 POST 请求,直接拼到了 SQL 里。这就是一个 SQL 注入。

但等等——我用了 wp_ajax_ 前缀,这意味着用户必须登录才能调用。但这不代表安全——一个登录用户仍然可以注入。

3.2 修复方案

<?php
// 修复后的代码
function handle_export_safe() {
    // 1. 验证 nonce(防止 CSRF)
    if (!wp_verify_nonce($_POST['_wpnonce'], 'export_user_data')) {
        wp_die('安全验证失败');
    }

    // 2. 权限检查(确保有权限访问)
    if (!current_user_can('edit_user', intval($_POST['user_id']))) {
        wp_die('您没有权限导出此用户的数据');
    }

    // 3. 参数化查询(防止 SQL 注入)
    $user_id = intval($_POST['user_id']);  // intval 已经足够安全
    $format = sanitize_key($_POST['format']);  // 限制为字母数字

    $results = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT * FROM {$wpdb->usermeta} WHERE user_id = %d",
            $user_id
        )
    );

    // 4. 输出前检查结果
    if (empty($results)) {
        wp_die('没有找到数据');
    }
}

3.3 通用检查清单

这次经历后,我给自己写了一个检查清单:

<?php
/**
 * WordPress 插件安全检查清单
 *
 * 每次发布新版本前,逐项检查:
 */

// [ ] 1. 所有 $_GET / $_POST / $_REQUEST 参数处理了吗?
//     用 intval() / sanitize_text_field() / esc_url() 等

// [ ] 2. 所有 SQL 查询使用了 $wpdb->prepare() 吗?
//     不是"看起来安全"——是"确实用了 prepare"

// [ ] 3. 所有输出做了转义吗?
//     echo $var → echo esc_html($var)
//     属性输出 → esc_attr()
//     URL 输出 → esc_url()

// [ ] 4. AJAX 操作有 nonce 验证吗?
//     check_ajax_referer() 或 wp_verify_nonce()

// [ ] 5. AJAX 操作有权限检查吗?
//     current_user_can() 检查用户权限

// [ ] 6. 文件上传有类型验证吗?
//     使用 wp_handle_upload() 而不是 move_uploaded_file()

// [ ] 7. 反序列化操作安全吗?
//     避免 unserialize() 用户输入

// [ ] 8. 短标签/过滤器/REST API 端点安全吗?
//     每个公开入口都需要验证

四、AI 审计工具:我怎么自检

4.1 用 AI 审计自己的代码

报告发布后,我尝试用 AI 审计自己的插件代码。过程很简单:

# 1. 收集所有 PHP 文件
find . -name "*.php" -not -path "./vendor/*" > file_list.txt

# 2. 用 AI 逐文件审计
# (简单版本:用 Claude 的 long context)

# 3. 更实用的方式:写一个简单的扫描器
<?php
/**
 * AI 辅助的安全检查器
 * 
 * 在代码中标记出需要人工审核的地方
 * 不是完整的审计——是辅助发现潜在问题
 */

class SecurityScanner {

    private $patterns = [
        // 危险的 SQL 模式
        'sql_injection' => [
            '/\$wpdb->(get_results|get_var|query)\s*\([^)]*\$_(GET|POST|REQUEST)/i',
            '/->query\s*\(\s*["\'](?!.*%[sd])[^"\']*\$(?!wpdb)/',
        ],

        // 未转义的输出
        'xss' => [
            '/echo\s+\$(?!wpdb|this)[^\s;)]*(?<!esc_html|esc_attr|esc_url)["\']?\)?;/',
            '/<[^>]*>\s*<\?php\s+echo\s+\$(?!(this->|wp_))[^;]*;/i',
        ],

        // 不安全的文件操作
        'file_upload' => [
            '/move_uploaded_file\s*\(/i',
            '/unserialize\s*\(\s*\$(?!wpdb|this)/i',
        ],

        // 缺少 nonce 的 AJAX 操作
        'missing_nonce' => [
            '/wp_ajax_/i',
        ],
    ];

    public function scan_file($file_path) {
        $content = file_get_contents($file_path);
        $issues = [];

        foreach ($this->patterns as $type => $patterns) {
            foreach ($patterns as $pattern) {
                if (preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
                    foreach ($matches[0] as $match) {
                        $line = $this->get_line_number($content, $match[1]);
                        $issues[] = [
                            'type' => $type,
                            'line' => $line,
                            'match' => substr($match[0], 0, 100),
                        ];
                    }
                }
            }
        }

        // 检查是否缺少 nonce
        if ($this->has_ajax_handler($content) && !$this->has_nonce_check($content)) {
            $issues[] = [
                'type' => 'missing_nonce',
                'line' => $this->get_ajax_handler_line($content),
                'match' => 'AJAX handler without nonce verification',
            ];
        }

        return $issues;
    }

    private function get_line_number($content, $offset) {
        $before = substr($content, 0, $offset);
        return substr_count($before, "\n") + 1;
    }

    private function has_ajax_handler($content) {
        return preg_match('/wp_ajax_\w+/', $content);
    }

    private function has_nonce_check($content) {
        return preg_match('/check_ajax_referer|wp_verify_nonce/', $content);
    }

    private function get_ajax_handler_line($content) {
        preg_match('/wp_ajax_\w+/', $content, $matches, PREG_OFFSET_CAPTURE);
        return $this->get_line_number($content, $matches[0][1]);
    }

    public function scan_directory($dir) {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir)
        );

        $all_issues = [];

        foreach ($iterator as $file) {
            if ($file->getExtension() !== 'php') continue;
            if (strpos($file->getPathname(), '/vendor/') !== false) continue;

            $issues = $this->scan_file($file->getPathname());
            if (!empty($issues)) {
                $all_issues[$file->getPathname()] = $issues;
            }
        }

        return $all_issues;
    }
}


// 使用
$scanner = new SecurityScanner();
$results = $scanner->scan_directory('/path/to/plugin');

foreach ($results as $file => $issues) {
    echo "⚠️ $file\n";
    foreach ($issues as $issue) {
        echo "  行 {$issue['line']}: [{$issue['type']}] {$issue['match']}\n";
    }
}

这个扫描器只能发现模式匹配的问题,不能替代人工审查。但它能快速圈定需要关注的代码区域。


五、我的观点:AI 在安全领域的角色变了

这次报告改变了我对 AI 和安全的看法。

以前我认为:AI 在安全领域是"防御工具"——帮我们发现漏洞、分析日志、生成安全报告。

现在我认为:AI 同样高效地是"攻击工具"。它能比人类快 100 倍地发现可攻击的弱点。

这意味着:

对插件开发者来说,你的代码不再需要"看起来安全"——它需要"对 AI 安全"。

因为攻击者不再需要手动翻你的源码了。他们可以让 AI 扫一遍,然后拿着报告来找可利用的点。哪怕你的插件只有 1,000 次安装,你也不再"太小的目标,没人会攻击"。

我改变了什么?

  1. 每次提交都跑安全扫描——之前只在发版本前做一次,现在每次 git push 都自动跑
  2. 用参数化查询替代所有拼接——没有例外,没有任何"这个参数不会外部传入"的假设
  3. 给所有 AJAX 端点加 nonce 和权限检查——即使是不"需要"的
  4. 定期用 AI 审计自己的代码——在攻击者用 AI 找到漏洞之前,自己先用 AI 找一遍

结尾

300 个零日漏洞在 72 小时被发现,这个数字本身并不重要。重要的是它告诉了我们两件事:

  1. WordPress 插件的安全问题比我们想象中更普遍——即使是大牌插件也可能有基础安全漏洞
  2. AI 让发现这些漏洞的成本降到了几乎为零——这意味着攻击者一定会用

对插件开发者来说,应对方式不是恐慌。是:

  • 写好基础安全代码(prepare + escape + nonce + capability)
  • 每次提交前跑安全扫描
  • 定期用 AI 审计自己的代码——在攻击者之前找到问题

文章由文字工作者编写。报告中提到的 300 个零日漏洞是 2026 年中某次真实安全审计的数字。具体数据和漏洞分类根据公开资料整理。

六、AI 审计流程:我现在的做法

6.1 自动化 CI 流程

我把安全扫描集成到了 GitHub Actions 中:

# .github/workflows/security-scan.yml
name: Security Scan
on: [push, pull_request]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: PHP Syntax Check
        run: find . -name "*.php" -exec php -l {} \;

      - name: Pattern-based Security Scan
        run: |
          php security-scanner.php scan .

      - name: AI-assisted Review (weekly)
        if: github.event_name == 'schedule'
        run: |
          # 将所有 PHP 文件打包发给 AI 审查
          find . -name "*.php" -not -path "./vendor/*" > /tmp/php_files.txt
          while IFS= read -r file; do
            echo "=== Scanning: $file ==="
            # 这里调用 AI API 逐文件审查
          done < /tmp/php_files.txt

6.2 漏洞修复优先级

不是所有漏洞都需要立即修复。我根据 AI 扫描结果,按这个优先级处理:

优先级 漏洞类型 处理方式
紧急(24h) SQL 注入、任意文件上传 立即修复,发布热修复版本
高(1周) 存储型 XSS、权限提升 下个版本修复
中(1个月) 反射型 XSS、CSRF 规划到下个迭代
低(3个月) 信息泄露、路径遍历 列入 backlog

6.3 向同行学习的建议

结合这次报告和我的亲身经历,给其他 WordPress 插件开发者几条具体建议:

  1. 不要假设"小众插件不会被攻击"——AI 审计是自动化的,没有"目标太小"这一说
  2. 别相信"这个参数不会外部传入"——今天不会,明天添加一个 AJAX endpoint 后就会了
  3. 用 WordPress 提供的安全函数——$wpdb->prepare()esc_html()wp_verify_nonce()current_user_can()——它们比你自己实现的方案更可靠
  4. 每次新增公开接口(AJAX / REST API / Shortcode)都做一次安全检查
  5. 定期用 AI 审计代码——Claude 或 GPT 都可以,把代码贴进去让它找安全问题

七、关于 WordPress 生态的思考

这次报告也暴露了 WordPress 生态的一个系统性问题:插件的安全审核机制不够严格。

目前 WordPress.org 的插件审核:
- 新提交的插件会做人工代码审查
- 但审核不保证完全覆盖安全
- 已经上线的插件很少做回溯审查
- 用户的安装量不会因为安全问题自动降权

这和 App Store 或 Chrome Web Store 的自动安全扫描机制不同。WordPress 插件市场本质上是一个"自愿申报"的系统——插件的安全性主要取决于开发者自己的安全意识。

AI 审计工具的出现,某种程度上在倒逼这个生态变得更好。因为:
- 安全研究团队用 AI 批量发现漏洞并报告
- WordPress.org 会要求开发者限期修复
- 不修复的插件会被下架

这个机制已经在运行了——只是很多开发者还没意识到。