Of course, this is only for my specific environment; different systems may require different configurations.
I want to emphasize two key points: first, website error logs must not be accessible externally; second, REST APIs must not reveal usernames. If either of these issues exists, you will be required to rectify them. Of course, if your website is not based in China, you can disregard this.
I. First, the REST API interface issue
I have different permissions for different websites, but overall I use two types: completely blocking guest access and blocking guest access from the core.
I will only discuss the server-side here; I won’t cover the PHP side.
If you don’t use REST APIs at all, then the Nginx rules are:
# 完全禁止 WordPress REST API(所有 /wp-json*)
location ^~ /wp-json {
return 403;
}
# 兼容老式 REST 入口:?rest_route=
if ($arg_rest_route != "") {
return 403;
}
My complete nginx rules:
# 有 PUT/DELETE 等需求再放开
if ($request_method !~ ^(GET|POST|HEAD)$) { return 444; }
# 安全响应头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# 如果确认全站 HTTPS,可开启 HSTS
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 允许 robots.txt
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
# 仅当 rest_route 是空/根时跳回首页
# if ($arg_rest_route ~* "^/?$") {
# return 301 $scheme://$host/;
# }
# 出现任何 rest_route 都拒绝:
if ($arg_rest_route != "") { return 403; }
# 禁止直接访问 /wp-json 和 /wp-json/
location = /wp-json { return 301 $scheme://$host/; }
location = /wp-json/ { return 301 $scheme://$host/; }
# 屏蔽默认 REST API 端点(wp/v2 和 oembed/1.0)
location ~* ^/wp-json/(wp/v2|oembed/1\.0)(/.*)?$ {
return 403;
}
# # REST API:只屏蔽高风险枚举端点
# location ~* ^/wp-json/oembed/1\.0(/.*)?$ {
# return 403;
# }
# 其他 REST API 请求:仍走 WP
location ~* ^/wp-json(/.*)?$ {
try_files $uri $uri/ /index.php?$args;
}
# 禁止访问日志/调试文件 + debug.log 特别保护 + wp-content 调试文件
location ~* \.(log|logs|debug|txt)$ {
deny all;
access_log off;
log_not_found off;
}
location = /wp-content/debug.log { deny all; }
location ~* ^/wp-content/.*\.(log|txt|debug)$ {
deny all;
}
# 隐藏文件
location ~ /\.(?!well-known).* {
deny all;
}
# WordPress 核心敏感文件
location ~* ^/(wp-config\.php|readme\.html|license\.txt)$ {
deny all;
}
# 防止访问 wp-includes 里的 php
location ~* ^/wp-includes/.*\.php$ {
deny all;
}
# uploads 禁止执行 PHP
location ~* ^/wp-content/uploads/.*\.php$ {
deny all;
}
In other words, I only blocked core API requests. Some of my functionalities use APIs that need to be accessible to visitors, so I couldn’t completely block them.
Although I allowed other API requests in the nginx rules, it’s not enough. For deeper security, I also restricted API permissions in the theme. Therefore, for interfaces that need to be accessible to visitors, I need to allow them again on the PHP side by modifying the theme’s functions.php file:
// 控制 REST API 访问(统一处理 wp-json 和 ?rest_route= 两种情况)
add_filter('rest_authentication_errors', function($result) {
// 获取当前 REST 路径
if (!empty($GLOBALS['wp']->query_vars['rest_route'])) {
$current_route = trim($GLOBALS['wp']->query_vars['rest_route'], '/');
} elseif (!empty($_GET['rest_route'])) {
$current_route = trim($_GET['rest_route'], '/');
} else {
$current_route = '';
}
error_log("REST current_route: " . $current_route);
// 允许未登录访问的命名空间
$allowed_patterns = [
'#^xb_pdftwpe/v1(/.*)?$#',
];
foreach ($allowed_patterns as $pattern) {
if (preg_match($pattern, $current_route)) {
error_log("REST route allowed: $current_route");
return true; // 返回 true
}
}
// 屏蔽默认端点和根路径
if ($current_route === '' || preg_match('#^(wp/v2|oembed/1.0)(/.*)?$#', $current_route)) {
return new WP_Error('rest_forbidden', 'This feature is not available.', ['status' => 403]);
}
// 其他未匹配端口,未登录用户返回 401
if (!is_user_logged_in()) {
error_log("REST API blocked: Route $current_route not allowed for unauthenticated users");
return new WP_Error('rest_not_logged_in', '您必须登录才能访问此资源。', ['status' => 401]);
}
return $result;
}, 10, 1);
For example, the `xb_pdftwpe` interface here is a PDF to Word document conversion function that I need to make accessible to visitors.
II. WordPress Configuration File Permissions
That is, the permissions for `wp-config.php`, which I set to 600:
Meaning:
- 6 (owner): Read + Write
- 0 (group): None
- 0 (other): None
Only the PHP running user can read and write; other users (web, system, etc.) cannot see it. Of course, if your website doesn’t use backend updates, you can lower it to 444. You can manually change this each time you encounter a problem.
Because I need to see any errors on the website, but I can’t directly display the errors on the website, the configuration in `wp-config.php` is as follows:
define('WP_DEBUG', true);
define('WP_DEBUG_DISPLAY', false);
define('WP_DEBUG_LOG', true);
// 强制 PHP 错误也不输出到前台
// @ini_set('display_errors', '0');
I still need to view the logs, but they can’t be displayed on the front end or seen by other users. This is because we’ve already configured permissions in the nginx rules, so external access to download the debug.log file is also blocked.
This is my current security configuration limitation for my WordPress website. Of course, I won’t go into detail about the server itself and CDN; environments differ, so you should configure them according to your needs. My server uses key-based login, and I’ve also configured regular CDN rules. I used Tencent Cloud’s EdgeOne for a while, but switched back to a regular CDN due to the limited features of the free version. The free version of EdgeOne doesn’t support many of the features I need, and the paid version is too expensive, so it’s better to implement more restrictions in a regular CDN.