Nonce 是“一次性数字”,用于帮助保护 URL 和表单免受某些类型的滥用,无论是恶意的还是其他性质的。

从技术上讲,WordPress 的 nonce 并不严格是数字;它们是由数字和字母组成的哈希值。而且它们并非仅使用一次:它们有一个有限的“生命周期”,过期后便失效。在该时间段内,相同的 nonce 会为给定用户在给定上下文中生成。该操作的 nonce 对该用户而言将保持不变,直到该 nonce 生命周期完成。

WordPress 的安全令牌被称为"nonce"(尽管与真正的 nonce 存在上述差异),因为它们的功能与 nonce 非常相似。它们有助于防范多种类型的攻击,包括 CSRF,但由于不检查是否被一次性使用,因此无法防止重放攻击。切勿依赖 nonce 进行身份验证、授权或访问控制。请使用 current_user_can() 保护您的函数,并始终假设 nonce 可能被泄露。

为何要使用 nonce?

关于为何要使用 nonce,可考虑如下示例:管理界面可能会生成一个类似这样的 URL,用于删除编号为 123 的文章。

http://example.com/wp-admin/post.php?post=123&action=trash

当您访问该 URL 时,WordPress 会验证您的认证 Cookie 信息,如果您有权删除该文章,则会执行删除操作。攻击者可以利用这一点,在不知情的情况下让您的浏览器访问该 URL。例如,攻击者可以在第三方页面上构造一个伪装链接,如下所示:

<img src="http://example.com/wp-admin/post.php?post=123&action=trash" />

这将触发您的浏览器向 WordPress 发送请求,而浏览器会自动附加您的认证 Cookie,WordPress 会将此视为有效请求。

添加 nonce 可以防止此类情况。例如,使用 nonce 时,WordPress 为用户生成的 URL 看起来像这样:

http://example.com/wp-admin/post.php?post=123&action=trash&_wpnonce=b192fc4204

如果任何人尝试删除编号为 123 的文章,但未获得 WordPress 生成并分配给用户的正确 nonce,WordPress 将向浏览器返回"403 Forbidden"响应。

创建 nonce

您可以创建 nonce 并将其添加到 URL 的查询字符串中,也可以将其添加到表单的隐藏字段中,或以其他方式使用它。

对于要在 AJAX 请求中使用的 nonce,通常的做法是将其添加到隐藏字段中,以便 JavaScript 代码从中获取。

请注意,nonce 是针对当前用户会话唯一的,因此如果用户异步登录或登出,页面上的任何 nonce 都将不再有效。

自定义访客(未登录用户)的 nonce

默认情况下,WordPress 核心为访客生成相同的 nonce,因为他们的用户 ID 相同(值为 0)。也就是说,它并不能防止访客遭受 CSRF 攻击。为了增强关键操作的安全性,您可以为访客开发会话机制,并挂钩到 nonce_user_logged_out 过滤器,用来自会话机制的另一个随机 ID 替换用户 ID 值 0。

将 nonce 添加到 URL

要将 nonce 添加到 URL,请调用 wp_nonce_url(),指定裸 URL 和代表操作的字符串。例如:

$complete_url = wp_nonce_url( $bare_url, 'trash-post_'.$post->ID );

为了获得最大保护,请确保代表操作的字符串尽可能具体。

默认情况下,wp_nonce_url() 会添加一个名为 _wpnonce 的字段。您可以在函数调用中指定不同的名称。例如:

$complete_url = wp_nonce_url( $bare_url, 'trash-post_'.$post->ID, 'my_nonce' );

将 nonce 添加到表单

要将 nonce 添加到表单,请调用 wp_nonce_field(),指定代表操作的字符串。默认情况下,wp_nonce_field() 会生成两个隐藏字段,一个的值是 nonce,另一个的值是当前 URL(引用来源),并输出结果。例如,以下调用:

wp_nonce_field( 'delete-comment_'.$comment_id );

可能会输出如下内容:

<input type="hidden" id="_wpnonce" name="_wpnonce" value="796c7766b1" />
<input type="hidden" name="_wp_http_referer" value="/wp-admin/edit-comments.php" />

为了获得最大保护,请确保代表操作的字符串尽可能具体。

您可以为 nonce 字段指定不同的名称,可以指定不需要引用来源字段,还可以指定希望返回结果而不是输出。有关语法的详细信息,请参阅:wp_nonce_field()。

以其他方式创建 nonce

要以其他方式创建 nonce,请调用 wp_create_nonce(),指定代表操作的字符串。例如:

$nonce = wp_create_nonce( 'my-action_'.$post->ID );

这仅返回 nonce 本身。例如:295a686963

为了获得最大保护,请确保代表操作的字符串尽可能具体。

验证 nonce

您可以验证通过 URL、管理界面中的表单、AJAX 请求或其他上下文中传递的 nonce。

验证从管理界面传递的 nonce
要验证通过 URL 或管理界面表单传递的 nonce,请调用 check_admin_referer(),指定代表操作的字符串。

例如:

check_admin_referer( 'delete-comment_'.$comment_id );

此调用会检查 nonce 和引用来源,如果检查失败,则采取正常操作(终止脚本执行,返回"403 Forbidden"响应和错误消息)。

如果您在创建 nonce 时未使用默认字段名(_wpnonce),请指定字段名。

例如:

check_admin_referer( 'delete-comment_'.$comment_id, 'my_nonce' );

验证 AJAX 请求中传递的 nonce

要验证 AJAX 请求中传递的 nonce,请调用 check_ajax_referer(),指定代表操作的字符串。例如:

check_ajax_referer( 'process-comment' );

此调用会检查 nonce(但不检查引用来源),如果检查失败,则默认终止脚本执行。

如果您在创建 nonce 时未使用默认字段名之一(_wpnonce 或 _ajax_nonce),或者您希望在终止执行之外采取其他操作,则可以指定额外参数。有关详细信息,请参阅:check_ajax_referer()。

验证其他上下文中传递的 nonce

要验证在其他上下文中传递的 nonce,请调用 wp_verify_nonce(),指定 nonce 和代表操作的字符串。

例如:

wp_verify_nonce( $_POST['my_nonce'], 'process-comment'.$comment_id );

如果结果为 false,请勿继续处理请求。相反,请采取适当的操作。通常的操作是调用 wp_nonce_ays(),它会向浏览器发送"403 Forbidden"响应。

修改 nonce 系统

您可以通过添加各种动作和过滤器来修改 nonce 系统。

修改 nonce 生命周期

默认情况下,nonce 的生命周期为一天。之后,即使 nonce 匹配操作字符串,也不再有效。要更改生命周期,请添加 nonce_life 过滤器,指定以秒为单位的生命周期。

例如,要将生命周期更改为四小时:

add_filter( 'nonce_life', function () { return 4 * HOUR_IN_SECONDS; } );

执行额外验证

当 check_admin_referrer() 发现 nonce 和引用来源有效时,若要执行额外验证,请添加 check_admin_referer 动作。

例如:

function wporg_additional_check ( $action, $result ) {
    ...
}
add_action( 'check_admin_referer', 'wporg_additional_check', 10, 2 );

对于 check_ajax_referer(),以相同方式添加 check_ajax_referer 动作。

更改错误消息

您可以使用翻译系统更改发送 nonce 无效时的错误消息。例如:

function my_nonce_message ($translation) {
    if ($translation === 'Are you sure you want to do this?') {
       return 'No! No! No!';
    } 

    return $translation;
}

add_filter('gettext', 'my_nonce_message');

更多信息

本节包含关于 WordPress 中 nonce 系统的额外信息,这些信息可能偶尔有用。

Nonce 生命周期

请注意,正如 WordPress nonce 并非“一次性使用的数字”一样,nonce 的有效期也并非真正的 nonce 有效期。WordPress 使用一种包含两个时间片(即有效期的一半)的系统,并验证来自当前时间片和上一个时间片的 nonce。在默认设置(24 小时有效期)下,这意味着 nonce 中的时间信息与自 Unix 纪元以来经过的 12 小时周期数量相关。因此,在中午到午夜之间生成的 nonce,其有效期将延续至次日中午。实际有效期 thus 在 12 到 24 小时之间变化。

当 nonce 有效时,用于验证 nonce 的函数会返回当前的时间片编号,即 1 或 2。您可以利用此信息,例如刷新处于第二个时间片的 nonce,以防止它们过期。

Nonce 安全性

如果您正确安装了 WordPress,Nonce 是使用对您的站点唯一的密钥和盐生成的。NONCE_KEY 和 NONCE_SALT 定义在您的 wp-config.php 文件中,该文件包含提供更多信息的注释。

切勿依赖 Nonce 进行身份验证、授权或访问控制。请使用 current_user_can() 保护您的函数,并始终假设 Nonce 可能被攻破。

替换 nonce 系统

构成 nonce 系统的部分函数是可插拔的,您可以通过提供自己的函数来替换它们。

若要更改管理请求或 AJAX 请求的验证方式,您可以替换 check_admin_referrer() 或 check_ajax_referrer(),或者两者都替换。

若要使用其他 nonce 系统替换现有的 nonce 系统,您可以替换 wp_create_nonce()、wp_verify_nonce() 和 wp_nonce_tick()。

相关

Nonce 函数:wp_nonce_ays()、wp_nonce_field()、wp_nonce_url()、wp_verify_nonce()、wp_create_nonce()、check_admin_referer()、check_ajax_referer()、wp_referer_field()

Nonce 钩子:nonce_life、nonce_user_logged_out、explain_nonce_(verb)-(noun)、check_admin_referer