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
10
11
12
13
14
15if not sandbox then require("l-sandbox") end
16
17local next, type = next, type
18
19local replace = utilities.templates.replace
20local collapsepath = file.collapsepath
21local expandname = dir.expandname
22local sortedhash = table.sortedhash
23local lpegmatch = lpeg.match
24local platform = os.type
25local P, S, C = lpeg.P, lpeg.S, lpeg.C
26local gsub = string.gsub
27local lower = string.lower
28local find = string.find
29local concat = string.concat
30local unquoted = string.unquoted
31local optionalquoted = string.optionalquoted
32local basename = file.basename
33local nameonly = file.nameonly
34
35local sandbox = sandbox
36local validroots = { }
37local validrunners = { }
38local validbinaries = true
39local validlibraries = true
40local validators = { }
41local finalized = nil
42local trace = false
43
44local p_validroot = nil
45local p_split = lpeg.firstofsplit(" ")
46
47local report = logs.reporter("sandbox")
48
49trackers.register("sandbox",function(v) trace = v end)
50
51sandbox.setreporter(report)
52
53sandbox.finalizer {
54 category = "files",
55 action = function()
56 finalized = true
57 end
58}
59
60local function registerroot(root,what)
61 if finalized then
62 report("roots are already finalized")
63 else
64 if type(root) == "table" then
65 root, what = root[1], root[2]
66 end
67 if type(root) == "string" and root ~= "" then
68 root = collapsepath(expandname(root))
69 if what == "r" or what == "ro" or what == "readable" then
70 what = "read"
71 elseif what == "w" or what == "wo" or what == "writable" then
72 what = "write"
73 end
74
75 validroots[root] = what == "write" or false
76 end
77 end
78end
79
80sandbox.finalizer {
81 category = "files",
82 action = function()
83 if p_validroot then
84 report("roots are already initialized")
85 else
86 sandbox.registerroot(".","write")
87
88 for name in sortedhash(validroots) do
89 if p_validroot then
90 p_validroot = P(name) + p_validroot
91 else
92 p_validroot = P(name)
93 end
94 end
95 p_validroot = p_validroot / validroots
96 end
97 end
98}
99
100local function registerbinary(name)
101 if finalized then
102 report("binaries are already finalized")
103 elseif type(name) == "string" and name ~= "" then
104 if not validbinaries then
105 return
106 end
107 if validbinaries == true then
108 validbinaries = { [name] = true }
109 else
110 validbinaries[name] = true
111 end
112 elseif name == true then
113 validbinaries = { }
114 end
115end
116
117local function registerlibrary(name)
118 if finalized then
119 report("libraries are already finalized")
120 elseif type(name) == "string" and name ~= "" then
121 if not validlibraries then
122 return
123 end
124 if validlibraries == true then
125 validlibraries = { [nameonly(name)] = true }
126 else
127 validlibraries[nameonly(name)] = true
128 end
129 elseif name == true then
130 validlibraries = { }
131 end
132end
133
134
135
136local p_write = S("wa") p_write = (1 - p_write)^0 * p_write
137local p_path = S("\\/~$%:") p_path = (1 - p_path )^0 * p_path
138
139local function normalized(name)
140 if platform == "windows" then
141 name = gsub(name,"/","\\")
142 end
143 return name
144end
145
146function sandbox.possiblepath(name)
147 return lpegmatch(p_path,name) and true or false
148end
149
150local filenamelogger = false
151
152function sandbox.setfilenamelogger(l)
153 filenamelogger = type(l) == "function" and l or false
154end
155
156local function validfilename(name,what)
157 if p_validroot and type(name) == "string" and lpegmatch(p_path,name) then
158 local asked = collapsepath(expandname(name))
159
160
161
162 local okay = lpegmatch(p_validroot,asked)
163 if okay == true then
164
165 if filenamelogger then
166 filenamelogger(name,"w",asked,true)
167 end
168 return name
169 elseif okay == false then
170
171 if not what then
172
173 if filenamelogger then
174 filenamelogger(name,"r",asked,true)
175 end
176 return name
177 elseif lpegmatch(p_write,what) then
178 if filenamelogger then
179 filenamelogger(name,"w",asked,false)
180 end
181 return
182 else
183 if filenamelogger then
184 filenamelogger(name,"r",asked,true)
185 end
186 return name
187 end
188 elseif filenamelogger then
189 filenamelogger(name,"*",name,false)
190 end
191 else
192 return name
193 end
194end
195
196local function readable(name,finalized)
197
198
199
200 return validfilename(name,"r")
201end
202
203local function normalizedreadable(name,finalized)
204
205
206
207 local valid = validfilename(name,"r")
208 if valid then
209 return normalized(valid)
210 end
211end
212
213local function writeable(name,finalized)
214
215
216
217 return validfilename(name,"w")
218end
219
220local function normalizedwriteable(name,finalized)
221
222
223
224 local valid = validfilename(name,"w")
225 if valid then
226 return normalized(valid)
227 end
228end
229
230validators.readable = readable
231validators.writeable = normalizedwriteable
232validators.normalizedreadable = normalizedreadable
233validators.normalizedwriteable = writeable
234validators.filename = readable
235
236table.setmetatableindex(validators,function(t,k)
237 if k then
238 t[k] = readable
239 end
240 return readable
241end)
242
243
244
245
246
247function validators.string(s,finalized)
248
249 if finalized and suspicious(s) then
250 return ""
251 else
252 return s
253 end
254end
255
256function validators.cache(s)
257 if finalized then
258 return basename(s)
259 else
260 return s
261 end
262end
263
264function validators.url(s)
265 if finalized and find("^file:") then
266 return ""
267 else
268 return s
269 end
270end
271
272
273
274local function filehandlerone(action,one,...)
275 local checkedone = validfilename(one)
276 if checkedone then
277 return action(one,...)
278 else
279
280 end
281end
282
283local function filehandlertwo(action,one,two,...)
284 local checkedone = validfilename(one)
285 if checkedone then
286 local checkedtwo = validfilename(two)
287 if checkedtwo then
288 return action(one,two,...)
289 else
290
291 end
292 else
293
294 end
295end
296
297local function iohandler(action,one,...)
298 if type(one) == "string" then
299 local checkedone = validfilename(one)
300 if checkedone then
301 return action(one,...)
302 end
303 elseif one then
304 return action(one,...)
305 else
306 return action()
307 end
308end
309
310
311
312
313
314
315
316
317
318local osexecute = sandbox.original(os.execute)
319local iopopen = sandbox.original(io.popen)
320local reported = { }
321
322local function validcommand(name,program,template,checkers,defaults,variables,reporter,strict)
323 if validbinaries ~= false and (validbinaries == true or validbinaries[program]) then
324 local binpath = nil
325 if variables then
326 for variable, value in next, variables do
327 local chktype = checkers[variable]
328 if chktype == "verbose" then
329
330 else
331 local checker = validators[chktype]
332 if checker and type(value) == "string" then
333 value = checker(unquoted(value),strict)
334 if value then
335 variables[variable] = optionalquoted(value)
336 else
337 report("variable %a with value %a fails the check",variable,value)
338 return
339 end
340 else
341 report("variable %a has no checker",variable)
342 return
343 end
344 end
345 end
346 for variable, default in next, defaults do
347 local value = variables[variable]
348 if not value or value == "" then
349 local chktype = checkers[variable]
350 if chktype == "verbose" then
351
352 elseif type(default) == "string" then
353 local checker = validators[chktype]
354 if checker then
355 default = checker(unquoted(default),strict)
356 if default then
357 variables[variable] = optionalquoted(default)
358 else
359 report("variable %a with default %a fails the check",variable,default)
360 return
361 end
362 end
363 end
364 end
365 end
366 binpath = variables.binarypath
367 end
368 if type(binpath) == "string" and binpath ~= "" then
369
370
371 program = binpath .. "/" .. program
372 end
373 local command = program .. " " .. replace(template,variables)
374 if reporter then
375 reporter("executing runner %a: %s",name,command)
376 elseif trace then
377 report("executing runner %a: %s",name,command)
378 end
379 return command
380 elseif not reported[name] then
381 report("executing program %a of runner %a is not permitted",program,name)
382 reported[name] = true
383 end
384end
385
386local runners = {
387
388
389
390 resultof = function(...)
391 local command = validcommand(...)
392 if command then
393 if trace then
394 report("resultof: %s",command)
395 end
396 local handle = iopopen(command,"r")
397 if handle then
398 local result = handle:read("*all") or ""
399 handle:close()
400 return result
401 end
402 end
403 end,
404 execute = function(...)
405 local command = validcommand(...)
406 if command then
407 if trace then
408 report("execute: %s",command)
409 end
410 local okay = osexecute(command)
411 return okay
412 end
413 end,
414 pipeto = function(...)
415 local command = validcommand(...)
416 if command then
417 if trace then
418 report("pipeto: %s",command)
419 end
420
421 return iopopen(command)
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
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 internal = specification.internal
478 local runner = runners[method]
479 if runner then
480 local finalized = finalized
481 local internalized = false
482 local wrapped = function(variables)
483 if internal and not internalized then
484
485
486 internal = internal(specification)
487 if type(internal) ~= "function" then
488 internal = false
489 end
490 internalized = true
491 end
492 return (internal or runner)(name,program,template,checkers,defaults,variables,specification.reporter,finalized)
493 end
494 validrunners[name] = wrapped
495 return wrapped
496 else
497 validrunners[name] = nil
498 report("invalid method for runner %a",name)
499 end
500end
501
502function sandbox.getrunner(name)
503 return name and validrunners[name]
504end
505
506local function suspicious(str)
507 return (find(str,"[/\\]") or find(command,"..",1,true)) and true or false
508end
509
510local function binaryrunner(action,command,...)
511 if validbinaries == false then
512
513 report("no binaries permitted, ignoring command: %s",command)
514 return
515 end
516 if type(command) ~= "string" then
517
518 report("command should be a string")
519 return
520 end
521 local program = lpegmatch(p_split,command)
522 if not program or program == "" then
523 report("unable to filter binary from command: %s",command)
524 return
525 end
526 if validbinaries == true then
527
528 elseif not validbinaries[program] then
529 report("binary not permitted, ignoring command: %s",command)
530 return
531 elseif suspicious(command) then
532 report("/ \\ or .. found, ignoring command (use sandbox.registerrunner): %s",command)
533 return
534 end
535 return action(command,...)
536end
537
538local function dummyrunner(action,command,...)
539 if type(command) == "table" then
540 command = concat(command," ",command[0] and 0 or 1)
541 end
542 report("ignoring command: %s",command)
543end
544
545sandbox.filehandlerone = filehandlerone
546sandbox.filehandlertwo = filehandlertwo
547sandbox.iohandler = iohandler
548
549do
550
551 local library = optional.library
552 local foreign = optional.foreign
553 local reported = { }
554 local libraryload = library.load
555 local foreignload = foreign.load
556
557 function library.load(name,...)
558 if validlibraries == false then
559
560 elseif validlibraries == true then
561
562 return libraryload(name,...)
563 elseif validlibraries[nameonly(name)] then
564
565 return libraryload(name,...)
566 else
567
568 end
569 if not reported[name] then
570 report("using library %a is not permitted",name)
571 reported[name] = true
572 end
573 return nil
574 end
575
576 function foreign.load(name,...)
577 if validlibraries == false then
578
579 elseif validlibraries == true then
580
581 return foreignload(name,...)
582 elseif validlibraries[nameonly(name)] then
583
584 return foreignload(name,...)
585 else
586
587 end
588 if not reported[name] then
589 report("using foreign %a is not permitted",name)
590 reported[name] = true
591 end
592 return nil
593 end
594
595
596
597 function sandbox.disablelibraries()
598 validlibraries = false
599 library.load = function() end
600 foreign.load = function() end
601 end
602
603 function sandbox.disablerunners()
604 validbinaries = false
605 end
606
607end
608
609
610
611local overload = sandbox.overload
612local register = sandbox.register
613
614 overload(loadfile, filehandlerone,"loadfile")
615
616if io then
617 overload(io.open, filehandlerone,"io.open")
618 overload(io.popen, binaryrunner, "io.popen")
619 overload(io.input, iohandler, "io.input")
620 overload(io.output, iohandler, "io.output")
621 overload(io.lines, filehandlerone,"io.lines")
622end
623
624if os then
625 overload(os.execute, binaryrunner, "os.execute")
626 overload(os.spawn, dummyrunner, "os.spawn")
627 overload(os.exec, dummyrunner, "os.exec")
628 overload(os.resultof, binaryrunner, "os.resultof")
629 overload(os.pipeto, binaryrunner, "os.pipeto")
630 overload(os.rename, filehandlertwo,"os.rename")
631 overload(os.remove, filehandlerone,"os.remove")
632end
633
634if lfs then
635 overload(lfs.chdir, filehandlerone,"lfs.chdir")
636 overload(lfs.mkdir, filehandlerone,"lfs.mkdir")
637 overload(lfs.rmdir, filehandlerone,"lfs.rmdir")
638 overload(lfs.isfile, filehandlerone,"lfs.isfile")
639 overload(lfs.isdir, filehandlerone,"lfs.isdir")
640 overload(lfs.attributes, filehandlerone,"lfs.attributes")
641 overload(lfs.dir, filehandlerone,"lfs.dir")
642 overload(lfs.lock_dir, filehandlerone,"lfs.lock_dir")
643 overload(lfs.touch, filehandlerone,"lfs.touch")
644 overload(lfs.link, filehandlertwo,"lfs.link")
645 overload(lfs.setmode, filehandlerone,"lfs.setmode")
646 overload(lfs.readlink, filehandlerone,"lfs.readlink")
647 overload(lfs.shortname, filehandlerone,"lfs.shortname")
648 overload(lfs.symlinkattributes,filehandlerone,"lfs.symlinkattributes")
649end
650
651
652
653if zip then
654 zip.open = register(zip.open, filehandlerone,"zip.open")
655end
656
657sandbox.registerroot = registerroot
658sandbox.registerbinary = registerbinary
659sandbox.registerlibrary = registerlibrary
660sandbox.validfilename = validfilename
661
662
663
664
665
666
667
668
669 |