到目前为止,你已经熟悉了为 WordPress 开发插件和主题的基础知识。现在也是考虑编写代码时安全影响的好时机。
在本课中,你将学习如何以安全为出发点进行开发。
你将了解确保代码安全的好处、保护代码安全的步骤,以及在哪里可以找到更多关于以安全为出发点进行开发的信息。
免责声明
本课旨在作为开发插件和主题时培养安全意识的入门介绍。本课中使用的代码非常简化。你不应在插件或主题中使用本教程中的任何代码。
请务必阅读 WordPress 开发者手册中关于安全的完整文档,网址为 https://developer.wordpress.org/apis/security/,以确保你遵循正确的方法和流程。
什么是以安全为出发点进行开发?
以安全为出发点进行开发,是指确保你的代码不仅能够正常工作,而且不会引入任何安全漏洞的过程。
如果你的插件或主题代码存在安全漏洞,可能会使任何安装了你的产品的 WordPress 网站面临潜在攻击,并可能导致该网站被入侵。
在编写代码时,培养安全思维并思考你的代码可能被恶意利用的方式非常重要。
- 不要信任任何数据,无论是用户输入、第三方 API 数据,甚至是数据库中的数据。始终检查以确保数据有效且使用安全。
- WordPress 提供了许多 API,可以帮助你完成常见任务,例如清理用户输入、验证数据和转义输出。依赖这些 API 来帮助验证和清理数据,而不是编写自己的函数。
- 及时了解常见漏洞,并保持代码更新以防止它们。
安全在开发过程中处于什么位置?
安全应该在开发过程的每个阶段都加以考虑。
通常,发现的大多数安全漏洞都发生在 PHP 层,该层在服务器上执行。
因此,本课将重点介绍 PHP 代码中最常见的预防措施。
清理输入
开发时要采取的首要步骤之一是确保任何用户输入都经过清理。这意味着任何来自用户的数据,例如表单提交或 URL 参数,都要经过检查以确保使用安全。
在此示例代码中,名称和电子邮件字段是从插件生成的表单提交的,然后保存在名为 form_submissions 的自定义数据库表中。
$name = $_POST['name'];
$email = $_POST['email'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);
如你所见,数据直接保存到数据库,没有任何清理。
这意味着如果用户提交的名称为 John'; DROP TABLE form_submissions;--,SQL INSERT 查询将运行,随后是 DROP 查询,该表将从数据库中删除!
WordPress 提供了一个清理 API,可用于清理传入的数据。你可以使用 sanitize_text_field 和 sanitize_email 函数在名称和电子邮件字段用于查询之前对其进行清理。
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);
请注意,代码遵循了清理数据的一个关键原则:尽可能早地进行清理。
要了解更多关于 WordPress 开发者可用的清理函数,请查看 WordPress 开发者文档中的“清理数据”页面。
验证数据 (4:06)
验证数据是根据预定义模式(或多个模式)对其进行测试的过程,结果明确:有效或无效。
不可信数据可能来自许多来源:用户、第三方 API 数据,甚至你的数据库数据也可能被视为不可信,尤其是当其他内容修改过它时。即使是网站管理员也可能犯错,输入不正确或不安全的数据,因此始终检查数据非常重要。
在此示例中,删除函数要求将数字 ID 发布到 admin-ajax 回调:
add_action( 'wp_ajax_delete_form_submission', 'wp_learn_delete_form_submission' );
function wp_learn_delete_form_submission() {
if ( ! isset( $_POST['id'] ) ) {
wp_send_json_error( 'Invalid ID' );
}
$id = $_POST['id'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "DELETE FROM $table_name WHERE id = $id";
$result = $wpdb->get_results( $sql );
return wp_send_json( array( 'result' => $result ) );
}
这里 ID 直接用于 SQL 查询,没有任何验证。同样,这意味着如果用户提交的 ID 为 1; DROP TABLE form_submissions;,相同的 SQL DROP 查询将在删除后运行,表将被删除。
为了防止这种情况,你可以使用 PHP 的类型转换功能,确保 $id 的值始终是整数。可以通过在变量名前添加 (int) 来实现,如下所示:
$id = (int) $_POST['id'];
请注意,这仅当通过 $_POST 数组传递的字符串的第一个字符可以转换为整数时才有效,否则 $id 的值将为 0。在这种情况下,最好更新代码以处理这种情况。
该代码还遵循了验证数据的一个关键原则,即尽早进行验证。
if ($id === 0){
// return early with an error
return wp_send_json( array( 'result' => 'Invalid ID passed' ) );
}
要了解更多关于验证数据的不同方法,请查看 WordPress 开发者文档中的“验证数据”部分。
转义输出
安全的另一个方面是确保你输出到浏览器的任何信息都是安全的,包括任何文本、HTML 或 JavaScript 代码,以及来自数据库的数据。
即使你的代码不负责所显示数据的来源,它也有责任安全地显示它。
在这个例子中,代码从数据库中获取表单提交数据,然后循环遍历这些提交数据,并在 WordPress 仪表盘的管理界面中输出提交数据:
$submissions = wp_learn_get_form_submissions();
?>
<div class="wrap" id="wp_learn_admin">
<h1>Admin</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<?php foreach ($submissions as $submission){ ?>
<tr>
<td><?php echo $submission->name; ?></td>
<td><?php echo $submission->email; ?></td>
<td><a class="delete-submission" data-id="<?php echo $submission->id?>" style="cursor:pointer;">Delete</a></td>
</tr>
<?php } ?>
</table>
</div>
<?php
这里我们有三个需要转义的数据项:$submission->name 和 $submission->email 字段,以及 $submission->id。
<td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>
对于名称和电子邮件字段,我们可以使用 WordPress 内置的转义函数 esc_html()。ID 可以通过将其转换为整数来转义,就像你在数据验证时可能做的那样。
<td><?php echo esc_html( $submission->name ); ?></td>
<td><?php echo esc_html( $submission->email ); ?></td>
<td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>
请注意,这段代码遵循了转义数据的一个关键原则,即尽可能晚地转义数据。
要了解更多关于转义输出的不同方法,请查看 WordPress 开发者文档中的“转义数据”部分。
防止无效请求
每当发出请求时,检查该请求是否有效非常重要。这意味着要检查请求是否来自可信来源。
例如,你可能有一个短代码用于渲染表单,用户可以通过它提交信息。
渲染表单的函数可能看起来像这样。
add_shortcode( 'wp_learn_form_shortcode', 'wp_learn_form_shortcode' );
function wp_learn_form_shortcode() {
ob_start();
?>
<form method="post">
<input type="hidden" name="wp_learn_form" value="submit">
<div>
<label for="email">Name</label>
<input type="text" id="name" name="name" placeholder="Name">
</div>
<div>
<label for="email">Email address</label>
<input type="text" id="email" name="email" placeholder="Email address">
</div>
<div>
<input type="submit" id="submit" name="submit" value="Submit">
</div>
</form>
<?php
$form = ob_get_clean();
return $form;
}
?>
当用户提交表单时,数据会被发送到服务器。然后数据被处理、清理并存储在数据库中。
add_action( 'wp', 'wp_learn_maybe_process_form' );
function wp_learn_maybe_process_form() {
if (!isset($_POST['wp_learn_form'])){
return;
}
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);
if ( 0 < $result ) {
wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
die();
}
wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
die();
}
由于表单可能出现在任何使用该短代码的页面上,恶意用户可能会尝试向表单发送 POST 请求,要么寻找插件中的漏洞,要么发送多个请求。
为了防止这种情况,你可以检查请求是否来自可信来源。为此,你可以实现一种称为 nonce(一次性数字)的机制。
首先,在表单本身中,你可以通过使用 wp_nonce_field 函数添加一个 nonce 字段,并向该函数传递一个 nonce 操作和 nonce 名称:
wp_nonce_field( 'wp_learn_form_nonce_action', 'wp_learn_form_nonce_field' );
当表单在前端渲染时,会向表单中添加一个隐藏字段,使用 nonce 名称作为隐藏字段的 id 和 name 属性,并将生成的 nonce 作为字段值。当表单提交时,这些数据会被发送。
然后,在处理表单数据的函数中,你可以通过使用 wp_verify_nonce 函数来验证 nonce 是否有效,并将 POST 提交的 nonce 字段和 nonce 操作传递给该函数。如果验证结果为 false,你可以提前退出,防止任何进一步的代码执行。
if ( ! wp_verify_nonce( $_POST['wp_learn_form_nonce_field'], 'wp_learn_form_nonce_action' ) {
wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
die();
}
任何时候你的代码发出网络请求,无论是通过重定向到新 URL、向表单 POST 数据,还是发出 AJAX 请求,你都应该检查请求是否有效。
要了解更多关于如何在插件中使用 nonce,请查看 WordPress 开发者文档中的“Nonce”部分。
防止未认证用户
根据代码的功能,最好将某些功能限制为仅具有特定权限级别的用户使用。例如,你可能有一个从数据库中删除数据的函数。
function wp_learn_delete_form_submission() {
if ( ! isset( $_POST['id'] ) ) {
wp_send_json_error( 'Invalid ID' );
}
$id = (int) $_POST['id'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';
$sql = "DELETE FROM $table_name WHERE id = $id";
$result = $wpdb->get_results( $sql );
return wp_send_json( array( 'result' => $result ) );
}
虽然你可能会尽力防止任何没有权限的人运行此函数,但针对这种情况添加检查仍然是一个好主意。
WordPress 包含一个强大的用户角色和权限系统,允许你使用默认的用户角色和权限,或创建自定义角色和权限。
在这种情况下,可以简单地只允许具有管理站点选项权限的用户执行操作,这是管理员角色中包含的标准权限。
function wp_learn_delete_form_submission() {
if ( ! current_user_can( 'manage_options' ) ) {
return wp_send_json( array( 'result' => 'Authentication error' ) );
}
// rest of function code
}
WordPress 开发者文档中有一个关于用户角色和权限的详细部分,其中包含默认权限列表以及如何创建自定义权限。
进一步阅读
为了准备好以安全为出发点进行开发,请务必阅读 WordPress 开发者文档中关于安全性的条目,其中包含本课程中的所有示例,以及关于安全最佳实践、常见漏洞和更多示例代码的额外信息。