diff options
Diffstat (limited to 'tex/context/base/mkiv/util-sbx.lua')
-rw-r--r-- | tex/context/base/mkiv/util-sbx.lua | 529 |
1 files changed, 388 insertions, 141 deletions
diff --git a/tex/context/base/mkiv/util-sbx.lua b/tex/context/base/mkiv/util-sbx.lua index 260e8b3b5..66a650875 100644 --- a/tex/context/base/mkiv/util-sbx.lua +++ b/tex/context/base/mkiv/util-sbx.lua @@ -23,19 +23,23 @@ local platform = os.type local P, S, C = lpeg.P, lpeg.S, lpeg.C local gsub = string.gsub local lower = string.lower +local find = string.find +local concat = string.concat local unquoted = string.unquoted local optionalquoted = string.optionalquoted +local basename = file.basename local sandbox = sandbox local validroots = { } local validrunners = { } -local validbinaries = { } +local validbinaries = true -- all permitted +local validlibraries = true -- all permitted local validators = { } -local p_validroot = nil local finalized = nil -local norunners = false local trace = false -local p_split = lpeg.tsplitat(" ") -- more spaces? + +local p_validroot = nil +local p_split = lpeg.firstofsplit(" ") local report = logs.reporter("sandbox") @@ -43,69 +47,87 @@ trackers.register("sandbox",function(v) trace = v end) -- often too late anyway sandbox.setreporter(report) -sandbox.finalizer(function() - finalized = true -end) +sandbox.finalizer { + category = "files", + action = function() + finalized = true + end +} local function registerroot(root,what) -- what == read|write if finalized then report("roots are already finalized") else - root = collapsepath(expandname(root)) - if platform == "windows" then - root = lower(root) -- we assume ascii names + if type(root) == "table" then + root, what = root[1], root[2] + end + if type(root) == "string" and root ~= "" then + root = collapsepath(expandname(root)) + -- if platform == "windows" then + -- root = lower(root) -- we assume ascii names + -- end + if what == "r" or what == "ro" or what == "readable" then + what = "read" + elseif what == "w" or what == "wo" or what == "writable" then + what = "write" + end + -- true: read & write | false: read + validroots[root] = what == "write" or false end - -- true: read & write | false: read - validroots[root] = what == "write" or false end end -sandbox.finalizer(function() -- initializers can set the path - if p_validroot then - report("roots are already initialized") - else - sandbox.registerroot(".","write") -- always ok - -- also register texmf as read - for name in sortedhash(validroots) do - if p_validroot then - p_validroot = P(name) + p_validroot - else - p_validroot = P(name) +sandbox.finalizer { + category = "files", + action = function() -- initializers can set the path + if p_validroot then + report("roots are already initialized") + else + sandbox.registerroot(".","write") -- always ok + -- also register texmf as read + for name in sortedhash(validroots) do + if p_validroot then + p_validroot = P(name) + p_validroot + else + p_validroot = P(name) + end end + p_validroot = p_validroot / validroots end - p_validroot = p_validroot / validroots end -end) +} -local function registerrunner(specification) +local function registerbinary(name) if finalized then - report("runners are already finalized") - else - local name = specification.name - if not name then - report("no runner name specified") + report("binaries are already finalized") + elseif type(name) == "string" and name ~= "" then + if not validbinaries then return end - local program = specification.program - if type(program) == "string" then - -- common for all platforms - elseif type(program) == "table" then - program = program[platform] - end - if type(program) ~= "string" or program == "" then - report("invalid runner %a specified for platform %a",name,platform) - return + if validbinaries == true then + validbinaries = { [name] = true } + else + validbinaries[name] = true end - specification.program = program - validrunners[name] = specification + elseif name == true then + validbinaries = { } end end -local function registerbinary(name) +local function registerlibrary(name) if finalized then - report("binaries are already finalized") + report("libraries are already finalized") elseif type(name) == "string" and name ~= "" then - validbinaries[name] = true + if not validlibraries then + return + end + if validlibraries == true then + validlibraries = { [name] = true } + else + validlibraries[name] = true + end + elseif name == true then + validlibraries = { } end end @@ -134,9 +156,9 @@ end local function validfilename(name,what) if p_validroot and type(name) == "string" and lpegmatch(p_path,name) then local asked = collapsepath(expandname(name)) - if platform == "windows" then - asked = lower(asked) -- we assume ascii names - end + -- if platform == "windows" then + -- asked = lower(asked) -- we assume ascii names + -- end local okay = lpegmatch(p_validroot,asked) if okay == true then -- read and write access @@ -163,39 +185,53 @@ local function validfilename(name,what) end return name end - else - if filenamelogger then - filenamelogger(name,"*",name,false) - end + elseif filenamelogger then + filenamelogger(name,"*",name,false) end else return name end end -local function readable(name) - if platform == "windows" then - name = lower(name) -- we assume ascii names - end +local function readable(name,finalized) +-- if platform == "windows" then -- yes or no +-- name = lower(name) -- we assume ascii names +-- end + return validfilename(name,"r") +end + +local function normalizedreadable(name,finalized) +-- if platform == "windows" then -- yes or no +-- name = lower(name) -- we assume ascii names +-- end local valid = validfilename(name,"r") if valid then return normalized(valid) end end -local function writeable(name) - if platform == "windows" then - name = lower(name) -- we assume ascii names - end +local function writeable(name,finalized) +-- if platform == "windows" then +-- name = lower(name) -- we assume ascii names +-- end + return validfilename(name,"w") +end + +local function normalizedwriteable(name,finalized) +-- if platform == "windows" then +-- name = lower(name) -- we assume ascii names +-- end local valid = validfilename(name,"w") if valid then return normalized(valid) end end -validators.readable = readable -validators.writeable = writeable -validators.filename = readable +validators.readable = readable +validators.writeable = normalizedwriteable +validators.normalizedreadable = normalizedreadable +validators.normalizedwriteable = writeable +validators.filename = readable table.setmetatableindex(validators,function(t,k) if k then @@ -204,23 +240,39 @@ table.setmetatableindex(validators,function(t,k) return readable end) -function validators.string(s) - return s -- can be used to prevent filename checking +function validators.string(s,finalized) + -- can be used to prevent filename checking (todo: only when registered) + if finalized and suspicious(s) then + return "" + else + return s + end end --- end of validators +function validators.cache(s) + if finalized then + return basename(s) + else + return s + end +end + +function validators.url(s) + if finalized and find("^file:") then + return "" + else + return s + end +end -sandbox.registerroot = registerroot -sandbox.registerrunner = registerrunner -sandbox.registerbinary = registerbinary -sandbox.validfilename = validfilename +-- end of validators local function filehandlerone(action,one,...) local checkedone = validfilename(one) if checkedone then return action(one,...) else --- report("file %a is unreachable",one) + -- report("file %a is unreachable",one) end end @@ -231,10 +283,10 @@ local function filehandlertwo(action,one,two,...) if checkedtwo then return action(one,two,...) else --- report("file %a is unreachable",two) + -- report("file %a is unreachable",two) end else --- report("file %a is unreachable",one) + -- report("file %a is unreachable",one) end end @@ -254,101 +306,289 @@ end -- runners can be strings or tables -- -- os.execute : string --- os.exec : table with program in [0|1] --- os.spawn : table with program in [0|1] +-- os.exec : string or table with program in [0|1] +-- os.spawn : string or table with program in [0|1] -- -- our execute: registered program with specification -local function runhandler(action,name,specification) - local kind = type(name) - if kind ~= "string" then +local osexecute = sandbox.original(os.execute) +local iopopen = sandbox.original(io.popen) +local reported = { } + +local function validcommand(name,program,template,checkers,defaults,variables,reporter,strict) + if validbinaries ~= false and (validbinaries == true or validbinaries[program]) then + if variables then + for variable, value in next, variables do + local checker = validators[checkers[variable]] + if checker then + value = checker(unquoted(value),strict) + if value then + variables[variable] = optionalquoted(value) + else + report("variable %a with value %a fails the check",variable,value) + return + end + else + report("variable %a has no checker",variable) + return + end + end + for variable, default in next, defaults do + local value = variables[variable] + if not value or value == "" then + local checker = validators[checkers[variable]] + if checker then + default = checker(unquoted(default),strict) + if default then + variables[variable] = optionalquoted(default) + else + report("variable %a with default %a fails the check",variable,default) + return + end + end + end + end + end + local command = program .. " " .. replace(template,variables) + if reporter then + reporter("executing runner %a: %s",name,command) + elseif trace then + report("executing runner %a: %s",name,command) + end + return command + elseif not reported[name] then + report("executing program %a of runner %a is not permitted",program,name) + reported[name] = true + end +end + +local runners = { + -- + -- name,program,template,checkers,variables,reporter + -- + resultof = function(...) + local command = validcommand(...) + if command then + if trace then + report("resultof: %s",command) + end + local handle = iopopen(command,"r") -- already has flush + if handle then + local result = handle:read("*all") or "" + handle:close() + return result + end + end + end, + execute = function(...) + local command = validcommand(...) + if command then + if trace then + report("execute: %s",command) + end + return osexecute(command) + end + end, + pipeto = function(...) + local command = validcommand(...) + if command then + if trace then + report("pipeto: %s",command) + end + return iopopen(command,"w") -- already has flush + end + end, +} + +function sandbox.registerrunner(specification) + if type(specification) == "string" then + local wrapped = validrunners[specification] + inspect(table.sortedkeys(validrunners)) + if wrapped then + return wrapped + else + report("unknown predefined runner %a",specification) + return + end + end + if type(specification) ~= "table" then + report("specification should be a table (or string)") return end - if norunners then - report("no runners permitted, ignoring command: %s",name) + local name = specification.name + if type(name) ~= "string" then + report("invalid name, string expected",name) return end - local spec = validrunners[name] - if not spec then - report("unknown runner: %s",name) + if validrunners[name] then + report("invalid name, runner %a already defined") return end - -- specs are already checked - local program = spec.program - local variables = { } - local checkers = spec.checkers or { } - if specification then - -- we only handle runners that are defined before the sandbox is - -- closed so in principle we cannot have user runs with no files - -- while for context runners we assume a robust specification - for k, v in next, specification do - local checker = validators[checkers[k]] - local value = checker(unquoted(v)) -- todo: write checkers - if value then - variables[k] = optionalquoted(value) - else - report("suspicious argument found, run blocked: %s",v) - return - end - end + local program = specification.program + if type(program) == "string" then + -- common for all platforms + elseif type(program) == "table" then + program = program[platform] or program.default or program.unix + end + if type(program) ~= "string" or program == "" then + report("invalid runner %a specified for platform %a",name,platform) + return end - local command = replace(program,variables) - if trace then - report("executing runner: %s",command) + local template = specification.template + if not template then + report("missing template for runner %a",name) + return end - return action(command) + local method = specification.method or "execute" + local checkers = specification.checkers or { } + local defaults = specification.defaults or { } + local runner = runners[method] + if runner then + local finalized = finalized -- so, the current situation is frozen + local wrapped = function(variables) + return runner(name,program,template,checkers,defaults,variables,specification.reporter,finalized) + end + validrunners[name] = wrapped + return wrapped + else + validrunners[name] = nil + report("invalid method for runner %a",name) + end +end + +function sandbox.getrunner(name) + return name and validrunners[name] end --- only registered (from list) -- no checking on writable so let's assume harmless --- runs +local function suspicious(str) + return (find(str,"[/\\]") or find(command,"%.%.")) and true or false +end -local function binaryhandler(action,name) - local kind = type(name) - local list = name - if kind == "string" then - list = lpegmatch(p_split,name) +local function binaryrunner(action,command,...) + if validbinaries == false then + -- nothing permitted + report("no binaries permitted, ignoring command: %s",command) + return end - local program = name[0] or name[1] - if type(program) ~= "string" or program == "" then - return --silently ignore + if type(command) ~= "string" then + -- we only handle strings, maybe some day tables + report("command should be a string") + return end - if norunners then - report("no binaries permitted, ignoring command: %s",program) + local program = lpegmatch(p_split,command) + if not program or program == "" then + report("unable to filter binary from command: %s",command) return end - if not validbinaries[program] then - report("binary is not permitted: %s",program) + if validbinaries == true then + -- everything permitted + elseif not validbinaries[program] then + report("binary not permitted, ignoring command: %s",command) + return + elseif suspicious(command) then + report("/ \\ or .. found, ignoring command (use sandbox.registerrunner): %s",command) return end - for i=0,#list do - local n = list[i] - if n then - local v = readable(unquoted(n)) - if v then - list[i] = optionalquoted(v) - else - report("suspicious argument found, run blocked: %s",n) - return - end - end + return action(command,...) +end + +-- local function binaryrunner(action,command,...) +-- local original = command +-- if validbinaries == false then +-- -- nothing permitted +-- report("no binaries permitted, ignoring command: %s",command) +-- return +-- end +-- local program +-- if type(command) == "table" then +-- program = command[0] +-- if program then +-- command = concat(command," ",0) +-- else +-- program = command[1] +-- if program then +-- command = concat(command," ") +-- end +-- end +-- elseif type(command) = "string" then +-- program = lpegmatch(p_split,command) +-- else +-- report("command should be a string or table") +-- return +-- end +-- if not program or program == "" then +-- report("unable to filter binary from command: %s",command) +-- return +-- end +-- if validbinaries == true then +-- -- everything permitted +-- elseif not validbinaries[program] then +-- report("binary not permitted, ignoring command: %s",command) +-- return +-- elseif find(command,"[/\\]") or find(command,"%.%.") then +-- report("/ \\ or .. found, ignoring command (use sandbox.registerrunner): %s",command) +-- return +-- end +-- return action(original,...) +-- end + +local function dummyrunner(action,command,...) + if type(command) == "table" then + command = concat(command," ",command[0] and 0 or 1) end - return action(name) + report("ignoring command: %s",command) end sandbox.filehandlerone = filehandlerone sandbox.filehandlertwo = filehandlertwo sandbox.iohandler = iohandler -sandbox.runhandler = runhandler -sandbox.binaryhandler = binaryhandler function sandbox.disablerunners() - norunners = true + validbinaries = false end -local execute = sandbox.original(os.execute) +function sandbox.disablelibraries() + validlibraries = false +end + +if FFISUPPORTED and ffi then + + function sandbox.disablelibraries() + validlibraries = false + for k, v in next, ffi do + if k ~= "gc" then + ffi[k] = nil + end + end + end + + local load = ffi.load + + if load then + + local reported = { } + + function ffi.load(name,...) + if validlibraries == false then + -- all blocked + elseif validlibraries == true then + -- all permitted + return load(name,...) + elseif validlibraries[name] then + -- 'name' permitted + return load(name,...) + else + -- 'name' not permitted + end + if not reported[name] then + report("using library %a is not permitted",name) + reported[name] = true + end + return nil + end + + end -function sandbox.run(name,specification) - return runhandler(execute,name,specification) end ------------------- @@ -360,16 +600,18 @@ local register = sandbox.register if io then overload(io.open, filehandlerone,"io.open") - overload(io.popen, filehandlerone,"io.popen") + overload(io.popen, binaryrunner, "io.popen") overload(io.input, iohandler, "io.input") overload(io.output, iohandler, "io.output") overload(io.lines, filehandlerone,"io.lines") end if os then - overload(os.execute, binaryhandler, "os.execute") - overload(os.spawn, binaryhandler, "os.spawn") - overload(os.exec, binaryhandler, "os.exec") + overload(os.execute, binaryrunner, "os.execute") + overload(os.spawn, dummyrunner, "os.spawn") + overload(os.exec, dummyrunner, "os.exec") + overload(os.resultof, binaryrunner, "os.resultof") + overload(os.pipeto, binaryrunner, "os.pipeto") overload(os.rename, filehandlertwo,"os.rename") overload(os.remove, filehandlerone,"os.remove") end @@ -406,6 +648,11 @@ if epdf then epdf.open = register(epdf.open, filehandlerone,"epdf.open") end +sandbox.registerroot = registerroot +sandbox.registerbinary = registerbinary +sandbox.registerlibrary = registerlibrary +sandbox.validfilename = validfilename + -- not used in a normal mkiv run : os.spawn = os.execute -- not used in a normal mkiv run : os.exec = os.exec |