mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-27 01:52:51 +08:00
9.6 KiB
9.6 KiB
Security — luci-app-substore
SSRF 防护
http.lua下载前解析主机名,拒绝 localhost/内网/链路本地/保留地址- DNS 解析后二次校验,处理重定向
- 仅允许 http/https 协议;端口限定 1–65535
- 解析失败即拒绝(fail-closed):有解析手段却解析不出 IP 时不放行。
检查的强度不能低于被检查者 ——
127.0.0.1.nip.io这类「公网可解析到内网」的域名, 若在本机解析失败的那一刻被放行,curl 自己仍会把它解析到 127.0.0.1 并连上去。 - 解析手段多级回退:
nixio.getaddrinfo→ busyboxnslookup。[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,可配置
- 连接/总超时 20s
- 并发限制由系统调度
- 下载临时文件(
.tmp/.tmp.hdr/.tmp.err)在每一条退出路径上清理。 OpenWrt 的/tmp是 tmpfs,占的是内存;失败路径不清理会让 cron 定时重试 一轮轮往内存里堆文件,其中.tmp可能是最大到max-size的部分响应体 - 批量节点探测并发上限
probe.MAX_PARALLEL = 16。节点数由订阅内容决定(可上千), 每个探测占 1 个进程 + 1 个管道 fd;不限并发会打满路由器的 fd / 进程额度, 之后io.popen静默失败 —— 症状是一大片节点探测不出来,而不是报错
输入校验
- 订阅名/文件名白名单校验,禁止
..、/ - 解析时限制结构/大小/嵌套深度,防 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,引号挡不住这一层,必须在取值时就拒绝
凭据保护
- 订阅 URL 中的 token 不写入普通日志
- 输出/下载接口使用每订阅随机 16 位十六进制 token 鉴权(
core.ensure_token),不可猜测;/substore/download无登录态 - 日志使用
logger -t luci-app-substore,不记录敏感信息 - 落盘权限:
/etc/substore目录0700,subscriptions.json与nodes/*.json0600。io.open按 umask 创建(通常 0644),同机任何用户都能读到订阅 URL、下载 token 与节点凭据,因此util.atomic_write在os.rename之后显式 chmod —— 先 chmod 再 rename 的话,临时文件名可猜,中间窗口里仍能读到。 目录侧由core.ensure_dirs建目录时带-m 700,并对旧版本升级上来、 已存在的目录补一次chmod 700(每进程一次,避免load()每次都 fork) - 注入到页面
<script>的 JSON 转义全部<(<):只转</挡得住</script>提前闭合,却挡不住<!--—— 后者让 HTML 词法阶段进入 script data escaped 状态,其后的</script>不再结束脚本块,整页被吞进脚本
安全测试
均为自包含 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。(ACL 的实际拦截效果已于 2026-10-02 在设备上实测通过,见上文「访问控制」。)
参见 docs/ARCHITECTURE.md 第 5 节安全设计。
Key requirements (from CLAUDE.md):
- Prevent SSRF (reject localhost / private / link-local / reserved addresses)
- DNS rebinding protection
- Limit subscription response body size and set request timeouts
- Prevent path traversal and command injection
- Never log subscription credentials / tokens
- Output interface requires access control or a random token
- Validate imported data; prevent malicious YAML/JSON resource exhaustion