Files
HomeDash/admin-server/main_test_setup_test.go
HomeDash dea722f51c fix(security): 管理 API 默认拒绝 + 堵住渗透测试报告的 5 个攻击面
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 打补丁(经代理必超时)。
2026-10-22 14:08:06 +08:00

34 lines
1.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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())
}