util-sbx.lua /size: 20 Kb    last modification: 2025-02-21 11:03
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    command = function(...)
425        local command = validcommand(...)
426        if command then
427            if trace then
428                report("command: %s",command)
429            end
430            return command
431        end
432    end,
433}
434
435function sandbox.registerrunner(specification)
436    if type(specification) == "string" then
437        local wrapped = validrunners[specification]
438        inspect(table.sortedkeys(validrunners))
439        if wrapped then
440            return wrapped
441        else
442            report("unknown predefined runner %a",specification)
443            return
444        end
445    end
446    if type(specification) ~= "table" then
447        report("specification should be a table (or string)")
448        return
449    end
450    local name = specification.name
451    if type(name) ~= "string" then
452        report("invalid name, string expected",name)
453        return
454    end
455    if validrunners[name] then
456        report("invalid name, runner %a already defined",name)
457        return
458    end
459    local program = specification.program
460    if type(program) == "string" then
461        -- common for all platforms
462    elseif type(program) == "table" then
463        program = program[platform] or program.default or program.unix
464    end
465    if type(program) ~= "string" or program == "" then
466        report("invalid runner %a specified for platform %a",name,platform)
467        return
468    end
469    local template = specification.template
470    if not template then
471        report("missing template for runner %a",name)
472        return
473    end
474    local method   = specification.method   or "execute"
475    local checkers = specification.checkers or { }
476    local defaults = specification.defaults or { }
477    local runner   = runners[method]
478    if runner then
479        local finalized = finalized -- so, the current situation is frozen
480        local wrapped = function(variables)
481            return runner(name,program,template,checkers,defaults,variables,specification.reporter,finalized)
482        end
483        validrunners[name] = wrapped
484        return wrapped
485    else
486        validrunners[name] = nil
487        report("invalid method for runner %a",name)
488    end
489end
490
491function sandbox.getrunner(name)
492    return name and validrunners[name]
493end
494
495local function suspicious(str)
496    return (find(str,"[/\\]") or find(command,"..",1,true)) and true or false
497end
498
499local function binaryrunner(action,command,...)
500    if validbinaries == false then
501        -- nothing permitted
502        report("no binaries permitted, ignoring command: %s",command)
503        return
504    end
505    if type(command) ~= "string" then
506        -- we only handle strings, maybe some day tables
507        report("command should be a string")
508        return
509    end
510    local program = lpegmatch(p_split,command)
511    if not program or program == "" then
512        report("unable to filter binary from command: %s",command)
513        return
514    end
515    if validbinaries == true then
516        -- everything permitted
517    elseif not validbinaries[program] then
518        report("binary not permitted, ignoring command: %s",command)
519        return
520    elseif suspicious(command) then
521        report("/ \\ or .. found, ignoring command (use sandbox.registerrunner): %s",command)
522        return
523    end
524    return action(command,...)
525end
526
527-- local function binaryrunner(action,command,...)
528--     local original = command
529--     if validbinaries == false then
530--         -- nothing permitted
531--         report("no binaries permitted, ignoring command: %s",command)
532--         return
533--     end
534--     local program
535--     if type(command) == "table" then
536--         program = command[0]
537--         if program then
538--             command = concat(command," ",0)
539--         else
540--             program = command[1]
541--             if program then
542--                 command = concat(command," ")
543--             end
544--         end
545--     elseif type(command) = "string" then
546--         program = lpegmatch(p_split,command)
547--     else
548--         report("command should be a string or table")
549--         return
550--     end
551--     if not program or program == "" then
552--         report("unable to filter binary from command: %s",command)
553--         return
554--     end
555--     if validbinaries == true then
556--         -- everything permitted
557--     elseif not validbinaries[program] then
558--         report("binary not permitted, ignoring command: %s",command)
559--         return
560--     elseif find(command,"[/\\]") or find(command,"%.%.") then
561--         report("/ \\ or .. found, ignoring command (use sandbox.registerrunner): %s",command)
562--         return
563--     end
564--     return action(original,...)
565-- end
566
567local function dummyrunner(action,command,...)
568    if type(command) == "table" then
569        command = concat(command," ",command[0] and 0 or 1)
570    end
571    report("ignoring command: %s",command)
572end
573
574sandbox.filehandlerone = filehandlerone
575sandbox.filehandlertwo = filehandlertwo
576sandbox.iohandler      = iohandler
577
578function sandbox.disablerunners()
579    validbinaries = false
580end
581
582function sandbox.disablelibraries()
583    validlibraries = false
584end
585
586if FFISUPPORTED and ffi then
587
588    function sandbox.disablelibraries()
589        validlibraries = false
590        for k, v in next, ffi do
591            if k ~= "gc" then
592                ffi[k] = nil
593            end
594        end
595    end
596
597    local fiiload = ffi.load
598
599    if fiiload then
600
601        local reported = { }
602
603        function ffi.load(name,...)
604            if validlibraries == false then
605                -- all blocked
606            elseif validlibraries == true then
607                -- all permitted
608                return fiiload(name,...)
609            elseif validlibraries[nameonly(name)] then
610                -- 'name' permitted
611                return fiiload(name,...)
612            else
613                -- 'name' not permitted
614            end
615            if not reported[name] then
616                report("using library %a is not permitted",name)
617                reported[name] = true
618            end
619            return nil
620        end
621
622    end
623
624end
625
626-------------------
627
628local overload = sandbox.overload
629local register = sandbox.register
630
631    overload(loadfile,             filehandlerone,"loadfile") -- todo
632
633if io then
634    overload(io.open,              filehandlerone,"io.open")
635    overload(io.popen,             binaryrunner,  "io.popen")
636    overload(io.input,             iohandler,     "io.input")
637    overload(io.output,            iohandler,     "io.output")
638    overload(io.lines,             filehandlerone,"io.lines")
639end
640
641if os then
642    overload(os.execute,           binaryrunner,  "os.execute")
643    overload(os.spawn,             dummyrunner,   "os.spawn")    -- no longer there
644    overload(os.exec,              dummyrunner,   "os.exec")     -- no longer there
645    overload(os.resultof,          binaryrunner,  "os.resultof")
646    overload(os.pipeto,            binaryrunner,  "os.pipeto")
647    overload(os.rename,            filehandlertwo,"os.rename")
648    overload(os.remove,            filehandlerone,"os.remove")
649end
650
651if lfs then
652    overload(lfs.chdir,            filehandlerone,"lfs.chdir")
653    overload(lfs.mkdir,            filehandlerone,"lfs.mkdir")
654    overload(lfs.rmdir,            filehandlerone,"lfs.rmdir")
655    overload(lfs.isfile,           filehandlerone,"lfs.isfile")
656    overload(lfs.isdir,            filehandlerone,"lfs.isdir")
657    overload(lfs.attributes,       filehandlerone,"lfs.attributes")
658    overload(lfs.dir,              filehandlerone,"lfs.dir")
659    overload(lfs.lock_dir,         filehandlerone,"lfs.lock_dir")
660    overload(lfs.touch,            filehandlerone,"lfs.touch")
661    overload(lfs.link,             filehandlertwo,"lfs.link")
662    overload(lfs.setmode,          filehandlerone,"lfs.setmode")
663    overload(lfs.readlink,         filehandlerone,"lfs.readlink")
664    overload(lfs.shortname,        filehandlerone,"lfs.shortname")
665    overload(lfs.symlinkattributes,filehandlerone,"lfs.symlinkattributes")
666end
667
668-- these are used later on
669
670if zip then
671    zip.open = register(zip.open, filehandlerone,"zip.open")
672end
673
674sandbox.registerroot    = registerroot
675sandbox.registerbinary  = registerbinary
676sandbox.registerlibrary = registerlibrary
677sandbox.validfilename   = validfilename
678
679-- not used in a normal mkiv run : os.spawn = os.execute
680-- not used in a normal mkiv run : os.exec  = os.exec
681
682-- print(io.open("test.log"))
683-- sandbox.enable()
684-- print(io.open("test.log"))
685-- print(io.open("t:/test.log"))
686