mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 19:33:56 +08:00
🦄 Sync 2026-10-02 14:52:05
This commit is contained in:
1 parent
4c85793342
commit
0bfe84f1e8
133 files changed
+7983
-3225
No files matched your search
@@ -4,11 +4,61 @@
|
||||
- `http.lua` 下载前解析主机名,拒绝 localhost/内网/链路本地/保留地址
|
||||
- DNS 解析后二次校验,处理重定向
|
||||
- 仅允许 http/https 协议;端口限定 1–65535
|
||||
- **解析失败即拒绝(fail-closed)**:主机名解析不出 IP 时不放行。
|
||||
- **解析失败即拒绝(fail-closed)**:**有解析手段**却解析不出 IP 时不放行。
|
||||
检查的强度不能低于被检查者 —— `127.0.0.1.nip.io` 这类「公网可解析到内网」的域名,
|
||||
若在本机解析失败的那一刻被放行,curl 自己仍会把它解析到 127.0.0.1 并连上去。
|
||||
代价为零:本包依赖 `luci-lua-runtime`,后者硬依赖 `luci-lib-nixio`,
|
||||
所以解析失败只意味着「真的解析不了」,此时下载本来也会失败
|
||||
- **解析手段多级回退**:`nixio.getaddrinfo` → busybox `nslookup`。
|
||||
`[2.6.8-r1]` 曾假设「本包依赖 luci-lua-runtime,后者硬依赖 luci-lib-nixio,
|
||||
所以解析失败只意味着真的解析不了」——**该假设在部分设备上不成立**,
|
||||
导致所有域名订阅报「无法解析目标主机名」(`[2.6.12-r1]` 修复)。
|
||||
现在把「本机没有解析手段」与「这个域名解析不出来」严格区分:
|
||||
前者放行并标记 `unverified`,后者仍 fail-closed。
|
||||
- **连接时对端校验**:`fetch_curl` 用 `-w` 取 `%{remote_ip}`,在**连接建立后**
|
||||
复核真正连上的 IP;重定向的每一跳(含第一跳)都复核。
|
||||
这既兜住上面 `unverified` 的放行路径,也消除了「预检解析」与
|
||||
「下载时解析」之间的 TOCTOU 窗口(DNS rebinding)。
|
||||
走代理时跳过 —— `%{remote_ip}` 那时是代理地址。
|
||||
`unverified` 且拿不到对端 IP 时拒绝:「无法校验」不等于「放行」。
|
||||
- **wget 后端在 `unverified` 时直接拒绝**(`[2.6.16-r1]`)。此前该后端只有预检:
|
||||
busybox wget 拿不到对端 IP,`-S` 日志里的重定向链也只能事后看 —— 于是
|
||||
「本机无解析手段」的设备上,预检放行之后**不存在任何一处校验**,等于完全没有
|
||||
SSRF 防护,且这条路径是静默的。现按与 `verify_peer_ip` 相同的原则收敛:
|
||||
校验不了就拒绝,错误信息提示安装 curl。字面 IP 目标不受影响
|
||||
(`check_public` 对 IP 提前返回,不产生 `unverified`)。
|
||||
- **重定向链逐跳复检**:`validate_redirect_chain` 从 wget `-S` 日志按顺序取出每一跳
|
||||
的 `Location`,逐跳 `check_public`,任一跳不安全即整体拒绝并丢弃响应体。
|
||||
限制:这是**事后**校验(busybox wget 无 `--max-redirect`),请求已经发出;
|
||||
curl 路径用 `--max-redirs 0` 自行逐跳跟随,每跳都在**发出前**校验。
|
||||
|
||||
## 访问控制
|
||||
- **ACL 组**(`[2.6.16-r1]`):`root/usr/share/rpcd/acl.d/luci-app-substore.json`
|
||||
定义 `luci-app-substore` 组(读写 `uci: substore`),`menu.d` 两个条目均声明
|
||||
`depends.acl` 引用它 → 未获授权的 LuCI 用户看不到本应用入口。
|
||||
在此之前本应用**没有任何 ACL**,任意已登录 LuCI 用户都能读写全部订阅(含凭据 URL)。
|
||||
- **授权方式**:把用户加入该 ACL 组,例如在 `/etc/config/rpcd` 中为该用户的
|
||||
`read` / `write` 列表加上 `luci-app-substore`(与其它 LuCI 应用一致);
|
||||
仅 root 可见时无需任何配置。
|
||||
- **改完必须刷新 LuCI 索引缓存**:菜单树与 ACL 都被缓存在 `/tmp/luci-indexcache*`
|
||||
(缓存文件名含 menu.d 文件列表的哈希)。新增/修改 menu.d 或 acl.d 后若不刷新,
|
||||
改动**不会生效**且不报错。包的 `postinst` 已自动执行
|
||||
`rm -f /tmp/luci-indexcache && /etc/init.d/luci reload`;
|
||||
手工拷贝文件时需自己执行这两条。
|
||||
- **执行范围(已从上游源码核实)**:这是**入口级**门禁,不只是「菜单里看不见」。
|
||||
ucode dispatcher(23.05+,现代目标机实际运行的那套)的 `build_pagetree()` 把
|
||||
menu.d 与 Lua 控制器装进**同一棵树**,`dispatch()` 逐段 `ctx_append` 累积
|
||||
路径上每个节点的 `depends.acl`,随后 ACL 不足即 `http.status(403)` ——
|
||||
**直接访问 URL 会拿到 403**。又因 ACL 沿路径累积,挂在父节点
|
||||
`admin/services/substore` 上的 ACL 已覆盖其下**全部 18 个** `entry`
|
||||
(form / nodes / delete / save / update / probe …)。
|
||||
旧版 Lua dispatcher(≤ 22.03)行为不同:menu.d 的 `depends.acl` 只影响菜单渲染,
|
||||
入口级需另补 `entry.acl_depends` —— 但**本项目最低支持 23.05**,不在支持范围内,
|
||||
故无需处理(见 LEGACY_ISSUES 1.2)。
|
||||
**已在设备上实测通过**(2026-10-02,按 TESTING.md 第 9 项):未授权用户看不到
|
||||
入口、加入组后入口出现、**直接访问 URL 返回 403 Forbidden**。
|
||||
- **表单 token**:写操作要求 `token` 存在、非空,并在可取到时与
|
||||
`luci.dispatcher.context.authtoken` 比对(该值正是模板中 `token` 的来源),
|
||||
校验失败**回显原因**。所有 `entry` 均未声明 `post`,因此框架的
|
||||
`test_post_security` **不会**执行 —— 上述检查是本应用自己的那一层。
|
||||
|
||||
## 资源限制
|
||||
- 订阅响应体最大 10MB,可配置
|
||||
@@ -25,6 +75,12 @@
|
||||
- 订阅名/文件名白名单校验,禁止 `..`、`/`
|
||||
- 解析时限制结构/大小/嵌套深度,防 YAML/JSON 资源耗尽
|
||||
- URL 参数解码后二次校验
|
||||
- **订阅客户端类型(User-Agent)**:`http.validate_user_agent` 拒绝控制字符与超过
|
||||
256 字符的值。UA 来自表单,最终以 `-A <ua>` / `-U <ua>` 进入 shell 命令 ——
|
||||
`util.shq` 的单引号挡得住注入,但挡不住 UA 里的**换行**:curl 会把它当成额外的
|
||||
请求头拼进去(header injection),日志里也会被换行截断、伪造出额外行。
|
||||
与代理同理,非法 UA **明确失败而非静默忽略**(§12):静默忽略会让用户以为 UA
|
||||
已生效,实际拿到的仍是占位节点
|
||||
- 探测目标主机名拒绝以 `-` 开头:`probe.lua` 的命令形如 `ping -c 1 -W 2 <host>`,
|
||||
而 `--help` / `-c` 会被 busybox 的 getopt 当成**选项**。`util.shq` 的单引号由
|
||||
shell 剥掉,getopt 看到的仍是 `-x`,引号挡不住这一层,必须在取值时就拒绝
|
||||
@@ -44,9 +100,25 @@
|
||||
script data escaped 状态,其后的 `</script>` 不再结束脚本块,整页被吞进脚本
|
||||
|
||||
## 安全测试
|
||||
- 已验证 SSRF 私网拒绝
|
||||
- Base64/JSON 解析异常处理
|
||||
- 超大响应体截断测试
|
||||
|
||||
均为自包含 Lua 5.1 测试,`lua5.1 tests/<file>.lua` 即可运行(无需设备):
|
||||
|
||||
- `tests/dns_fallback_test.lua` —— SSRF 私网拒绝、解析手段回退、
|
||||
「有解析手段却解析不出 → 拒绝」、「无解析手段 → 标记 `unverified`」、
|
||||
curl 的 `%{remote_ip}` 连接后复核,以及 **wget 后端在 `unverified` 时拒绝**
|
||||
(用例 H,含对照组 H2:有解析器时 wget 正常下载)
|
||||
- `tests/parser_mixed_format_test.lua` —— 混合格式导入的拒绝与单一格式的不误判
|
||||
- `tests/acl_menu_test.lua` —— ACL 组定义、菜单 `depends.acl` 引用同一组名、
|
||||
Makefile 确实安装这两个文件(接线上任何一环写错都只会**静默失效**)
|
||||
- `tests/controller_robustness_test.lua` —— 表单 token 校验(合法 / 空 / 不匹配 /
|
||||
取不到 authtoken 的回退)与删除部分失败时的 cron 重写
|
||||
- `tests/data_integrity_test.lua` —— 注入 `atomic_write` 写失败,断言失败被上报
|
||||
而非静默吞掉
|
||||
- Base64/JSON 解析异常处理、超大响应体截断测试见 `tests/` 下同名用例
|
||||
|
||||
**仍未在设备上验证的部分**(如实记录):LuCI 模板渲染、cron 落盘行为均需在目标
|
||||
设备上复核 —— 见 [TESTING.md](TESTING.md)。(ACL 的实际拦截效果已于 2026-10-02
|
||||
在设备上实测通过,见上文「访问控制」。)
|
||||
|
||||
参见 docs/ARCHITECTURE.md 第 5 节安全设计。
|
||||
|
||||
|
||||
Reference in new issue
Block a user