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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
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]+$/
# - namespace: 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]
# generate the script to exfiltrate data from the manifest
variable mf_required {
name 1
namespace {[regexp {^[_a-zA-Z0-9-]+$} $val]}
slug {[regexp {^[_a-zA-Z0-9-]+$} $val]}
version {$val != ""} }
variable mf_optional {
description 1
author 1
license 1 }
variable mf_procs {
}
variable manifest_reader {
set manifest_dict {}
}
foreach {key check} $mf_required {
set chunk {
if {[info exists %key]} {
set val [set %key]
if %check {
dict set manifest_dict %key [set %key]
} else { return -code error "invalid value for %key: $val" }
} else { return -code error "missing key in manifest: %key" }
}
regsub -all "%key" $chunk [list $key] chunk
regsub -all "%check" $chunk [list $check] chunk
append manifest_reader $chunk
}
foreach {key check} $mf_optional {
set chunk {
if {[info exists %key]} {
set val [set %key]
if %check {
dict set manifest_dict %key [set %key]
}
}
}
regsub -all "%key" $chunk [list $key] chunk
regsub -all "%check" $chunk [list $check] chunk
append manifest_reader $chunk
}
foreach {key _} $mf_procs {
set chunk {
if {%key in [info procs %key]} {
dict set manifest_dict procs %key [list [info args %key] [info body %key]]
}
}
regsub -all "%key" $chunk [list $key] chunk
append manifest_reader $chunk
}
append manifest_reader { set manifest_dict }
# generation done
proc vercmp {v1 v2} {
# normalize the lists, check for equality
if {[list {*}$v1] == [list {*}$v2]} { return 0 }
# compare every element
for {set n 0} {$n < min([llength $v1], [llength $v2])} {incr n} {
if [set cmp [string compare [lindex $v1 $n] [lindex $v2 $n]]] {
return $cmp
}
}
# they can't be equal, so the longer one must be the newer one
if {[llength $v1] > [llength $v2]} { return 1 }
return -1
}
proc enroll_interp {tid iid} {}
proc load {dir {allow_internal 0}} {
variable log
variable manifest_reader
${log}::info "attempting to load plugin from \"$dir\"..."
set mf_path [file join $dir 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
interp hide mf_exec interp
# 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] + 2}]
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 mf_valid [catch { interp eval mf_exec $manifest_reader } result opts]
if {$mf_valid != 0} {
${log}::error "couldn't validate plugin: $result"
return 0
}
# hooray, we have the manifest!
set manifest $result
interp delete mf_exec
} result opts]
switch -- $untrusted_result {
0 { } 2 { return -code 2 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 PLUGIN SANDBOX COMPROMISE!"
return 0
}
}
set pl_name [dict get $manifest name]
set pl_namespace [dict get $manifest namespace]
set pl_slug [dict get $manifest slug]
set pl_version [dict get $manifest version]
${log}::debug "manifest loaded: ${pl_name} (${pl_namespace}::${pl_slug}) v[join ${pl_version} .]"
set stored [core_db eval {
SELECT hashes, version, trusted FROM plugins
WHERE slug = $pl_slug
AND namespace = $pl_namespace}]
if ![llength $stored] {
puts "new plugin"
} else {
lassign $stored st_hashes st_version st_trusted
switch -- [vercmp $pl_version $st_version] {
0 { puts "same version" }
-1 { puts "older version" }
1 { puts "newer version" }
}
}
}
}