1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package require sha256
# plugin manifest format:
# tcl script that must set the following variables at a minimum:
# - name: string
# - slug: string
# must match /^[-_a-zA-Z0-9]+$/
# - version: string[]
# ordered like {1} > {0}, {0 1} < {1 0}. {1 1} > {1}
# may set:
# - description: string
# - author: string[]
# should contain a list of names with optional website or email, e.g.
# {{Aleteoryx <https://aleteoryx.me>}
# {Alice P Hacker <aph@example.com>}}
# - license: string
# may begin with spdx: to indicate an spdx id or file: to indicate
# a file path relative to the plugin root. .. and root references are
# stripped. if neither prefix applies, assumed to be literal license
# text.
namespace eval plugins {
variable plugins
variable log [logger::init tclircc::plugins]
proc load {dir} {
variable log
${log}::info "attempting to load plugin from \"$dir\"..."
set mf_path "$dir[file separator]manifest.tcl"
if {!([file isfile $mf_path] && [file readable $mf_path])} {
${log}::error "couldn't load plugin: manifest \"$mf_path\" not present or not readable!"
return 0
}
set mf_fd [open $mf_path]
set manifest [read $mf_fd]
close $mf_fd
interp create -safe mf_exec
# prevent escapes
interp hide mf_exec package
# prevent hanging the thread
interp hide mf_exec vwait
interp hide mf_exec after
interp hide mf_exec update
interp hide mf_exec gets
interp hide mf_exec read
# this is more than enough for any reasonable manifest
interp limit mf_exec time -seconds [expr {[clock seconds] + 1}]
interp limit mf_exec command -value 10000
# untrusted code time
${log}::debug "evaluating manifest..."
set untrusted_result [catch {
set mf_result [catch { interp eval mf_exec $manifest } result opts]
if {$mf_result != 0} {
${log}::error "couldn't load plugin: manifest return code $mf_result: $result"
return 0
}
set has_required [interp eval mf_exec {expr {[info exists name] &&
[info exists slug] &&
[info exists version]}}]
if {!$has_required} {
${log}::error "couldn't load plugin: missing required values in manifest"
return 0
}
set name [interp eval mf_exec {set name}]
set slug [interp eval mf_exec {set slug}]
set version [interp eval mf_exec {set version}]
${log}::debug "manifest loaded: $name ($slug) v[join $version .]"
} result opts]
switch -- $untrusted_result {
0 { return result } 2 { return result }
1 {
${log}::error "couldn't load plugin: $result"
return 0
}
default {
${log}::alert "unexpected return code from plugin handling: $untrusted_result"
${log}::alert "return options: $opts"
${log}::alert "THIS MAY INDICATE A SANDBOX COMPROMISE!"
return 0
}
}
}
}