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 打补丁(经代理必超时)。
148 lines
6.8 KiB
Docker
148 lines
6.8 KiB
Docker
# 不用 `# syntax=docker/dockerfile:1`:该指令会让 BuildKit 去 pull
|
||
# docker/dockerfile 前端镜像,在无外网/仅代理的环境里会直接构建失败。
|
||
# 本文件未使用任何 BuildKit 专有语法,去掉后走内置前端即可。
|
||
|
||
# ── Stage 1: Go binaries ──
|
||
FROM golang:1.21-bookworm AS gobuild
|
||
|
||
WORKDIR /src/history
|
||
COPY history-server/ .
|
||
RUN GOPROXY=https://goproxy.cn,direct CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/history-server .
|
||
|
||
WORKDIR /src/admin
|
||
COPY admin-server/ .
|
||
RUN GOPROXY=https://goproxy.cn,direct go mod tidy && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/admin-server .
|
||
|
||
# ── Stage 2: SearXNG from source ──
|
||
FROM debian:bookworm-slim AS searxng-build
|
||
|
||
# 需要代理时用 --build-arg 传入(默认不设,保证他人环境可直接构建)
|
||
ARG HTTP_PROXY=
|
||
ARG HTTPS_PROXY=
|
||
|
||
ENV DEBIAN_FRONTEND=noninteractive
|
||
ENV http_proxy=${HTTP_PROXY}
|
||
ENV https_proxy=${HTTPS_PROXY}
|
||
ENV HTTP_PROXY=${HTTP_PROXY}
|
||
ENV HTTPS_PROXY=${HTTPS_PROXY}
|
||
|
||
RUN apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 update \
|
||
&& apt-get -o Acquire::Retries=5 install -y --no-install-recommends \
|
||
python3 python3-pip python3-venv git ca-certificates \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
|
||
# 打补丁:上游 duckduckgo_web 引擎抓取首页链接时把 timeout 硬编码成 2 秒
|
||
# (searx/engines/duckduckgo_web.py 的 _fetch_first_page_link)。经代理出网时这
|
||
# 段握手常需 2~3 秒,于是必然超时 -> 连续失败 -> 引擎被挂起,表现为「秒回 0 条
|
||
# 结果」。放宽到 8 秒。上游改动导致下面 test/grep 失配时构建会失败,属预期。
|
||
RUN git clone --depth 1 https://github.com/searxng/searxng.git /src/searxng \
|
||
&& python3 -m venv /opt/searxng-venv \
|
||
&& /opt/searxng-venv/bin/pip install --upgrade pip setuptools wheel msgspec \
|
||
&& cd /src/searxng \
|
||
&& /opt/searxng-venv/bin/pip install --no-cache-dir -r requirements.txt \
|
||
&& /opt/searxng-venv/bin/pip install --no-cache-dir --no-build-isolation . \
|
||
&& DDG_WEB=/opt/searxng-venv/lib/python3.11/site-packages/searx/engines/duckduckgo_web.py \
|
||
&& test "$(grep -c '^ timeout=2,$' $DDG_WEB)" = "1" \
|
||
&& sed -i 's/^ timeout=2,$/ timeout=8,/' $DDG_WEB \
|
||
&& grep -q '^ timeout=8,$' $DDG_WEB \
|
||
&& rm -rf /src/searxng
|
||
|
||
# ── Stage 3: browser extension packages ──
|
||
# 供 /extension/install.html 下载(Chromium 用 zip,Firefox 用 xpi)
|
||
FROM debian:bookworm-slim AS extpack
|
||
|
||
RUN apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 update \
|
||
&& apt-get -o Acquire::Retries=5 install -y --no-install-recommends zip ca-certificates \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
|
||
COPY extension/chrome /src/chrome
|
||
COPY extension/firefox /src/firefox
|
||
# Go 的 -o 会自动建父目录,zip 不会,所以这里必须显式创建 /out
|
||
RUN mkdir -p /out \
|
||
&& cd /src/chrome && zip -qr /out/navi-chrome.zip . \
|
||
&& cd /src/firefox && zip -qr /out/navi-firefox.xpi .
|
||
|
||
# ── Stage 3b: bundled acme.sh (for cert-agent) ──
|
||
# 为什么要内置:cert-agent 若在运行时安装 acme.sh,installer 会从
|
||
# raw.githubusercontent.com 拉源码,而该域名在境内直连超时
|
||
# (实测直连 8.2s 失败 / 经代理 0.53s 成功),导致首次启动静默失败。
|
||
# 内置一份让 cert-agent 彻底不依赖运行时下载。
|
||
FROM debian:bookworm-slim AS acmebuild
|
||
|
||
RUN apt-get -o Acquire::Retries=5 install -y --no-install-recommends \
|
||
git ca-certificates curl openssl \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
|
||
RUN git clone --depth 1 https://github.com/acmesh-official/acme.sh /src/acme.sh \
|
||
&& chmod +x /src/acme.sh/acme.sh \
|
||
&& rm -rf /src/acme.sh/.git
|
||
|
||
# ── Stage 4: Runtime ──
|
||
FROM debian:bookworm-slim
|
||
|
||
ENV DEBIAN_FRONTEND=noninteractive
|
||
|
||
RUN apt-get -o Acquire::Retries=5 -o Acquire::http::Timeout=30 update \
|
||
&& apt-get -o Acquire::Retries=5 install -y --no-install-recommends \
|
||
nginx python3 supervisor curl ca-certificates git \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
|
||
# Copy SearXNG venv + source
|
||
COPY --from=searxng-build /opt/searxng-venv /app/searxng-venv
|
||
COPY --from=searxng-build /usr/lib/python3.11 /usr/lib/python3.11
|
||
COPY --from=searxng-build /lib/x86_64-linux-gnu /lib/x86_64-linux-gnu
|
||
COPY --from=searxng-build /usr/lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu
|
||
|
||
# Dashy static files
|
||
COPY --from=lissy93/dashy:latest /app/dist /app/dashy
|
||
|
||
# Go binaries
|
||
COPY --from=gobuild /out/history-server /usr/local/bin/history-server
|
||
COPY --from=gobuild /out/admin-server /usr/local/bin/admin-server
|
||
|
||
# Config files
|
||
COPY nginx.conf /etc/nginx/nginx.conf
|
||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||
COPY searxng-settings.yml /etc/searxng/settings.yml
|
||
COPY conf.example.yml /app/defaults/conf.yml
|
||
COPY admin/index.html /app/admin/index.html
|
||
COPY portal /app/portal
|
||
# 站点图标:纸飞机。覆盖 Dashy 自带的 web-icons/favicon-*.png,
|
||
# 这样即使将来上游 index.html 变了、patch 失效,标签栏仍是我们的图标。
|
||
COPY portal/favicon.svg /app/dashy/favicon.svg
|
||
COPY portal/favicon.ico /app/dashy/favicon.ico
|
||
COPY portal/apple-touch-icon.png /app/dashy/apple-touch-icon.png
|
||
COPY portal/web-icons/ /app/dashy/web-icons/
|
||
# 见 zd-host.sh 同名注释:Dashy 前端 JS 运行时会把 icon link 改成
|
||
# /icons/favicon.ico,而镜像里没有 /icons/ 目录,静态改 index.html 无效。
|
||
COPY portal/favicon.ico /app/dashy/icons/favicon.ico
|
||
COPY portal/favicon.svg /app/dashy/icons/favicon.svg
|
||
|
||
# 浏览器扩展安装页与安装包(通过 /extension/ 访问)
|
||
COPY web/extension/install.html /app/dashy/extension/install.html
|
||
COPY --from=extpack /out/navi-chrome.zip /app/dashy/extension/navi-chrome.zip
|
||
COPY --from=extpack /out/navi-firefox.xpi /app/dashy/extension/navi-firefox.xpi
|
||
|
||
# 容器内自动续签(cert-agent)。脚本本身不含任何凭据,凭据只从环境变量读。
|
||
COPY deploy/cert-agent.sh /usr/local/bin/cert-agent
|
||
RUN chmod +x /usr/local/bin/cert-agent
|
||
# 内置 acme.sh:cert-agent 优先用它,避免运行时从 raw.githubusercontent.com
|
||
# 下载失败(该域名境内直连超时)。
|
||
COPY --from=acmebuild /src/acme.sh /usr/local/share/acme.sh
|
||
|
||
# Inject search script into Dashy
|
||
COPY web/navi.js /app/dashy/navi.js
|
||
COPY web/navi.css /app/dashy/navi.css
|
||
COPY web/patch-index.py /tmp/patch-index.py
|
||
# index.html 里不一定有 </body>,用脚本幂等注入,不能靠 sed 替换 </body>。
|
||
RUN python3 /tmp/patch-index.py /app/dashy/index.html
|
||
|
||
# Data directory
|
||
RUN mkdir -p /app/data/item-icons /app/data/gateway /app/data/certs /app/data/acme /var/log/supervisor /var/cache/searxng /run/nginx
|
||
|
||
VOLUME ["/app/data"]
|
||
EXPOSE 18080 3080 3081 8443
|
||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
||
CMD curl -fs http://localhost:18080/healthz || exit 1
|
||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|