util-sbx.lua /size: 20 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['util-sbx'] = {
2    version   = 1.001,
3    comment   = "companion to luat-lib.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Note: we use expandname and collapsepath and these use chdir
10-- which is overloaded so we need to use originals there. Just
11-- something to keep in mind.
12
13if not sandbox then require("l-sandbox") end -- for testing
14
15local next, type = next, type
16
17local replace        = utilities.templates.replace
18local collapsepath   = file.collapsepath
19local expandname     = dir.expandname
20local sortedhash     = table.sortedhash
21local lpegmatch      = lpeg.match
22local platform       = os.type
23local P, S, C        = lpeg.P, lpeg.S, lpeg.C
24local gsub           = string.gsub
25local lower          = string.lower
26local find           = string.find
27local concat         = string.concat
28local unquoted       = string.unquoted
29local optionalquoted = string.optionalquoted
30local basename       = file.basename
31local nameonly       = file.nameonly
32
33local sandbox        = sandbox
34local validroots     = { }
35local validrunners   = { }
36local validbinaries  = true -- all permitted
37local validlibraries = true -- all permitted
38local validators     = { }
39local finalized      = nil
40local trace          = false
41
42local p_validroot    = nil
43local p_split        = lpeg.firstofsplit(" ")
44
45local report         = logs.reporter("sandbox")
46
47trackers.register("sandbox",function(v) trace = v end) -- often too late anyway
48
49sandbox.setreporter(report)
50
51sandbox.finalizer {
52    category = "files",
53    action   = function()
54        finalized = true
55    end
56}
57
58local function registerroot(root,what) -- what == read|write
59    if finalized then
60        report("roots are already finalized")
61    else
62        if type(root) == "table" then
63            root, what = root[1], root[2]
64        end
65        if type(root) == "string" and root ~= "" then
66            root = collapsepath(expandname(root))
67         -- if platform == "windows" then
68         --     root = lower(root) -- we assume ascii names
69         -- end
70            if what == "r" or what == "ro" or what == "readable" then
71                what = "read"
72            elseif what == "w" or what == "wo" or what == "writable" then
73                what = "write"
74            end
75            -- true: read & write | false: read
76            validroots[root] = what == "write" or false
77        end
78    end
79end
80
81sandbox.finalizer {
82    category = "files",
83    action   = function() -- initializers can set the path
84        if p_validroot then
85            report("roots are already initialized")
86        else
87            sandbox.registerroot(".","write") -- always ok
88            -- also register texmf as read
89            for name in sortedhash(validroots) do
90                if p_validroot then
91                    p_validroot = P(name) + p_validroot
92                else
93                    p_validroot = P(name)
94                end
95            end
96            p_validroot = p_validroot / validroots
97        end
98    end
99}
100
101local function registerbinary(name)
102    if finalized then
103        report("binaries are already finalized")
104    elseif type(name) == "string" and name ~= "" then
105        if not validbinaries then
106            return
107        end
108        if validbinaries == true then
109            validbinaries = { [name] = true }
110        else
111            validbinaries[name] = true
112        end
113    elseif name == true then
114        validbinaries = { }
115    end
116end
117
118local function registerlibrary(name)
119    if finalized then
120        report("libraries are already finalized")
121    elseif type(name) == "string" and name ~= "" then
122        if not validlibraries then
123            return
124        end
125        if validlibraries == true then
126            validlibraries = { [nameonly(name)] = true }
127        else
128            validlibraries[nameonly(name)] = true
129        end
130    elseif name == true then
131        validlibraries = { }
132    end
133end
134
135-- begin of validators
136
137local p_write = S("wa")       p_write = (1 - p_write)^0 * p_write
138local p_path  = S("\\/~$%:")  p_path  = (1 - p_path )^0 * p_path  -- be easy on other arguments
139
140local function normalized(name) -- only used in executers
141    if platform == "windows" then
142        name = gsub(name,"/","\\")
143    end
144    return name
145end
146
147function sandbox.possiblepath(name)
148    return lpegmatch(p_path,name) and true or false
149end
150
151local filenamelogger = false
152
153function sandbox.setfilenamelogger(l)
154    filenamelogger = type(l) == "function" and l or false
155end
156
157local function validfilename(name,what)
158    if p_validroot and type(name) == "string" and lpegmatch(p_path,name) then
159        local asked = collapsepath(expandname(name))
160     -- if platform == "windows" then
161     --     asked = lower(asked) -- we assume ascii names
162     -- end
163        local okay = lpegmatch(p_validroot,asked)
164        if okay == true then
165            -- read and write access
166            if filenamelogger then
167                filenamelogger(name,"w",asked,true)
168            end
169            return name
170        elseif okay == false then
171            -- read only access
172            if not what then
173                -- no further argument to io.open so a readonly case
174                if filenamelogger then
175                    filenamelogger(name,"r",asked,true)
176                end
177                return name
178            elseif lpegmatch(p_write,what) then
179                if filenamelogger then
180                    filenamelogger(name,"w",asked,false)
181                end
182                return -- we want write access
183            else
184                if filenamelogger then
185                    filenamelogger(name,"r",asked,true)
186                end
187                return name
188            end
189        elseif filenamelogger then
190            filenamelogger(name,"*",name,false)
191        end
192    else
193        return name
194    end
195end
196
197local function readable(name,finalized)
198--     if platform == "windows" then -- yes or no
199--         name = lower(name) -- we assume ascii names
200--     end
201    return validfilename(name,"r")
202end
203
204local function normalizedreadable(name,finalized)
205--     if platform == "windows" then -- yes or no
206--         name = lower(name) -- we assume ascii names
207--     end
208    local valid = validfilename(name,"r")
209    if valid then
210        return normalized(valid)
211    end
212end
213
214local function writeable(name,finalized)
215--     if platform == "windows" then
216--         name = lower(name) -- we assume ascii names
217--     end
218    return validfilename(name,"w")
219end
220
221local function normalizedwriteable(name,finalized)
222--     if platform == "windows" then
223--         name = lower(name) -- we assume ascii names
224--     end
225    local valid = validfilename(name,"w")
226    if valid then
227        return normalized(valid)
228    end
229end
230
231validators.readable            = readable
232validators.writeable           = normalizedwriteable
233validators.normalizedreadable  = normalizedreadable
234validators.normalizedwriteable = writeable
235validators.filename            = readable
236
237table.setmetatableindex(validators,function(t,k)
238    if k then
239        t[k] = readable
240    end
241    return readable
242end)
243
244-- function validators.verbose(s)
245--     return s
246-- end
247
248function validators.string(s,finalized)
249    -- can be used to prevent filename checking (todo: only when registered)
250    if finalized and suspicious(s) then
251        return ""
252    else
253        return s
254    end
255end
256
257function validators.cache(s)
258    if finalized then
259        return basename(s)
260    else
261        return s
262    end
263end
264
265function validators.url(s)
266    if finalized and find("^file:") then
267        return ""
268    else
269        return s
270    end
271end
272
273-- end of validators
274
275local function filehandlerone(action,one,...)
276    local checkedone = validfilename(one)
277    if checkedone then
278        return action(one,...)
279    else
280     -- report("file %a is unreachable",one)
281    end
282end
283
284local function filehandlertwo(action,one,two,...)
285    local checkedone = validfilename(one)
286    if checkedone then
287        local checkedtwo = validfilename(two)
288        if checkedtwo then
289            return action(one,two,...)
290        else
291         -- report("file %a is unreachable",two)
292        end
293    else
294     -- report("file %a is unreachable",one)
295    end
296end
297
298local function iohandler(action,one,...)
299    if type(one) == "string" then
300        local checkedone = validfilename(one)
301        if checkedone then
302            return action(one,...)
303        end
304    elseif one then
305        return action(one,...)
306    else
307        return action()
308    end
309end
310
311-- runners can be strings or tables
312--
313-- os.execute : string
314-- os.exec    : string or table with program in [0|1] -- no longer there
315-- os.spawn   : string or table with program in [0|1] -- no longer there
316--
317-- our execute: registered program with specification
318
319local osexecute = sandbox.original(os.execute)
320local iopopen   = sandbox.original(io.popen)
321local reported  = { }
322
323local function validcommand(name,program,template,checkers,defaults,variables,reporter,strict)
324    if validbinaries ~= false and (validbinaries == true or validbinaries[program]) then
325        local binpath = nil
326        if variables then
327            for variable, value in next, variables do
328                local chktype = checkers[variable]
329                if chktype == "verbose" then
330                    -- for now, we will have a "flags" checker
331                else
332                    local checker = validators[chktype]
333                    if checker and type(value) == "string" then
334                        value = checker(unquoted(value),strict)
335                        if value then
336                            variables[variable] = optionalquoted(value)
337                        else
338                            report("variable %a with value %a fails the check",variable,value)
339                            return
340                        end
341                    else
342                        report("variable %a has no checker",variable)
343                        return
344                    end
345                end
346            end
347            for variable, default in next, defaults do
348                local value = variables[variable]
349                if not value or value == "" then
350                    local chktype = checkers[variable]
351                    if chktype == "verbose" then
352                        -- for now, we will have a "flags" checker
353                    elseif type(default) == "string" then
354                        local checker = validators[chktype]
355                        if checker then
356                            default = checker(unquoted(default),strict)
357                            if default then
358                                variables[variable] = optionalquoted(default)
359                            else
360                                report("variable %a with default %a fails the check",variable,default)
361                                return
362                            end
363                        end
364                    end
365                end
366            end
367            binpath = variables.binarypath
368        end
369        if type(binpath) == "string" and binpath ~= "" then
370            -- this works on the console but not from e.g. scite
371         -- program = '"' .. binpath .. "/" .. program .. '"'
372            program = binpath .. "/" .. program
373        end
374        local command = program .. " " .. replace(template,variables)
375        if reporter then
376            reporter("executing runner %a: %s",name,command)
377        elseif trace then
378            report("executing runner %a: %s",name,command)
379        end
380        return command
381    elseif not reported[name] then
382        report("executing program %a of runner %a is not permitted",program,name)
383        reported[name] = true
384    end
385end
386
387local runners = {
388    --
389    -- name,program,template,checkers,variables,reporter
390    --
391    resultof = function(...)
392        local command = validcommand(...)
393        if command then
394            if trace then
395                report("resultof: %s",command)
396            end
397            local handle = iopopen(command,"rb") -- already has flush
398            if handle then
399                local result = handle:read("*all") or ""
400                handle:close()
401                return result
402            end
403        end
404    end,
405    execute = function(...)
406        local command = validcommand(...)
407        if command then
408            if trace then
409                report("execute: %s",command)
410            end
411            local okay = osexecute(command)
412            return okay
413        end
414    end,
415    pipeto = function(...)
416        local command = validcommand(...)
417        if command then
418            if trace then
419                report("pipeto: %s",command)
420            end
421            return iopopen(command,"w") -- already has flush
422        end
423    end,
424}
425
426function sandbox.registerrunner(specification)
427    if type(specification) == "string" then
428        local wrapped = validrunners[specification]
429        inspect(table.sortedkeys(validrunners))
430        if wrapped then
431            return wrapped
432        else
433            report("unknown predefined runner %a",specification)
434            return
435        end
436    end
437    if type(specification) ~= "table" then
438        report("specification should be a table (or string)")
439        return
440    end
441    local name = specification.name
442    if type(name) ~= "string" then
443        report("invalid name, string expected",name)
444        return
445    end
446    if validrunners[name] then
447        report("invalid name, runner %a already defined",name)
448        return
449    end
450    local program = specification.program
451    if type(program) == "string" then
452        -- common for all platforms
453    elseif type(program) == "table" then
454        program = program[platform] or program.default or program.unix
455    end
456    if type(program) ~= "string" or program == "" then
457        report("invalid runner %a specified for platform %a",name,platform)
458        return
459    end
460    local template = specification.template
461    if not template then
462        report("missing template for runner %a",name)
463        return
464    end
465    local method   = specification.method   or "execute"
466    local checkers = specification.checkers or { }
467    local defaults = specification.defaults or { }
468    local runner   = runners[method]
469    if runner then
470        local finalized = finalized -- so, the current situation is frozen
471        local wrapped = function(variables)
472            return runner(name,program,template,checkers,defaults,variables,specification.reporter,finalized)
473        end
474        validrunners[name] = wrapped
475        return wrapped
476    else
477        validrunners[name] = nil
478        report("invalid method for runner %a",name)
479    end
480end
481
482function sandbox.getrunner(name)
483    return name and validrunners[name]
484end
485
486local function suspicious(str)
487    return (find(str,"[/\\]") or find(command,"..",1,true)) and true or false
488end
489
490local function binaryrunner(action,command,...)
491    if validbinaries == false then
492        -- nothing permitted
493        report("no binaries permitted, ignoring command: %s",command)
494        return
495    end
496    if type(command) ~= "string" then
497        -- we only handle strings, maybe some day tables
498        report("command should be a string")
499        return
500    end
501    local program = lpegmatch(p_split,command)
502    if not program or program == "" then
503        report("unable to filter binary from command: %s",command)
504        return
505    end
506    if validbinaries == true then
507        -- everything permitted
508    elseif not validbinaries[program] then
509        report("binary not permitted, ignoring command: %s",command)
510        return
511    elseif suspicious(command) then
512        report("/ \\ or .. found, ignoring command (use sandbox.registerrunner): %s",command)
513        return
514    end
515    return action(command,...)
516end
517
518-- local function binaryrunner(action,command,...)
519--     local original = command
520--     if validbinaries == false then
521--         -- nothing permitted
522--         report("no binaries permitted, ignoring command: %s",command)
523--         return
524--     end
525--     local program
526--     if type(command) == "table" then
527--         program = command[0]
528--         if program then
529--             command = concat(command," ",0)
530--         else
531--             program = command[1]
532--             if program then
533--                 command = concat(command," ")
534--             end
535--         end
536--     elseif type(command) = "string" then
537--         program = lpegmatch(p_split,command)
538--     else
539--         report("command should be a string or table")
540--         return
541--     end
542--     if not program or program == "" then
543--         report("unable to filter binary from command: %s",command)
544--         return
545--     end
546--     if validbinaries == true then
547--         -- everything permitted
548--     elseif not validbinaries[program] then
549--         report("binary not permitted, ignoring command: %s",command)
550--         return
551--     elseif find(command,"[/\\]") or find(command,"%.%.") then
552--         report("/ \\ or .. found, ignoring command (use sandbox.registerrunner): %s",command)
553--         return
554--     end
555--     return action(original,...)
556-- end
557
558local function dummyrunner(action,command,...)
559    if type(command) == "table" then
560        command = concat(command," ",command[0] and 0 or 1)
561    end
562    report("ignoring command: %s",command)
563end
564
565sandbox.filehandlerone = filehandlerone
566sandbox.filehandlertwo = filehandlertwo
567sandbox.iohandler      = iohandler
568
569function sandbox.disablerunners()
570    validbinaries = false
571end
572
573function sandbox.disablelibraries()
574    validlibraries = false
575end
576
577if FFISUPPORTED and ffi then
578
579    function sandbox.disablelibraries()
580        validlibraries = false
581        for k, v in next, ffi do
582            if k ~= "gc" then
583                ffi[k] = nil
584            end
585        end
586    end
587
588    local fiiload = ffi.load
589
590    if fiiload then
591
592        local reported = { }
593
594        function ffi.load(name,...)
595            if validlibraries == false then
596                -- all blocked
597            elseif validlibraries == true then
598                -- all permitted
599                return fiiload(name,...)
600            elseif validlibraries[nameonly(name)] then
601                -- 'name' permitted
602                return fiiload(name,...)
603            else
604                -- 'name' not permitted
605            end
606            if not reported[name] then
607                report("using library %a is not permitted",name)
608                reported[name] = true
609            end
610            return nil
611        end
612
613    end
614
615end
616
617-------------------
618
619local overload = sandbox.overload
620local register = sandbox.register
621
622    overload(loadfile,             filehandlerone,"loadfile") -- todo
623
624if io then
625    overload(io.open,              filehandlerone,"io.open")
626    overload(io.popen,             binaryrunner,  "io.popen")
627    overload(io.input,             iohandler,     "io.input")
628    overload(io.output,            iohandler,     "io.output")
629    overload(io.lines,             filehandlerone,"io.lines")
630end
631
632if os then
633    overload(os.execute,           binaryrunner,  "os.execute")
634    overload(os.spawn,             dummyrunner,   "os.spawn")    -- no longer there
635    overload(os.exec,              dummyrunner,   "os.exec")     -- no longer there
636    overload(os.resultof,          binaryrunner,  "os.resultof")
637    overload(os.pipeto,            binaryrunner,  "os.pipeto")
638    overload(os.rename,            filehandlertwo,"os.rename")
639    overload(os.remove,            filehandlerone,"os.remove")
640end
641
642if lfs then
643    overload(lfs.chdir,            filehandlerone,"lfs.chdir")
644    overload(lfs.mkdir,            filehandlerone,"lfs.mkdir")
645    overload(lfs.rmdir,            filehandlerone,"lfs.rmdir")
646    overload(lfs.isfile,           filehandlerone,"lfs.isfile")
647    overload(lfs.isdir,            filehandlerone,"lfs.isdir")
648    overload(lfs.attributes,       filehandlerone,"lfs.attributes")
649    overload(lfs.dir,              filehandlerone,"lfs.dir")
650    overload(lfs.lock_dir,         filehandlerone,"lfs.lock_dir")
651    overload(lfs.touch,            filehandlerone,"lfs.touch")
652    overload(lfs.link,             filehandlertwo,"lfs.link")
653    overload(lfs.setmode,          filehandlerone,"lfs.setmode")
654    overload(lfs.readlink,         filehandlerone,"lfs.readlink")
655    overload(lfs.shortname,        filehandlerone,"lfs.shortname")
656    overload(lfs.symlinkattributes,filehandlerone,"lfs.symlinkattributes")
657end
658
659-- these are used later on
660
661if zip then
662    zip.open = register(zip.open, filehandlerone,"zip.open")
663end
664
665sandbox.registerroot    = registerroot
666sandbox.registerbinary  = registerbinary
667sandbox.registerlibrary = registerlibrary
668sandbox.validfilename   = validfilename
669
670-- not used in a normal mkiv run : os.spawn = os.execute
671-- not used in a normal mkiv run : os.exec  = os.exec
672
673-- print(io.open("test.log"))
674-- sandbox.enable()
675-- print(io.open("test.log"))
676-- print(io.open("t:/test.log"))
677