2026-10-22 渗透测试报告(report.md)实测确认:统一入口的管理 API 在公网
完全未鉴权,可匿名改路由表、拆登录墙、改门户口令、写门户 HTML、读全部
浏览历史、做内网 SSRF 探测。根因有两层,本次一并修复。
## 根因 1:requireAdminToken 默认放行(代码层)
if !requireTokenForWrite { return true }
线上没有 NAVI_ADMIN_REQUIRE_TOKEN,于是所有"调了鉴权"的 handler 实际全部
放行——形同虚设。现在改为 fail-closed:
- 配了 NAVI_ADMIN_TOKEN → 校验 Bearer token
- 未配且显式 NAVI_ADMIN_ALLOW_UNAUTHENTICATED=1 → 放行(仅本机调试)
- 其它 → 401 拒绝
## 根因 2:login 域没有任何 auth_request(部署层)
kind=auth 的 server 块全站 requireLogin=false(它本身就是登录服务,这点
合理),但 location / 把 /api/、/admin/ 原样反代到 127.0.0.1:3005。两层
都没管 → 管理 API 裸奔。
新增 Route.AdminGuard:开启后 server 级挂 auth_request,只白名单放行登录
流程(/_portal/login|logout|verify、/login、/portal-page、favicon),
其余 /api/、/admin/ 一律要求已登录。
## 同时修掉的 4 个具体问题
1. /api/sync-info 明文回显 syncToken → 不再回显,且要求鉴权。
泄露该 token 等于浏览器历史的全读写权。
2. /api/history GET 无鉴权 → 与 DELETE 分支一致,要求管理员令牌。
3. /api/frameable 未鉴权 SSRF → 加鉴权 + 内网/回环黑名单。除 IP 字面量外
还对域名解析结果做校验,防止 localtest.me 之类指向 127.0.0.1 的绕过。
4. dashy 域 bypassPaths 里的 "/api/" 过宽 → 收窄到 "/api/history/"
(本意只放行浏览记录上报,实际把整个管理 API 暴露了)。
## 回归护栏
新增 admin-server/security_test.go(10 条)与 main_test_setup_test.go。
每条断言都是"改回旧行为就会失败",已用 3 次变异测试验证:
- 恢复默认放行 → TestAdminAPIFailsClosedByDefault FAIL
- sync-info 回显 token → TestSyncInfoDoesNotLeakToken FAIL
- 移除内网黑名单 → TestFrameableBlocksInternalTargets FAIL
- /api/ 豁免认证 → TestAuthKindWithAdminGuardProtectsAPI FAIL
- 不给 server 级 auth → 同上 FAIL
测试 56 项全绿。
## 顺带
searxng-settings.yml:停用 360search / ddgw / wikidata / wikipedia
(各自的实测理由见文件内注释),request_timeout 8.0→4.0。
Dockerfile:给 duckduckgo_web 引擎硬编码的 timeout=2 打补丁(经代理必超时)。
34 lines
1.2 KiB
Go
34 lines
1.2 KiB
Go
package main
|
||
|
||
import (
|
||
"os"
|
||
"testing"
|
||
)
|
||
|
||
// TestMain 统一设置测试期的安全开关。
|
||
//
|
||
// 背景:2026-10-22 的渗透测试后,管理 API 改成「默认拒绝」,
|
||
// /api/frameable 也默认拒绝探测回环地址。这两条都是给线上用的正确默认值,
|
||
// 但会让测试(在 127.0.0.1 的 httptest 服务器上跑、且不带头部的请求)全部 401。
|
||
//
|
||
// 因此测试进程显式打开这两个仅用于本地调试的开关:
|
||
// - NAVI_ADMIN_ALLOW_UNAUTHENTICATED=1:无 token 时放行,便于直接测 handler
|
||
// - NAVI_ALLOW_LOOPBACK_FRAMEABLE=1:允许探测 127.0.0.1 的 httptest 服务器
|
||
//
|
||
// 注意这里同时把 NAVI_ADMIN_TOKEN 清空,确保测的是「无 token」这条路径;
|
||
// 生产部署必须配置真实 token,绝不设这两个开关。
|
||
func TestMain(m *testing.M) {
|
||
_ = os.Unsetenv("NAVI_ADMIN_TOKEN")
|
||
_ = os.Unsetenv("NAVI_ADMIN_REQUIRE_TOKEN")
|
||
_ = os.Setenv("NAVI_ADMIN_ALLOW_UNAUTHENTICATED", "1")
|
||
_ = os.Setenv("NAVI_ALLOW_LOOPBACK_FRAMEABLE", "1")
|
||
|
||
// 变量在包初始化时已从环境读取,这里同步覆盖一次,确保生效。
|
||
adminToken = ""
|
||
requireTokenForWrite = false
|
||
allowUnauthenticated = true
|
||
allowLoopbackFrameable = true
|
||
|
||
os.Exit(m.Run())
|
||
}
|