🎁 Sync 2026-10-28 19:35:15

This commit is contained in:
github-actions[bot] committed 2026-10-28 19:35:15 +08:00
1 parent 85209f310d
commit 390801c844
90 files changed
+5821 -1967

No files matched your search

@@ -22,7 +22,7 @@ local function sink_socket(sock, io_err)
if not chunk then
return 1
else
return sock:send(chunk)
return sock:sendall(chunk)
end
end
else
@@ -64,14 +64,17 @@ local function chunksource(sock, buffer)
return function()
local output
local _, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
while not count and #buffer <= 1024 do
while not count and #buffer < 1024 do
local newblock, code = sock:recv(1024 - #buffer)
if not newblock then
if not newblock or newblock == "" then
return nil, code
end
buffer = buffer .. newblock
_, endp, count = buffer:find("^([0-9a-fA-F]+);?.-\r\n")
end
if not count then
return nil, -1, "invalid encoding"
end
count = tonumber(count, 16)
if not count then
return nil, -1, "invalid encoding"
@@ -83,6 +86,7 @@ local function chunksource(sock, buffer)
return output
else
output = buffer:sub(endp+1, endp+count)
local buffered_tail = #buffer - endp - #output
buffer = ""
if count - #output > 0 then
local remain, code = sock:recvall(count-#output)
@@ -92,7 +96,7 @@ local function chunksource(sock, buffer)
output = output .. remain
count, code = sock:recvall(2)
else
count, code = sock:recvall(count+2-#buffer+endp)
count, code = sock:recvall(2-buffered_tail)
end
if not count then
return nil, code
@@ -95,14 +95,30 @@ function auto_setup()
-- form style
local packages = luci.http.formvalue("packages")
local pkgs = ""
local function valid_package(value)
return type(value) == "string" and #value <= 128 and
value:match("^[%w][%w%.%+_%-]*$") ~= nil
end
local function invalid_package()
luci.http.prepare_content("application/json")
luci.http.write_json({success=1, scope="params", error="Invalid package name"})
end
if type(packages) == "table" then
if #packages > 0 then
for k, v in pairs(packages) do
if not valid_package(v) then
invalid_package()
return
end
pkgs = pkgs .. " " .. luci.util.shellquote(v)
end
end
else
if packages ~= nil and packages ~= "" then
if not valid_package(packages) then
invalid_package()
return
end
pkgs = luci.util.shellquote(packages)
end
end
@@ -1,12 +1,22 @@
#!/bin/sh
# Validate all arguments before touching logs, installing packages or sourcing
# metadata scripts. This script is also callable without the LuCI controller.
[ "$#" -gt 0 ] || { echo "No packages specified" >&2; exit 1; }
for pkg in "$@"; do
case "$pkg" in
''|[!a-zA-Z0-9]*|*[!a-zA-Z0-9_.+-]*) echo "Invalid package name" >&2; exit 1 ;;
esac
[ "${#pkg}" -le 128 ] || { echo "Invalid package name" >&2; exit 1; }
done
> /var/log/auto_setup.success
> /var/log/auto_setup.failed
> /var/log/auto_setup.input
save_input() {
local pkg
for pkg in $@; do
for pkg in "$@"; do
echo "$pkg" >> /var/log/auto_setup.input
done
}
@@ -37,14 +47,14 @@ load_quickstart_cfg() {
auto_setup_app() {
local pkg=$1
is-opkg install "app-meta-$pkg" || return 1
sh -c ". '/usr/libexec/istorea/$pkg.sh'"
sh -c '. "$1"' sh "/usr/libexec/istorea/$pkg.sh"
}
auto_setup_apps() {
local pkg
for pkg in $@; do
for pkg in "$@"; do
echo "Setting up $pkg..."
if auto_setup_app $pkg; then
if auto_setup_app "$pkg"; then
echo "Set up $pkg success"
echo "$pkg" >> /var/log/auto_setup.success
else
@@ -0,0 +1,55 @@
"""Exercise the real script with task-owned paths and mocked package installs."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
class AutoSetupTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.logs = self.root / "log"
self.logs.mkdir()
(self.root / "bin").mkdir()
(self.root / "metadata").mkdir()
self.installed = self.root / "installed"
self.configured = self.root / "configured"
helper = self.root / "functions.sh"
helper.write_text('config_load() { return 0; }\nconfig_get() { eval "$1=\\\"$FIXTURE_MAIN\\\""; }\n')
installer = self.root / "bin/is-opkg"
installer.write_text('#!/bin/sh\nprintf "%s\\n" "$@" >> "$FIXTURE_INSTALLED"\n')
installer.chmod(0o700)
source = (Path(__file__).resolve().parents[1] / "root/usr/libexec/quickstart/auto_setup.sh").read_text()
source = source.replace("/var/log", str(self.logs)).replace("/lib/functions.sh", str(helper)).replace("/usr/libexec/istorea", str(self.root / "metadata"))
self.script = self.root / "auto_setup.sh"
self.script.write_text(source)
self.env = dict(os.environ, PATH=str(self.root / "bin") + ":" + os.environ["PATH"], FIXTURE_MAIN=str(self.root / "data"), FIXTURE_INSTALLED=str(self.installed), FIXTURE_CONFIGURED=str(self.configured))
def run_script(self, *args):
return subprocess.run(["sh", str(self.script), *args], env=self.env, capture_output=True, text=True, timeout=5)
def test_rejects_all_invalid_arguments_before_side_effects(self):
for name in ("../other", "--help", "two packages", "pkg'", "", "a" * 129):
result = self.run_script("aria2", name)
self.assertNotEqual(result.returncode, 0)
self.assertFalse(self.installed.exists())
self.assertEqual(list(self.logs.iterdir()), [])
def test_normal_packages_install_configure_and_report(self):
packages = ("aria2", "foo+bar_1.2-3")
for name in packages:
(self.root / "metadata" / (name + ".sh")).write_text('printf "%s\\n" "' + name + '" >> "$FIXTURE_CONFIGURED"\n')
result = self.run_script(*packages)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(self.installed.read_text().splitlines(), ["install", "app-meta-aria2", "install", "app-meta-foo+bar_1.2-3"])
self.assertEqual(self.configured.read_text().splitlines(), list(packages))
self.assertEqual((self.logs / "auto_setup.success").read_text().splitlines(), list(packages))
self.assertEqual((self.logs / "auto_setup.input").read_text().splitlines(), list(packages))
self.assertEqual((self.logs / "auto_setup.failed").read_text(), "")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,88 @@
-- Run against a staged source copy: lua tests/security_regression.lua ROOT
-- All LuCI, filesystem and command dependencies are mocked.
local root = assert(arg[1], "source root required")
local forms, response, executions = {}, nil, {}
local http = {
formvalue = function(key) return forms[key] end,
prepare_content = function() end,
write_json = function(value) response = value end,
}
local fs = { readfile = function() return "" end, unlink = function() end }
local nixio = { bit = { rshift = function(value, bits) return math.floor(value / 2^bits) end } }
local util = { shellquote = function(value) return "'" .. value:gsub("'", "'\\''") .. "'" end }
package.preload["luci.http"] = function() return http end
package.preload["luci.util"] = function() return util end
package.preload["luci.ltn12"] = function() return {} end
package.preload["nixio"] = function() return nixio end
package.preload["nixio.fs"] = function() return fs end
_G.luci = { http = http, util = util }
_G.nixio = nixio
local original_execute = os.execute
os.execute = function(command) executions[#executions+1] = command; return 0 end
assert(loadfile(root .. "/luasrc/controller/quickstart.lua"))()
local quickstart = package.loaded["luci.controller.quickstart"]
for _, packages in ipairs({"../other", "--help", "two packages", "pkg'", {"aria2", "../other"}, {"aria2", {}}}) do
forms, response, executions = {packages=packages}, nil, {}
quickstart.auto_setup()
assert(response and response.success ~= 0 and response.scope == "params", "invalid package accepted")
assert(#executions == 0, "invalid package reached shell")
end
for _, packages in ipairs({"aria2", {"qbittorrent", "foo+bar_1.2-3"}}) do
forms, response, executions = {packages=packages}, nil, {}
quickstart.auto_setup()
assert(response and response.success == 0 and #executions == 1, "normal package failed")
end
os.execute = original_execute
assert(loadfile(root .. "/luasrc/controller/istore_backend.lua"))()
local proxy = package.loaded["luci.controller.istore_backend"].istore_backend
local function upvalue(name)
for i=1,100 do
local key, value = debug.getupvalue(proxy, i)
if not key then break end
if key == name then return value end
end
error("missing proxy helper " .. name)
end
local sink = upvalue("sink_socket")
local sent = ""
assert(sink({sendall=function(_, chunk) sent=sent..chunk; return #chunk end})("whole body"))
assert(sent == "whole body", "proxy did not send complete input")
local chunksource = upvalue("chunksource")
local wire = "5\r\nhello\r\n5\r\nworld\r\n0\r\n\r\n"
for split=0,#wire do
local pos, calls = split+1, 0
local sock = {}
function sock:recv(n)
assert(n > 0, "zero-length receive")
calls = calls + 1
assert(calls < 100, "unbounded receive loop")
local data=wire:sub(pos, pos+math.min(n, 3)-1)
pos=pos+#data
return data
end
function sock:recvall(n)
local result=""
while #result<n do
local data=self:recv(n-#result)
if not data or data=="" then return nil, -1 end
result=result..data
end
return result
end
local source=chunksource(sock, wire:sub(1,split))
local output=""
for i=1,100 do
local data, err=source()
assert(not err, "chunk parse failed at split " .. split)
if not data then break end
output=output..data
end
assert(output=="helloworld", "chunk boundary corrupted body at " .. split)
end
local source=chunksource({recv=function() return "" end}, "incomplete")
assert(source()==nil, "EOF not handled")
source=chunksource({recv=function() error("must not read zero bytes") end}, string.rep("z",1024))
assert(source()==nil, "oversize chunk header accepted")
print("PASS: package validation, full proxy writes, all chunk boundaries, EOF and header limit")