The key concept of Safe Tcl is that there are two Tcl interpreters in the application, a trusted one and an untrusted (or "safe") one. The trusted interpreter can do anything, and it is used for the main application (e.g., the web browser or email user interface). When the main application receives a message containing an untrusted script, it evalutates that script in the context of the untrusted interpreter. The restricted nature of the untrusted interpreter means the application is safe from attack. This model is much like user-mode and kernel-mode in a multi-user operating system like UNIX or Windows/NT. In these systems, applications run in user mode and trap into the kernel to access resources like files and the network. The kernel implements access controls so that users cannot read and write each others files, or hijack network services.
The dual interpreter model of Safe Tcl has been generalised in Tcl 7.5 and made accesible to Tcl scripts. A Tcl script can create other interpreters, destroy them, create command aliases among them, share I/O channels among them, and evaluate scripts in them.
Creating Interpreters
Here is a simple example that creates an interpreter, evaluates a couple of commands in it, and then deletes the interpreter:
interp create foo
=> foo
interp eval foo {set a 5}
=> 5
set sum [interp eval foo {expr $a + $a}]
=> 10
interp delete fooIn Example 16-1 the interpreter is named foo. Two commands are evaluated in the foo interpreter:
set a 5
expr $a + $aNote that curly braces are used to protect the commands from any interpretation by the main interpreter. The variable a is defined in the foo interpreter and does not conflict with variables in the main interpreter. The set of variables and procedures in each interpreter is completely independent. Example X shows how to link variables between interpreters.
interp create foo
=> foo
interp eval foo {interp create bar}
=> bar
interp create {foo bar2}
=> foo bar2
interp slaves
=> foo
interp slaves foo
=> bar bar2interp delete bar
=> interpreter named "bar" not found
interp delete {foo bar}The example creates foo, and then it creates two children of foo. The first one is created by foo with this command:
interp eval foo {interp create bar}The second child is created by the main interpreter. In this case the grandchild must be named by a two-element list to indicate it is a child of a child. The same naming convention is used when the grandchild is deleted:
interp create {foo bar2}
interp delete {foo bar2}The interp slaves operation returns the names of child (i.e., slave) interpreters. The names are relative to their parent, so the slaves of foo are reported simply as bar and bar2. The name for the current interpreter is the empty list, or {}. This is useful in command aliases and file sharing described later. For security reasons, it is not possible to name the master interpreter from within the slave.
slave operation args ...
interp operation slave args ...For example, the following are equivalent commands:
foo eval {set a 5}
interp eval foo {set a 5}And so are these:
foo issafe
interp issafe fooHowever, the operations delete, exists, hide, expose, share, and transfer cannot be used this way. For example, there is no foo delete operation: you must use interp delete foo. If you have a deep hierarchy of interpreters, remember that the command corresponding to the slave is only defined in the parent. For example, if a master creates foo, and foo creates bar, then the master must operate on bar with the interp command. There would not be a foo\ bar command defined in the master. With all these exceptions, you may find it easier to just remember how to use the interp command.
interp eval slave [list set var $value]
interp alias slave cmd1 target cmd2 ?arg arg ...?This creates cmd1 in slave that is an alias for cmd2 in target. When cmd1 is invoked in slave, cmd2 is invoked in target. The alias mechanism is transparent to the slave. Whatever cmd2 returns, the slave sees as the return value of cmd1. If cmd2 raises and error, the error is propagated to the slave.
The arguments to cmd1 are passed to cmd2, after any additional arguments to cmd2 that were specified when the alias was created. These hidden arguments provide a safe way to pass extra arguments to an alias. For example, it is quite common to pass the name of the slave to the alias. In Example 16-3, exit in the interpreter foo is an alias that is implemented in the current interpreter (i.e., {}). When the slave executes exit, the master executes:
interp delete foo
interp create foo
interp alias foo exit {} interp delete foo
interp eval foo exit
# Child foo is gone.
proc Interp_ListAliases {name out} {
puts $out "Aliases for $name"
foreach alias [interp aliases $name] {
puts $out [format "%-20s => (%s) %s" $alias \
[interp target $name $alias] \
[interp alias $name $alias]]
}
}Example 16-4 generates output in a human readable format. Example 16-5 generates the aliases as Tcl commands that can be used to recreate them later:
proc Interp_DumpAliases {name out} {
puts $out "# Aliases for $name"
foreach alias [interp aliases $name] {
puts $out [format "interp alias %s %s %s %s" \
$name $alias [list [interp target $name $alias]] \
[interp alias $name $alias]]
}
}
interp create -safe untrustedAn interpreter is made safe by eliminating commands. Table 16-2 lists the commands removed from safe interpreters. It does not have commands to manipulate the file system and other programs (e.g., cd, open, and exec). This ensures that untrusted scripts cannot harm the host computer. The socket command is removed so untrusted scripts cannot access the network. The exit, source, and load commands are removed so an untrusted script cannot harm the hosting application. Note that commands like puts and gets are not removed. A safe interpreter can still do I/O, but it cannot create an I/O channel. We will show how to pass an I/O channel to a child interpreter on page 177.
In addition, a safe interpreter does not have any standard procedures from the Tcl script library. For example, there is no implementation of unknown to automatically load library procedures and packages. Libraries, packages, and the unknown command are described in Chapter 12. Restoring these functions in a slave is described in Example 16-6 on page 176.
The initial state of a safe interpreter is very safe, but it is too limited. The only thing a safe interpreter can do is compute a string and return that value to the parent. By creating command aliases, a master can give a safe interpreter controlled access to resources. A security policy implements a set of command aliases that add controlled capabilities to a safe interpreter. We willshow, for example, how to provided limited network and file system access to untrusted slaves.
Hidden Commands
The commands listed in Table 16-2 are hidden instead of being completely removed. A hidden command can be invoked in a slave by its master. For example, a master can load Tcl scripts into a slave by using its hidden source command:
interp create -safe slave
interp invokehidden slave source filenameWithout hidden commands the master has to do a bit more work to achieve the same thing. It must open and read the file and eval the contents of the file in the slave. File operations are described in Chapter 9.
interp create -safe slave
set in [open filename]
interp eval slave [read $in]
close $inHidden commands were added in Tcl 7.7 in order to better support the Tcl/Tk browser plugin described in Chapter 19. In some cases hidden commands are strictly necessary; it is not possible to simulate them any other way. The best examples are in the context of Safe Tk, where the master creates widgets or does potentially dangerous things on behalf of the slave. These will be discussed in more detail later.
A master can hide and expose commands using the interp hide and interp expose operations, respectively. You can even hide Tcl procedures. However, the commands inside the procedure run with the same privilege as the slave.
interp create -safe slave
interp hide slave clock
interp hide slave timeYou can remove them from the slave entirely like this:
interp eval slave [list rename clock {}]
interp eval slave [list rename time {}]On the otherhand, you might want to let slaves source scripts and use the unknown facility that automatically loads scripts. It can be argued that this is safe: even if an untrusted script loaded scripts that used dangerous commands, those commands would not be accessible to the slave. Attempts to use them would fail. However, by letting a slave use source, the slave can learn some things about your file system. It is up to you to decide if this risk is worth it. Example 16-6 gives unrestricted access to source, and it initializes the unknown command and package loading facilities by sourcing the standard init.tcl script. A more restricted solution is presented in Example X.:
proc Interp_AllowSource {slave} {
interp expose $slave source
interp expose $slave load
foreach varName {tcl_library auto_path} {
upvar #0 $varName var
interp eval $slave [list set $varName $var]
}
interp eval $slave \
[list source [file join $tcl_library init.tcl]]
}
With interp eval the command is subject to a complete round of parsing and substitutions in the target interpreter. This occurs after the parsing and substitutions for the interp eval command itself. In addition, if you pass several arguments to interp eval, those are concatenated before evaluation. This is similar to the way the eval command works as described on page 101. The most reliable way to use interp eval is to construct a list to ensure the command is well structured:
interp eval slave [list cmd arg1 arg2]With hidden commands, the command and arguments are taken directly from the arguments to interp invokehidden , and there are no substitutions done in the target interpreter. This means that the master has complete control over the command structure, and nothing funny can happen in the other interpreter. For this reason you should not create a list. If you do that, the whole list will be interpreted as the command name! Instead, just pass separate arguments to interp invokehidden and they are passed straight through to the target:
interp invokehidden slave command arg1 arg2With aliases, all the parsing and substitutions occur in the slave before the alias is invoked in the master. The alias implementation should never eval or subst any values it gets from the slave to avoid executing arbitrary code.
For example, suppose there is an alias to open files. The alias does some checking and then invokes the hidden open command. An untrusted script might pass [exit] as the name of the file to open in order to create mischief. The untrusted code is hoping that the master will accidentally eval the filename and cause the application to exit. This attack has nothing to do with opening files, it just hopes for a poor alias implementation. Example 16-7 shows an alias that is not subject to this attack:
interp alias slave open {} safeopen slaveproc safeopen {slave filename {mode r}} {
# do some checks, then...
interp invokehidden $slave open $filename $mode
}
interp eval slave {open \[exit\]}
# Opens file named "[exit]"The command in the slave starts out as:
open \[exit\]The master has to quote the brackets in its interp eval command or else the slave will try to invoke exit because of command substitution. Presumably exit isn't defined, or it is defined to terminate the slave. Once this quoting is done, the value of filename is [exit] and it is not subject to substitutions. It is safe to use $filename in the interp invokehidden command because it is only substituted once, in the master. The hidden open command also gets [exit] as its filename argument.
interp share interp1 chanName interp2
interp transfer interp1 chanName interp2In these commands, chanName exists in interp1 and is being shared or transferred to interp2. As with command aliases, if interp1 is the current interpreter, name it with {}.
The following example creates a temporary file for an unsafe interpreter. The file is opened for reading and writing, and the slave can use it to store data temporarily.
proc TempfileAlias {slave} {
set i 0
while {[file exists Temp$slave$i]} {
incr i
}
set out [open Temp$slave$i w+]
interp transfer {} $out $slave
return $out
}
proc TempfileExitAlias {slave} {
foreach file [glob -nocomplain Temp$slave*] {
# Platform independent file remove.
# This is part of Tcl 7.6.
file delete $file
}
interp delete $slave
}
interp create -safe foo
interp alias foo Tempfile {} TempfileAlias foo
interp alias foo exit {} TempFileExitAlias fooThe TempfileAlias procedure is invoked in the parent when the child interpreter invokes Tempfile. TempfileAlias returns the name of the open channel, and this becomes the return value from Tempfile so the child knows the name of the I/O channel. TempfileAlias uses interp transfer to pass the I/O channel to the child so the child has permission to access the I/O channel. In this simple example, it would also work to invoke the hidden open command to create the I/O channel directly in the slave.
Example 16-8 is not fully safe because the unsafe interpreter can still overflow the disk or create a million files. Because the parent has transferred the I/O channel to the child, it cannot easily monitor the I/O activity by the child. Example 16-10 addresses these issues.
Security Policies
A security policy defines what a safe interpreter can do. Designing security policies that are secure is difficult. If you design your own, make sure to have your colleges review the code. Give out prizes to folks that can break your policy. Good policy implementations are proven with lots of review and trial attacks. The good news is that Safe Tcl security policies can be implemented in relatively small amounts of Tcl code. This makes them easier to analyze and get correct. Here are a number of rules of thumb:
# The index is a host name, and the
# value is a list of port specifications, which can be
# an exact port number
# a lower bound on port number: N-
# a range of port numbers, inclusive: N-M
array set safesock {
sage.eng 3000-4000
www.sun.com 80
webcache.eng {80 8080}
bisque.eng 80 1025-
}
proc Safesock_PolicyInit {slave} {
interp alias $slave socket {} SafesockAlias $slave
}
proc SafesockAlias {slave host port} {
global safesock
if ![info exists safesock($host)] {
error "unknown host: $host"
}
foreach portspec $safesock($host) {
set low [set high ""]
if {[regexp {^([0-9]+)-([0-9]*)$ $portspec x low high]} {
if {($low <= $port && $high == "") ||
($low <= $port && $high >= $port)} {
set good $port
break
}
} elseif {$port == $portspec} {
set good $port
}
}
if [info exists goodport] {
set sock [interp invokehidden $slave socket $host $good]
interp invokehidden $slave fconfigure $sock -blocking 0
return $sock
}
error "bad port: $host"
}The policy is initialized with Safesock_PolicyInit. The name of this procedure follows a naming convention used by the Tcl/Tk browser plugin. In the plugin, untrusted scripts request a policy by name (e.g., Safesock) and the plugin implementation calls Safesock_PolicyInit to initialize the security policy. In this case, a single alias is installed. The alias gives the slave a socket command that is implemented by SafesockAlias in the master.
The alias checks for a port that matches one of the port specifications for the host. If a match is found then the invokehidden operation is used to invoke two commands in the slave. The socket command creates the network connection, and the fconfigure command puts the socket into non-blocking mode so read and gets by the slave do not block the application:
set sock [interp invokehidden $slave socket $host $good]
interp invokehidden $slave fconfigure $sock -blocking 0The socket alias in the slave does not conflict with the hidden socket command. There are two distinct sets of commands, hidden and exposed. It is quite common for the alias implementation to invoke the hidden command after various permission checks are made.
proc Tempfile_PolicyInit {slave directory maxfile maxsize} {
# directory is the location for the files
# maxfile is the number of files allowed in the directory
# maxsize is the max size for any single file.
interp alias $slave open {} \
TempfileOpenAlias $slave $directory $maxfile
# Override existing puts with an alias
interp alias $slave puts {} TempfilePutsAlias $slave $maxsize
interp alias $slave exit {} TempfileExitAlias $slave
}
proc TempfileOpenAlias {slave dir maxfile name {mode r}} {
# interpState records open files for each child
global interpState
# Ignore any leading pathname components
set name [file join $dir [file tail $name]]
# Limit the number of files
set files [glob -nocomplain [file join $dir *]]
set N [llength $files]
if {(N >= $maxfile) && ([lsearch -exact $files $name] < 0)} {
error "permission denied"
}
set out [open $name $mode]
lappend interpState(channels,$slave) $out
interp share {} $out $slave
return $out
}
proc TempfileExitAlias {slave} {
global interpState
interp delete $slave
if [info exists interpState(channels,$slave)] {
foreach out $interpState(channels,$slave) {
close $out
}
}
}
proc TempfilePutsAlias {slave max chan args} {
# max is the file size limit, in bytes
# chan is the I/O channel
# args is either a single string argument,
# or the -nonewline flag plus the string.
if {[llength $args] > 2} {
error "invalid arguments"
}
if {[llength $args] == 2} {
if {![string match -n* [lindex $argv 0]]} {
error "invalid arguments"
}
set string [lindex $args 1]
} else {
set string [lindex $args 0]\n
}
set size [expr [tell $chan] + [string length $string]]
if {$size > $max} {
error "File size exceeded"
} else {
puts -nonewline $chan $string
}
}The TempfileAlias procedure is generalized in Example 16-10 to have several parameters that specify the directory, name, and a limit to the number of files allowed. The directory and maxfile limit are part of the alias definition. Their existence is transparent to the slave. The slave only specifies the name and access mode (i.e., for reading or writing.) The Tempfile policy could be used by different slave interreters with different parameters.
The master is careful to restrict the files to the specified directory. It uses file tail to strip off any leading pathname components that the slave might specify.
The master and slave share the I/O channel. The name of the I/O channel is recorded in interpState, and TempfileExitAlias uses this information to close the channel when the child interpreter is deleted. This is necessary because both parent and child have a reference to the channel when it is shared. The child's reference is automatically removed when the interpreter is deleted, but the parent must close its own reference.
interp alias slave tell {} interp invokehidden tell
Example 16-11 defines an alias that implements after on behalf of safe interpreters. The basic idea is to carefully check the arguments, and then do the after in the parent interpreter. As an additional feature, the number of outstanding after events is limited. The master keeps a record of each after event scheduled. Two id's are associated with each event. One chosen by the master (i.e., myid), and the other chosen by the after command (i.e., id). The master keeps a map from myid to id. The map serves two puposes. The number of map entries counts the number of outstanding events. The map also hides the real after ID from the slave, which prevents a slave from attempting mischief by specifying invalid after IDs to after cancel. The SafeAfterCallback is the procedure scheduled. It maintains state and then invokes the original callback in the slave.
# SafeAfter_PolicyInit creates a child with
# a safe after command
proc SafeAfter_PolicyInit {slave max} {
# max limits the number of outstanding after events
global interpState
interp alias $slave after {} SafeAfterAlias $slave $max
interp alias $slave exit {} SafeAfterExitAlias $slave
# This is used to generate after IDs for the slave.
set interpState(id,$slave) 0
}
# SafeAfterAlias is an alias for after. It disallows after
# with only a time argument and no command.
proc SafeAfterAlias {slave max args} {
global interpState
set argc [llength $args]
if {$argc == 0} {
error "Usage: after option args"
}
switch -- [lindex $args 0] {
cancel {
# A naive implementation would just
# eval after cancel $args
# but something dangerous could be hiding in args.
set myid [lindex $args 1]
if {[info exists interpState(id,$slave,$myid)]} {
set id $interpState(id,$slave,$myid)
unset interpState(id,$slave,$myid)
after cancel $id
}
return ""
}
default {
if {$argc == 1} {
error "Usage: after time command args..."
}
if {[llength [array names interpState id,$slave,*]]\
>= $max} {
error "Too many after events"
}
# Maintain concat semantics
set command [concat [lrange $args 1 end]]
# Compute our own id to pass the callback.
set myid after#[incr interpState(id,$slave)]
set id [after [lindex $args 0] \
[list SafeAfterCallback $slave $myid $command]]
set interpState(id,$slave,$myid) $id
return $myid
}
}
}
# SafeAfterCallback is the after callback in the trusted code.
# It evaluates its command in the safe interpreter.
proc SafeAfterCallback {slave myid cmd} {
global interpState
unset interpState(id,$slave,$myid)
if [catch {
interp eval $slave $cmd
} err] {
catch {interp eval $slave bgerror $error}
}
}
# SafeAfterExitAlias is an alias for exit that does cleanup.
proc SafeAfterExitAlias {slave} {
global interpState
foreach id [array names interpState id,$slave,*] {
after cancel $interpState($id)
}
interp delete $slave
unset interpState
}