1. 一句话

个人自用工具是在「单用户假设」下写出来的。一旦挂到公网,三类问题必然同时暴露:共享会话让匿名访客直接继承机主身份、静态目录把源码和调试产物公开、演示数据里内嵌着真实个人信息。

它们都不是编码错误,是假设失效:

写法隐含假设挂公网后
会话/凭据存服务器全局文件或模块级单例「只有一个用户」所有访客共用一份会话,谁都能读到机主数据
Flask(static_folder=".")「只有我自己会访问」整个项目目录可被下载
demo/示例数据填真实信息「没人会看这些数据」匿名接口直接把真实身份返回出去

2. 三类泄露

① 共享会话 → 匿名继承机主身份

症状:/api/status 对完全未登录的访客返回机主姓名与学号。

curl -s -k https://host/路由/api/status
# {"logged_in":true,"student_name":"<机主姓名>","su":"<学号>",...}

连带的业务数据接口也全部敞开,实测字节数:

/api/grades      9625 B
/api/timetable  39484 B
/api/selected   22512 B
/api/exams       6249 B

根因:凭据/会话被持久化在服务器全局文件(如 ~/.xxx_config.json)或模块级实例里,每个请求都读到同一份。只要机主登录过一次,服务器就长期持有一份有效会话,任何人打开 URL 都直接继承。

修法:每浏览器独立会话——会话 id 存签名 cookie,客户端实例存服务端内存(LRU + TTL 淘汰);/api/status 只读当前会话、不再自动登录;数据接口无会话一律 401 或回落演示数据;登录成功后不回写全局配置文件。

② static_folder="." → 整个项目目录公开

症状:源码、调试转储、探测结果全部可直接下载。

curl -s -o /dev/null -w '%{http_code}\n' -k https://host/路由/client.py
# 200

实测某项目 37 个文件里 36 个返回 200,包括逆向源码、probe_results.json、页面转储、以及内嵌真实个人信息的 demo_data.py。

⚠️ 最容易误判的地方:.env 被 nginx 的隐藏文件规则挡成 403,但其他文件不受该规则保护。看到 .env 403 就以为目录是安全的,是这类事故的典型入口。

修法:Flask(__name__)(默认 static/ 目录,不存在即可),/ 路由显式 send_file('index.html')。前提是前端资源全走 CDN、没有本地静态依赖。

③ 演示数据里内嵌真实 PII

症状:一个自称 demo_*.py 的文件,里面 get_demo_student_profile() 填的是真实姓名、学号、身份证号、考生号、邮箱、家庭住址。而未登录访客访问 /api/student_profile 时会回落到这个函数。

证件号码: 360************710      ← 身份证
通讯地址: 广东省******************29号
电子邮箱: 180***********com
考生号:   254********217

散落位置不止一处:前端兜底值({{ x || '真实姓名' }})、README 的命令示例(cli.py login -u <真实学号>)、页面转储文件。

修法:按 姓名 / 学号 / 身份证 / 手机 / 邮箱 / 住址 / 准考证号 多模式全仓库扫描,全部换成合成值。身份证等标识改用明显打码形式(3301**********0000),避免"假号码恰好撞上真人"的次生问题。


3. 黑盒自查(不用读代码)

部署前,对每个公开路由跑一遍:

HOST=https://example.com; BASE=/路由

# ① 匿名不该看到身份
curl -s -k "$HOST$BASE/api/status"

# ② 匿名不该拿到业务数据(期望 401,而非 200)
for ep in /api/data /api/list /api/profile; do
  printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' -k "$HOST$BASE$ep")" "$ep"
done

# ③ 源码/产物不该可下载(期望 404)
for f in client.py app.py requirements.txt probe_results.json demo_data.py; do
  printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' -k "$HOST$BASE/$f")" "$f"
done

# ④ 整个目录逐个探(在服务器上跑,最彻底)
for f in $(ls); do
  printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' --noproxy '*' -k "$HOST$BASE/$f")" "$f"
done

代码侧只需 grep 四个模式:

grep -rn 'static_folder' *.py                    # 出现 "." 直接判定有问题
grep -n 'load_config\|\.json' *.py               # 会话/凭据是否落在全局文件
grep -n '^client = \|^app_client = ' web_app.py  # 模块级单例
grep -rln '<真实姓名>\|<真实学号>' .             # PII 残留

4. 泄露窗口内要能回溯「到底有没有人来过」

发现泄露后,第一个问题是有没有被利用。nginx access log 能直接回答:

# 访问过该路由的所有来源 IP
zcat -f /var/log/nginx/access*.log* | grep ' /路由/' | cut -d' ' -f1 | sort | uniq -c | sort -rn

# 重点:访问过源码/转储文件的 IP(这才是真正的危险信号)
zcat -f /var/log/nginx/access*.log* | grep -E ' /路由/.*\.(py|json|md)' | cut -d' ' -f1 | sort | uniq -c

判读要点:

  • 页面/接口的访问可能只是自己或同学用;源码/转储文件的下载才是攻击者在搜集信息。
  • 注意区分服务器自身 IP(自己的巡检脚本)与外部 IP。
  • 交叉印证:同一 IP 是否还在访问你这台机器上的其他个人服务(如面板、WebSocket)。若是,基本可判定是本人或自己给的权限。
  • 写操作接口(提交/删除类)要单独拉出来看:grep -E '/api/(submit|drop|delete)'。带 Referer 指向自己前端页面的,多半是本人点出来的,而非脚本行为。

5. 下次怎么识别

  • 部署前必做第 3 节那四条 curl,不要靠读代码判断——尤其第 ③ 类藏在自称 demo 的文件里,review 时最容易跳过。
  • 看到 Flask(..., static_folder=".") 直接判定有源码暴露问题。
  • .env 返回 403 不等于目录安全。
  • 判断依据是代码形态而不是文件名:名叫 demo_* / sample_* 的文件要当作高风险文件单独审一遍内容。
  • 存量服务上线已久才发现 → 一定跑第 4 节的日志回溯,别假设"没人知道这个 URL"。

6. 相关

来源:2026-09-16 审计两个已部署的个人校园工具(zwu-jwgl-tools、zwu-sztz-tools)。三类问题在两个项目上各中一遍,其中第 ③ 类含真实身份证号与家庭住址。泄露窗口约 4 天,日志回溯确认除本人外无第三方访问。

本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web