www.gibmonks.com

  Previous section   Next section

Practical Programming in Tcl & Tk, Third Edition
By Brent B. Welch

Table of Contents
Chapter 19.  Multiple Interpreters and Safe-Tcl


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 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:

  • Small policies are better than big, complex policies. If you do a lot of complex processing to allow or disallow access to resources, chances are there are holes in your policy. Keep it simple.

  • Never eval arguments to aliases. If an alias accepts arguments that are passed by the slave, you must avoid being tricked into executing arbitrary Tcl code. The primary way to avoid this never is to eval arguments that are passed into an alias. Watch your expressions, too. The expr command does an extra round of substitutions, so brace all your expressions so that an attacker cannot pass [exit] where you expect a number!

  • Security policies do not compose. Each time you add a new alias to a security policy, it changes the nature of the policy. Even if alias1 and alias2 are safe in isolation, there is no guarantee that they cannot be used together to mount an attack. Each addition to a security policy requires careful review.

Limited Socket Access

The Safesock security policy provides limited socket access. The policy is designed around a simple table of allowed hosts and ports. An untrusted interpreter can connect only to addresses listed in the table. For example, I would never let untrusted code connect to the sendmail, ftp, or telnet ports on my hosts. There are just too many attacks possible on these ports. On the other hand, I might want to let untrusted code fetch a URL from certain hosts, or connect to a database server for an intranet application. The goal of this policy is to have a simple way to specify exactly what hosts and ports a slave can access. Example 19-8 shows a simplified version of the Safesock security policy that is distributed with Tcl 8.0.

Example 19-8 The Safesock security policy.
# 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 nonblocking mode so that 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 0

The 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 that 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 version also lets a downloaded script connect to the Web server that it came from. The Web browser plug-in is described in Chapter 20.

Limited Temporary Files

Example 19-9 improves on Example 19-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 can invoke the hidden puts in the slave. This alternative is shown in Example 19-10.

Example 19-9 The Tempfile security policy.
# 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 22? on page 329
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 19-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 specifies only the name and access mode (i.e., for reading or writing.) The Tempfile policy can 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 302 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-3 on page 127. 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.

The shared I/O channel lets the master use puts and tell. It is also possible to implement this policy by using hidden puts and tell commands. The reason tell must be hidden is to prevent the slave from implementing its own version of tell that lies about the seek offset value. One advantage of using hidden commands is that there is no need to clean up the tempfile state about open channels. You can also layer the puts alias on top of any existing puts implementation. For example, a script may define puts to be a procedure that inserts data into a text widget. Example 19-10 shows the difference when using hidden commands.

Example 19-10 Restricted puts using hidden commands.
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
   }
}

Safe after Command

The after command is unsafe because it can block the application for an arbitrary amount of time. This happens if you only specify a time but do not specify a command. In this case, Tcl just waits for the time period and processes no events. This will stop all interpreters, not just the one doing the after command. This is a kind of resource attack. It doesn't leak information or damage anything, but it disrupts the main application.

Example 19-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.

Example 19-11 A safe after command.
# 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
}

      Previous section   Next section
    Top