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 evaluates 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. In Safe-Tcl the application implements access controls for untrusted scripts.
The dual interpreter model of Safe-Tcl has been generalized in Tcl 7.5 and made accessible 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 17-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.
interp create foo
=> foo
interp eval foo {interp create bar}
=> bar
interp create {foo bar2}
=> foo bar2
interp slaves
=> foo
interp slaves foo
=> bar bar2
interp 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, share, slaves, target, and transfer cannot be used with the per interpreter command. In particular, there is no foo delete operation; you must use interp delete foo.
If you have a deep hierarchy of interpreters, 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 is no "foo bar" command defined in the master.
interp eval slave [list set var $value]
interp create -safe untrusted
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 will show, for example, how to provide limited network and file system access to untrusted slaves. Tcl provides a framework to manage several security policies, which is described later.
Command Aliases
A command alias is a command in one interpreter that is implemented by a command in another interpreter. The master interpreter installs command aliases in its slaves. The command to create an alias has the following general form:
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 an error, the error is propagated to the slave.
Name the current interpreter with {}.
If target is the current interpreter, name it with {}. The empty list is the way to name yourself as the interpreter. This is the most common case, although target could be a different slave. The slave and target can even be the same interpreter.
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 17-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 17-4 generates output in a human readable format. Example 17-5 generates the aliases as Tcl commands that can be used to re-create 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 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 plug-in described in Chapter 48. 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.
interp create -safe slave
interp hide slave clock
interp hide slave timeYou can remove commands from the slave entirely like this:
interp eval slave [list rename clock {}]
interp eval slave [list rename time {}]
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 113. 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 arg2Never eval alias arguments.
With 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 17-6 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\]}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, which is never evaluated as a Tcl command.
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*] {
file delete -force $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 example, it would also work to invoke the hidden open command to create the I/O channel directly in the slave.
Example 17-7 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 17-9 addresses these issues.
The Safe Base
An safe interpreter created with interp create -safe has no script library environment and no way to source scripts. Tcl provides a safe base that extends a raw safe interpreter with the ability to source scripts and packages as described in Chapter 12. The safe base also defines an exit alias that terminates the slave like the one in Example 17-7. The safe base is implemented as Tcl scripts that are part of the standard Tcl script library. Create an interpreter that uses the safe base with safe::interpCreate:
safe::interpCreate fooThe safe base has source and load aliases that only access directories on an access path defined by the master interpreter. The master has complete control over what files can be loaded into a slave. In general it would be OK to source any Tcl program into an untrusted interpreter. However, untrusted scripts might learn things by sourcing arbitrary files. The safe base also has versions of the package and unknown commands that support the library facility.
Table 17-3 lists the Tcl procedures in the safe base:
Table 17-4 lists the aliases defined in a safe interpreter by the safe base.
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 colleagues review the code. Give out prizes to folks who can break your policy. Good policy implementations are proven with lots of review and trial attacks.
# 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 good] {
set sock [interp invokehidden $slave socket $host $good]
interp invokehidden $slave fconfigure $sock \
-blocking 0
return $sock
}
error "bad port: $port"
}The policy is initialized with Safesock_PolicyInit. The name of this procedure follows a naming convention used by the safe base. 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.
The Tcl Web browser plug-in ships with a slightly improved version of the Safesock policy. It adds an alias for fconfigure so the http package can set end of line translations and buffering modes. The fconfigure alias does not let you change the blocking behavior of the socket. The policy has also been extended to classify hosts into trusted and untrusted hosts based on their address. A different table of allowed ports is used for the two classes of hosts. The classification is done with two tables: one table lists patterns that match trusted hosts and the other table lists hosts that should not be trusted even though they match the first table. The improved also version lets a downloaded script connect to the Web server that it came from. The Web browser plug-in is described in Chapter 48.
Limited Temporary Files
Example 17-9 improves on Example 17-7 by limiting the number of temporary files and the size of the files. It is written to work with the safe base so it has a Tempfile_PolicyInit that takes the name of the slave as an argument. TempfileOpenAlias lets the child specify a file by name, yet it limits the files to a single directory.
The example demonstrates a shared I/O channel that gives the master control over output. TempfilePutsAlias restricts the amount of data that can be written to a file. By sharing the I/O channel for the temporary file the slave can use commands like gets, eof, and close, while the master does the puts. The need for shared I/O channels is somewhat reduced by hidden commands, which were added to Safe-Tcl more recently than shared I/O channels. For example, the puts alias can either write to a shared channel after checking the file size, or it could invoke the hidden puts in the slave. This alternative is shown in Example 17-10.
# Policy parameters:
# 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.
array set tempfile {
maxfile 4
maxsize 65536
}
# tempfile(directory) is computed dynamically based on
# the source of the script
proc Tempfile_PolicyInit {slave} {
global tempfile
interp alias $slave open {} \
TempfileOpenAlias $slave $tempfile(directory) \
$tempfile(maxfile)
interp alias $slave puts {} TempfilePutsAlias $slave \
$tempfile(maxsize)
interp alias $slave exit {} TempfileExitAlias $slave
}
proc TempfileOpenAlias {slave dir maxfile name {m r} {p 0777}} {
global tempfile
# remove sneaky characters
regsub -all {|/:} [file tail $name] {} real
set real [file join $dir $real]
# Limit the number of files
set files [glob -nocomplain [file join $dir *]]
set N [llength $files]
if {($N >= $maxfile) && (\
[lsearch -exact $files $real] < 0)} {
error "permission denied"
}
if [catch {open $real $m $p} out] {
return -code error "$name: permission denied"
}
lappend tempfile(channels,$slave) $out
interp share {} $out $slave
return $out
}
proc TempfileExitAlias {slave} {
global tempfile
interp delete $slave
if [info exists tempfile(channels,$slave)] {
foreach out $tempfile(channels,$slave) {
catch {close $out}
}
unset tempfile(channels,$slave)
}
}
# See also the puts alias in Example 19-4 on page 245
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 17-9 to have 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 interpreters 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 tempfile(directory) definition is not shown in the example. The application must choose a directory when it creates the safe interpreter. The Browser security policy described on page 611 chooses a directory based on the name of the URL containing the untrusted script.
The TempfilePutsAlias procedure implements a limited form of puts. It checks the size of the file with tell and measures the output string to see if the total exceeds the limit. The limit comes from a parameter defined when the alias is created. The file cannot grow past the limit, at least not by any action of the child interpreter. The args parameter is used to allow an optional -nonewline flag to puts. The value of args is checked explicitly instead of using the eval trick described in Example 10-2 on page 116. Never eval arguments to aliases or else a slave can attack you with arguments that contain embedded Tcl commands.
The master and slave share the I/O channel. The name of the I/O channel is recorded in tempfile, 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.
proc Tempfile_PolicyInit {slave} {
global tempfile
interp alias $slave open {} \
TempfileOpenAlias $slave $tempfile(directory) \
$tempfile(maxfile)
interp hide $slave tell
interp alias $slave tell {} TempfileTellAlias $slave
interp hide $slave puts
interp alias $slave puts {} TempfilePutsAlias $slave \
$tempfile(maxsize)
# no special exit alias required
}
proc TempfileOpenAlias {slave dir maxfile name {m r} {p 0777}} {
# remove sneaky characters
regsub -all {|/:} [file tail $name] {} real
set real [file join $dir $real]
# Limit the number of files
set files [glob -nocomplain [file join $dir *]]
set N [llength $files]
if {($N >= $maxfile) && (\
[lsearch -exact $files $real] < 0)} {
error "permission denied"
}
if [catch {interp invokehidden $slave \
open $real $m $p} out] {
return -code error "$name: permission denied"
}
return $out
}
proc TempfileTellAlias {slave chan} {
interp invokehidden $slave tell $chan
}
proc TempfilePutsAlias {slave max chan args} {
if {[llength $args] > 2} {
error "invalid arguments"
}
if {[llength $args] == 2} {
if {![string match -n* [lindex $args 0]]} {
error "invalid arguments"
}
set string [lindex $args 1]
} else {
set string [lindex $args 0]\n
}
set size [interp invokehidden $slave tell $chan]
incr size [string length $string]
if {$size > $max} {
error "File size exceeded"
} else {
interp invokehidden $slave \
puts -nonewline $chan $string
}
}
Example 17-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 IDs 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 purposes. 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 after
interp alias $slave after {} SafeAfterAlias $slave $max
interp alias $slave exit {} SafeAfterExitAlias $slave
# This is used to generate after IDs for the slave.
set after(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 after
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 after(id,$slave,$myid)]} {
set id $after(id,$slave,$myid)
unset after(id,$slave,$myid)
after cancel $id
}
return ""
}
default {
if {$argc == 1} {
error "Usage: after time command args..."
}
if {[llength [array names after 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 after(id,$slave)]
set id [after [lindex $args 0] \
[list SafeAfterCallback $slave $myid $command]]
set after(id,$slave,$myid) $id
return $myid
}
}
}
# SafeAfterCallback is the after callback in the master.
# It evaluates its command in the safe interpreter.
proc SafeAfterCallback {slave myid cmd} {
global after
unset after(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 after
foreach id [array names after id,$slave,*] {
after cancel $after($id)
unset after($id)
}
interp delete $slave
}