mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 01:23:56 +08:00
🎈 Sync 2026-09-30 10:37:56
This commit is contained in:
1 parent
7bb984c650
commit
214a01499a
17 files changed
+386
-69
No files matched your search
@@ -0,0 +1,124 @@
|
||||
-- view_injection_test.lua — 视图层的注入与字段清单一致性检查
|
||||
-- 用法:lua5.1 tests/view_injection_test.lua
|
||||
--
|
||||
-- 这些是静态源码检查(.htm 需要 LuCI 运行时才能渲染,lua5.1 跑不了模板),
|
||||
-- 但检查的正是那类「只在浏览器里才暴露」的缺陷:
|
||||
-- H2 注入到 <script> 里的值必须转义 "</":json_encode 不转义 "<",
|
||||
-- 而 "</script" 会在 HTML 词法阶段就闭合脚本元素(与 JS 字符串上下文无关),
|
||||
-- 查询串里的 id 可以直接构造出 "</script><script>alert(1)</script>"。
|
||||
-- 字段清单必须由服务端注入:core.merge_form_node 用 node.PROTO_FIELDS 决定
|
||||
-- 哪些字段可以被表单覆盖,前端若另抄一份就会漂移(vmess 的 TLS 字段
|
||||
-- 丢失正是这么来的)。
|
||||
|
||||
package.path = "./root/usr/share/?.lua;" .. package.path
|
||||
|
||||
local util = require("substore.util")
|
||||
local node = require("substore.node")
|
||||
|
||||
local passed, failed = 0, 0
|
||||
|
||||
local function check(name, cond)
|
||||
if cond then
|
||||
passed = passed + 1
|
||||
print("PASS " .. name)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("FAIL " .. name)
|
||||
end
|
||||
end
|
||||
|
||||
local VIEWS = {
|
||||
"root/usr/lib/lua/luci/view/substore/nodes.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/node_edit.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/local_form.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/subscriptions.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/form.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/combo.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/output.htm",
|
||||
}
|
||||
|
||||
-- ---------- H2:JSON 编码注入到 JS 的位置必须同时转义 "</" ----------
|
||||
--
|
||||
-- 逐个 json_encode 调用点检查:要么同一行/下一行出现 gsub("</", …),
|
||||
-- 要么该值是白名单归一化后的固定取值(注释里标注了「白名单」)。
|
||||
for _, path in ipairs(VIEWS) do
|
||||
local src = util.read_file(path)
|
||||
if src then
|
||||
local lines = {}
|
||||
for l in (src .. "\n"):gmatch("([^\n]*)\n") do lines[#lines + 1] = l end
|
||||
for i, l in ipairs(lines) do
|
||||
-- 只关心真正注入到模板里的调用,跳过注释
|
||||
if l:find("json_encode") and not l:match("^%s*%-%-") and not l:match("^%s*//") then
|
||||
-- 转义可能写在下一行,白名单说明通常写在紧邻的注释里(上一两行)
|
||||
local window = (lines[i - 2] or "") .. (lines[i - 1] or "") .. l .. (lines[i + 1] or "")
|
||||
local escaped = window:find('gsub("</"', 1, true) ~= nil
|
||||
-- 白名单归一化的取值("text"/"form")不含 "<",无需转义
|
||||
local whitelisted = window:find("白名单", 1, true) ~= nil
|
||||
check(path:match("[^/]+$") .. ":" .. i .. " json_encode escapes </",
|
||||
escaped or whitelisted)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 反向验证:不转义时确实能闭合 script(说明上面的检查不是空转)
|
||||
local payload = "</script><script>alert(1)</script>"
|
||||
check("json_encode does not escape <", util.json_encode(payload):find("</script", 1, true) ~= nil)
|
||||
check("gsub(</) removes the closing tag",
|
||||
(util.json_encode(payload):gsub("</", "<\\/")):find("</script", 1, true) == nil)
|
||||
|
||||
-- ---------- 字段清单:唯一来源在 Lua,页面负责注入 ----------
|
||||
local js = util.read_file("root/www/luci-static/resources/substore/nodeform.js")
|
||||
check("nodeform.js readable", js ~= nil)
|
||||
-- 注释里会举例说明注入方式("var PROTO_FIELDS = { proto: [...] }"),先剥掉注释再查
|
||||
local js_code = js:gsub("/%*.-%*/", ""):gsub("//[^\n]*", "")
|
||||
check("nodeform.js does not hardcode PROTO_FIELDS", js_code:find("PROTO_FIELDS = {") == nil)
|
||||
check("nodeform.js does not hardcode SUBSTORE_PROTOS",
|
||||
js_code:find('SUBSTORE_PROTOS = %[') == nil)
|
||||
check("nodeform.js reads injected PROTO_FIELDS", js:find("window.SUBSTORE_PROTO_FIELDS") ~= nil)
|
||||
check("nodeform.js reads injected SUBSTORE_PROTOS", js:find("window.SUBSTORE_PROTOS") ~= nil)
|
||||
|
||||
for _, path in ipairs({ "root/usr/lib/lua/luci/view/substore/node_edit.htm",
|
||||
"root/usr/lib/lua/luci/view/substore/local_form.htm" }) do
|
||||
local src = util.read_file(path) or ""
|
||||
local name = path:match("[^/]+$")
|
||||
check(name .. " injects SUBSTORE_PROTOS", src:find("var SUBSTORE_PROTOS = <%=js_protos%>", 1, true) ~= nil)
|
||||
check(name .. " injects SUBSTORE_PROTO_FIELDS", src:find("var SUBSTORE_PROTO_FIELDS = <%=js_proto_fields%>", 1, true) ~= nil)
|
||||
check(name .. " loads nodeform.js", src:find("nodeform.js", 1, true) ~= nil)
|
||||
end
|
||||
|
||||
-- 每个协议都要有字段清单,否则表单渲染不出任何输入框
|
||||
for _, p in ipairs(node.PROTOS) do
|
||||
local f = node.PROTO_FIELDS[p]
|
||||
check("PROTO_FIELDS has " .. p, type(f) == "table" and #f > 0)
|
||||
end
|
||||
|
||||
-- 每个渲染出来的字段都要有标签,否则界面上直接显示英文键名
|
||||
local label_src = (util.read_file("root/usr/lib/lua/luci/view/substore/node_edit.htm") or "")
|
||||
local labels = {}
|
||||
for k in label_src:gmatch('"([%w%-]+)"%s*:') do labels[k] = true end
|
||||
local missing = {}
|
||||
for p, fields in pairs(node.PROTO_FIELDS) do
|
||||
for _, k in ipairs(fields) do
|
||||
if not labels[k] then missing[#missing + 1] = p .. "." .. k end
|
||||
end
|
||||
end
|
||||
table.sort(missing)
|
||||
check("every form field has a label" .. (#missing > 0 and (" (missing: " .. table.concat(missing, ",") .. ")") or ""),
|
||||
#missing == 0)
|
||||
|
||||
-- TLS 层字段必须出现在表单里,否则编辑一次就丢 TLS(vmess 的历史缺陷)
|
||||
for _, p in ipairs({ "vmess", "vless" }) do
|
||||
local has = false
|
||||
for _, k in ipairs(node.PROTO_FIELDS[p]) do if k == "security" then has = true end end
|
||||
check(p .. " form renders security", has)
|
||||
end
|
||||
|
||||
-- TLS-only 协议必须由 normalize 保证 security,而不是靠 URI 解析器各自补
|
||||
for _, p in ipairs({ "hysteria2", "hysteria", "tuic", "trojan" }) do
|
||||
check(p .. " normalize forces security=tls",
|
||||
node.normalize({ proto = p, server = "h", port = 1 }).security == "tls")
|
||||
end
|
||||
|
||||
print(string.format("\n%d passed, %d failed", passed, failed))
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
Reference in new issue
Block a user