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
13if not sandbox then require("l-sandbox") end
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
37local validlibraries = true
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)
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)
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
68
69
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
76 validroots[root] = what == "write" or false
77 end
78 end
79end
80
81sandbox.finalizer {
82 category = "files",
83 action = function()
84 if p_validroot then
85 report("roots are already initialized")
86 else
87 sandbox.registerroot(".","write")
88
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
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
139
140local function normalized(name)
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
161
162
163 local okay = lpegmatch(p_validroot,asked)
164 if okay == true then
165
166 if filenamelogger then
167 filenamelogger(name,"w",asked,true)
168 end
169 return name
170 elseif okay == false then
171
172 if not what then
173
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
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
199
200
201 return validfilename(name,"r")
202end
203
204local function normalizedreadable(name,finalized)
205
206
207
208 local valid = validfilename(name,"r")
209 if valid then
210 return normalized(valid)
211 end
212end
213
214local function writeable(name,finalized)
215
216
217
218 return validfilename(name,"w")
219end
220
221local function normalizedwriteable(name,finalized)
222
223
224
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
245
246
247
248function validators.string(s,finalized)
249
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
274
275local function filehandlerone(action,one,...)
276 local checkedone = validfilename(one)
277 if checkedone then
278 return action(one,...)
279 else
280
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
292 end
293 else
294
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
312
313
314
315
316
317
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
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
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
371
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
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,"r")
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")
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
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
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
493 report("no binaries permitted, ignoring command: %s",command)
494 return
495 end
496 if type(command) ~= "string" then
497
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
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
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
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
597 elseif validlibraries == true then
598
599 return fiiload(name,...)
600 elseif validlibraries[nameonly(name)] then
601
602 return fiiload(name,...)
603 else
604
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")
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")
635 overload(os.exec, dummyrunner, "os.exec")
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
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
671
672
673
674
675
676
677 |