From 1c9eee37d59cadc5728509594b0fc666f3fa510c Mon Sep 17 00:00:00 2001 From: aleteoryx Date: Fri, 8 Nov 2024 15:13:52 -0500 Subject: [PATCH] manifest loading basics --- main.tcl | 2 + plugins.tcl | 82 +++++++++++++++++++++++++++++++++++++++-- testplugin/manifest.tcl | 3 ++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 testplugin/manifest.tcl diff --git a/main.tcl b/main.tcl index 12f8749..6bc03bc 100755 --- a/main.tcl +++ b/main.tcl @@ -43,6 +43,8 @@ source plugins.tcl start_thread irc start_thread ui +plugins::load "$path[file separator]testplugin" + ${log}::debug "opening initial window..." thread::send [t::ns tclircc::ui] {mk_toplevel name; return $name} initial ${log}::debug "initial window opened: $initial" diff --git a/plugins.tcl b/plugins.tcl index d9cdae8..efd3f00 100644 --- a/plugins.tcl +++ b/plugins.tcl @@ -1,16 +1,92 @@ package require sha256 # plugin manifest format: -# tcl script that must return the following +# 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 } +# {Alice P Hacker }} +# - 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} { - set mf_fd [open "$dir[file separator]manifest.tcl"] + 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 + } + } } } diff --git a/testplugin/manifest.tcl b/testplugin/manifest.tcl new file mode 100644 index 0000000..848c682 --- /dev/null +++ b/testplugin/manifest.tcl @@ -0,0 +1,3 @@ +set name "test plugin" +set slug "test" +set version {0 0 1} -- 2.45.2