先列攻击面
服务器是我自己写的,管着笔记和数据库的读写。我站在攻击者角度想了一遍,列了 6 类 62 个用例:
- 目录穿越 13 个:想方设法把读取/写入引到笔记目录之外,比如读 /etc/passwd;
- 命令注入 8 个:往参数里塞
;、|、$()这类 shell 元字符; - 超大 payload / DoS 5 个:50MB 单行、万层嵌套 JSON、巨型 id、7 万个端口的列表;
- 畸形 JSON-RPC 16 个:非法 JSON、顶层数组、缺字段、错误版本、不存在的方法;
- 认证绕过 3 个:跳过握手直接调工具、伪造身份信息、非法协议版本;
- SQL 注入 17 个:多语句、load_extension、readfile/writefile、递归 CTE 耗尽。
第一轮:61 个挡住了,1 个真被打穿了
打穿的那个很隐蔽,也是最值得说的——前缀目录符号链接逃逸。
我当时的边界检查是这么写的:
str(p).startswith(str(NOTES_DIR))
意思是”这个路径的字符串,是不是以笔记目录开头”。听起来没毛病,但它有个致命问题:字符串前缀不等于目录边界。
/home/admin/notes 是 /home/admin/notes2_evil 的字符串前缀,但 notes2_evil 根本不是 notes 的子目录。于是攻击手法是:在笔记目录里放一个符号链接,指向那个”前缀兄弟目录”里的文件,边界检查通过,越界文件被读了出来。
修复方法也简单,但得改对地方:
- 边界判断换成
Path.is_relative_to()——逐个路径分量比较,不是比字符串开头; - 笔记和数据库路径一律拒绝符号链接,堵死这条逃逸通道。
修完之后把同样的攻击再打一遍,返回”拒绝符号链接”,挡住了。
顺手还加固了四个会崩进程的地方
真漏洞只有那一个,但还有几个”没被打穿但必须修”的:
- 顶层不是对象的 JSON 会直接把进程干崩。
[1,2,3]能过json.loads,但后面msg.get()抛错,异常处理里再调一次又抛,进程就没了。修法是校验isinstance(msg, dict); - 一万层嵌套的 JSON 也会崩。它抛的是
RecursionError不是JSONDecodeError,我原来的except没覆盖到; - 单行 50MB 直接整行吸进内存。加了 1MB 的行上限;
- 读笔记没有大小上限,超大文件会把响应撑爆。加了 2MB 上限。
第二轮回归:62 个全挡住
修完之后把 62 个攻击全部重新打一遍:
- 三个会崩进程的攻击全活下来,进程不崩,按规范回错或直接丢弃;
- 命令注入 8 个全挡——因为我所有系统命令都走
subprocess.run(list),没有shell=True,天然没有注入面; - SQL 注入 17 个全挡——authorizer 白名单 + 只读模式 + 3 秒超时 + 行/字节上限,递归 CTE 耗尽被超时掐断;
- 目录穿越 13 个全挡——URL 编码、反斜杠、Unicode 全角、混合路径、绝对路径全被拦。
顺带还发现了一个事
用手写 HTTP 请求探的时候发现,家里一个 Web 控制台监听在 0.0.0.0(所有网卡),局域网 IP 直接访问返回 200,而且只读 API 没带任何认证就吐出了 17KB 的会话元数据。配置文件里其实配了 token,但那组 API 没强制校验。
这个不算漏洞(监听地址是内网段),但值得记下来:监听在 0.0.0.0 和 127.0.0.1 是两回事,配了认证和强制校验认证也是两回事。
收进来的几句话
这次演练最大的收获不是”找到 1 个漏洞”,而是验证了那句话:安全不是”我觉得”,是”我打过了”。
攻击面清单不是抄出来的,是站在攻击者角度一条条想出来的;修复也不是改完就完,是修完立刻把同样的攻击再打一遍确认挡住。
自己的门,得自己先踹几脚。