Securing your application

Very often we will want our application to run Tcl code provided from a trusted or untrusted source. In these cases, it makes sense to run such scripts in environments with limited access to operating system and / or limited resources. In some cases, we can use this to provide an additional security layer. In other cases, it can be used as additional layer for securing ourselves against badly written code.

Typical examples of such cases include running code retrieved over the network. Our application may choose to limit access to an operating system for code written by sources we do not fully trust. An example is a data processing system using multiple computers; commands received from science units might be run in different environment than commands received from system administrators.

Slave interpreters, functionality offers creating a Tcl sandbox for running commands, which we might not want to have full access to our main application's code and / or provide additional security to limit the potential dangers of running these scripts. It works in such a way that we can create an additional Tcl interpreter, called the slave interpreter, which we can evaluate commands in. From our slave interpreter's perspective, the interpreter that created it is called the master interpreter.

In addition we might impose limits on some operations, such as terminating after a specified amount of time. This can be used to detect and terminate scripts that are taking too long to complete. It can also be used as a typical feature of an application, allowing users to specify if a command should be terminated after a specified time.

Safe interpreters are based on a more generic concept of slave interpreters, which allow for creating additional Tcl interpreters, exporting commands and channels as needed. This can be used to both provide security and provide a separate interpreter, which can't directly access data in other interpreters.

Slave interpreters

All slave interpreters are managed from the interp command. It is used to create normal and safe slave interpreters. All of its functionality is accessed using the command's subcommands. All slave interpreters are referenced by name and all interpreters are also accessible via an additional command, which is the same as interpreter name.

Each interpreter is a completely separated from other Tcl interpreters by default—it has different namespaces, variables, commands, and channels. This allows things such as running legacy code that uses same command or variable names as other parts of our application; names will not collide and all that is needed is additional code to use remote commands from an interpreter. We can also use information from different interpreters and details on how to do this are described later in this chapter.

Complete documentation of the interp command and all of its subcommands can be found at: http://www.tcl.tk/man/tcl/TclCmd/interp.htm

Using slave interpreters

Command interp create can be used to create a slave interpreter. It can be run without parameters, in which case it creates a unique interpreter name or one with the interpreter name as an argument. In the latter case, that name is used for the interpreter. The command also accepts the optional flag -safe as first argument. In most cases it is better to use a name provided by interp create command. This will not cause problems with the same interpreter name being used by multiple parts of an application, and is generally a better approach, especially in reusable code.

For example, we can create a slave interpreter by doing:

set childInterp [interp create]

This will set the childInterp variable to the name of a newly created interpreter. We can access slave interpreters functionality either by using the interp command's subcommands or by using the interpreter's own command. The name of the interpreter is the same as the name of the command created.

Slave interpreters are run in the same thread as the current interpreter; therefore, entering event loop in any of the interpreters will cause all interpreters to process events.

We can now evaluate Tcl commands in this interpreter using the eval subcommand. It can be run either from generic interp command or from the interpreter's own command.

Running eval from the interpreter's own command requires specifying command(s) to evaluate as arguments. For example:

puts "1 + 1 = [$childInterp eval {expr { 1 + 1 }}]"

This will cause the expr 1+1 command to be evaluated in slave interpreter and result from that printed from master interpreter.

We can also use a generic interp command, which requires specifying the interpreter name followed by the command(s) to evaluate. For example:

set result [interp eval $childInterp {string repeat "Test " 10}]

The preceding code will evaluate the string repeat statement in the slave interpreter, and set the result variable in the master interpreter to the result of that command.

Commands passed to eval can be created as multiple arguments, but it is always a safer approach to specify all commands to evaluate as one argument.

We can also create commands using Tcl lists that will be evaluated in the slave interpreter. This can be used to pass arguments from local variables or for dynamically creating a command to run.

For example, we can set variables in the slave interpreter from a local value by doing:

$childInterp eval [list string repeat $someString 10]

This will cause a value of the variable someString in the master interpreter to be repeated 10 times, but using string repeat from the slave interpreter. This can be used to pass values from variables in the master as parameters to commands in the slave interpreter.

Sharing information with the slave interpreter

Tcl does not offer any way to map variables between the master and slave interpreter. Therefore, this is very often the most convenient way to set variables in slave interpreters.

For example, we can set variables in the slave interpreter from a local value by doing:

$childInterp eval [list set value $value]

This will set the variable in the slave interpreter to a current value from the master interpreter. However, if either the master or slave changes the value of a variable, this information will not be propagated.

Tcl channels are also not shared across interpreters by default. This means that a file opened in the master interpreter cannot be read from slave one without additional steps.

In order to use a channel from the master interpreter in the slave, we need to invoke the interp share or interp transfer commands. The first command makes a channel available in target interpreter without removing it from the source interpreter. The second command makes the channel available in the target interpreter, and removes it from the source interpreter. Both commands accept the source interpreter as the first argument, followed by the channel name and target interpreter.

For example, to make the stdout channel available in a slave interpreter we can do the following:

interp share {} stdout $childInterp

The channels stdin, stdout, and stderr are available in regular slave interpreters by default. They are not available in safe interpreters; if we want our safe interpreter to write to a standard output channel, we need to explicitly share the channel.

Channels shared between interpreters require both interpreters to close the channel for the underlying channel to be cleaned up. For example, if a file handle is shared across interpreters, unless all interpreters close it, the file will remain open. Channels transferred from one interpreter to another only need to be closed in the target interpreter.

Channel sharing and transferring can also be used to pass file handles or network connections to slave interpreters. For example, if we want to make code responsible for communication more secure, it can be run in a separate and safe interpreter.

Commands themselves cannot be shared, but the master interpreter can create commands in a slave interpreter by invoking the eval subcommand and running the proc command in the slave interpreter. Aliases also allow mapping of commands from the master interpreter to the slave interpreter. They are described in more detail in the next section.

The following table summarizes what information can be shared across interpreters:

Type

Command

Comments

Variables

N/A

Cannot be automatically shared; although variables in slave can be set from master

Channels

interp share

interp transfer

Channels can be shared and / or transferred between interpreters

Commands

interp alias

Commands from any interpreter can be run in any other interpreter

Creating and managing aliases

Slave interpreters can create commands that invoke other commands in any other interpreters. It can be used to have a slave interpreter call command in master interpreter. It can be used to expose functionality to slave interpreters, create wrappers for safe interpreters that limit functionality. This functionality is called aliases.

Aliases can be created between any interpreters, but usually a master creates an alias that is accessible in a slave interpreter. Creating or retrieving aliases can be done using an alias command. Invoked as the interp alias command, it requires specifying a target interpreter as the first argument, the source interpreter the command should be run in, and the name of the command to run.

When creating an alias by running the alias subcommand of the interpreter's command, it only requires specifying the target command name and source command name. The target interpreter is the interpreter passed as a command name, and the source interpreter is the current interpreter.

This will create a command in the target interpreter; the name will be the target command name, specified as a first argument. When invoked, it will cause the source command to be invoked in the source interpreter.

We can create a command to increase the value of a global variable and print it out in the master interpreter:

proc incrCounter {variable} {
global $variable
set v [incr $variable]
puts "Current value of "$variable" is $v"
}

It is worth noting that the preceding code is evaluated in the master interpreter, and we have to be careful with handling data passed by slave interpreters. For example, it is always a bad idea to pass values passed by users to commands such as eval. As we assume code run in the slave interpreter is not trusted, we also should not assume values passed by it are valid.

We also define an alias that will cause this command to be accessible from the slave interpreter under the same name.

interp alias $childInterp incrCounter {} incrCounter

An alias can be also created by invoking the interpreter command. The following command is equivalent to the previous code:

$childInterp alias incrCounter incrCounter

We can now invoke our command by doing:

set value 12
$childInterp eval {incrCounter value}

This will cause the Current value of "value" is 13 to be printed on standard output. As the incrCounter command is actually evaluated in the master interpreter, no variables are passed to the slave interpreter and the variable itself is not fully accessible.

We can also add arguments to pass to this command, before arguments provided when invoked as target command after source command name.

For example:

interp alias $childInterp incrCounterValue {} incrCounter value

Next, we can invoke the following command:

$childInterp eval {incrCounterValue}

This will cause the incrCounter command to be invoked in the master interpreter with value as its argument. The output would be similar to Current value of "value" is 14.

Another example is creating commands for things like HTTP or POP3 protocols. We can then pass a token or any other information as part of the alias; for example:

set handle [pop3::open $hostname $username $password]
interp alias $childInterp emailRetrieve {} pop3::retrieve $handle
interp alias $childInterp emailList {} pop3::list $handle

This will cause commands emailRetrieve and emailList in the slave interpreter to access a valid POP3 session from the master interpreter. POP3 protocol and related commands are described in more detail in Chapter 7, Using Common Internet Services

Typically aliases are used for two types of things. The first use is to export specific functions to the slave interpreter. This is then used to run scripts provided by users or different parts of the infrastructure; scripts themselves won't have access to an application's private data, but will be able to perform many operations remotely.

The second functionality is limiting the scope of a command. This is mainly used for safe interpreters. We can overwrite a command and check whether a specified operation is valid. For example, we can overwrite the socket command to only allow outgoing connections and only to specified hosts and / or ports. This method is described in more detail later in this chapter.

The Tcl slave interpreter's mechanism offers functionality for listing and getting definitions of aliases for a specified slave interpreter. This can be done using the aliases subcommand, either running the interp aliases <interpName> or<interpName> aliases command, where<interpName> is the name of the interpreter.

Retrieving the definition of an individual alias is done by invoking the alias subcommand with just alias name as argument. When invoking as an interp alias command, this needs to be prefixed by a target interpreter name—for example, interp alias <interpName> <commandName> or<interpName> alias <commandName>.

For example, we can list and print all aliases by doing:

foreach alias [interp aliases $childInterp] {
puts "$alias is "[$childInterp alias $alias]""
}

Deleting interpreters

Deleting a slave interpreter can be done using the interp delete command. It accepts a single argument, which is the name of the interpreter to delete. For example, we can delete our previously deleted interpreter by doing:

interp delete $childInterp

Tcl automatically removes all information and objects related to this interpreter, such as timers, variables, commands, and channels.

Note

Examples of using slave interpreters, described in this and previous sections, is located in the interp.tcl file in 05slaveinterp directory in the source code examples for this chapter.

Imposing limits on interpreters

Very often we need to limit time or the number of commands that can be run. In many cases it can serve as simply a mechanism for detecting infinite loops and / or other errors in the code sent to clients. This can be very useful mostly for scripts that are written and sent to clients ad hoc—often they will contain errors that can cause things such as infinite loops, or performing operations over a long period of time. Slave interpreter limits can be used to detect and prevent these issues.

Limits are checked before each command is run. If either number of commands run or time limit has been reached, an error is shown. If script has been run from the master interpreter, this error is then received by the master interpreter.

All limits are set using the interp limit <interpName> or<interpName> limit commands. The first argument is limit type, which can be either time or commands. It can be specified by one or more options for this limit. Options depend on limit type and are described further in the section.

When not specifying any additional option, it returns a list of name-value pairs containing all options and their current values. When specifying only the option name, its current value is returned. Specifying one or more options and corresponding values causes those options to be set.

There are two types of interpreter limits. We can set up a limit on the number of commands that can be run, specified as limit type commands. It accepts the -value option that specifies the number of commands, after which a limit should be reached.

Running the following command will cause an interpreter to limit it to only running 10000 commands:

$childInterp limit commands -value 10000

We can also set it in the following way:

interp limit $childInterp commands -value 10000

We can then run the following command:

set value 0
proc increaseValue {} {
global value
incr value
}
interp alias $childInterp increaseValue {} increaseValue
$childInterp eval {
while {true} {increaseValue}
}

It will show after 10000 increaseValue command executions. At that point the value variable should be set to 10001 or a similar value. The actual number might differ depending on the Tcl version, and how limits are internally handled. It is not a good idea to depend on an exact value; instead use a reasonable limit.

The current number of commands run can be retrieved using the info cmdcount command. This is the same value that limits are compared to.

For example, setting the limit to allow 10000 more operations to be performed can be done as follows:

interp limit $childInterp commands -value 
[expr {[$childInterp eval {info cmdcount}] + 10000}]

A time-based limit is another type of limit. It is defined as a time limit type and accepts two options -seconds and -milliseconds. The first option defines the Unix timestamp at which a limit for the interpreter should be triggered. This can be output from the clock scan command, but can be any Unix timestamp. The option -milliseconds defines the number of milliseconds after a specified Unix timestamp has been reached, at which point a limit should be triggered. This option should only be specified along with the -seconds option.

For example, in order to cause an interpreter to reach a limit after three seconds we can do the following:

$childInterp limit time -seconds [clock scan "+3 seconds"]

All types of limits also accept two additional options -command and -granularity. Both options are set independently for each limit type.

The first option specifies a Tcl script to be executed in the interpreter that the interp limit command is currently run from. It can modify limits of the interpreter if it wants the interpreter to continue executing.

We can also modify how often a check is made by using the -granularity option. This option specifies how frequently a check is made; usually the check is done before any command invocation. Setting the value to a greater value will cause a check to be made less often, which increases performance, especially in the case of a time limit type. Usually it is not necessary to change the default values of this option, but if your application needs more exact limit checking, it is advised to set the-granularity option to 1 for all limit types.

An additional limit can be set on recursion, that is, how many times one command can invoke another inside it, either itself or any other procedure. We can specify after how many levels of recursion an error should be thrown by using interp recursionlimit <interpName> or<interpName> recursionlimit. If run without arguments, it returns the current limit. If run with a single argument, it sets the value of the recursion limit. For Tcl 8.4 and 8.5, the default value for all interpreters is 1000.

For example, we can test it in the following way:

$childInterp recursionlimit 16
$childInterp eval {
proc test {} {
global recursecount
incr recursecount
test
}
test
}

This will give an error about the recursion limit being hit. The value of the recursecount variable in the slave interpreter should be set to 15 or similar. Similar to limits on a command count, it is not a good to depend on an exact value; instead use a reasonable limit.

Note

Examples of slave interpreter limits is located in the limits.tcl file in the 05slaveinterp directory in the source code examples for this chapter.

Working with safe interpreters

As mentioned earlier, Tcl offers a mechanism for creating safe interpreters. These can be created using the interp create command with the -safe option provided. For example:

set childInterp [interp create -safe]

This will cause a safe Tcl interpreter to be created. It can be used in the same way as a regular slave interpreter. The main difference is that many commands are not accessible. For example, the command socket is not available by default. Default limitations and command hiding and exposing is explained later in this chapter.

One of the limitations is that such an interpreter cannot access any of Tcl's libraries, as source and other commands are not available by default. For example, the following will fail:

package require http

The ability to load packages and provide limited access to the filesystem using the Safe Base mechanism is described later in this chapter.

Another difference between safe and normal interpreters is how binary extensions, such as Tcl package for SQLite3, are loaded. Binary extensions have two different mechanisms for initialization, and when creating an extension its author can choose to limit functionality of an extension in safe interpreters. For example, the sqlite3 package does not offer any functions in safe interpreters. The package Tclx only offers limited functionality in safe interpreters.

As mentioned earlier, safe interpreters do not have access to stdin, stdout, and stderr channels by default. The reason for this is that a safe interpreter is created with minimum resources available by default. We can use the interp share or interp transfer command to make some of the channels available in a safe interpreter.

Hiding and exposing commands

Slave interpreters do not offer a full set of Tcl commands so that commands such as exit could not be invoked, which would cause our application to exit completely. Instead of simply removing the commands, which would make any attempt of invoking them again impossible, slave interpreters allow hiding particular commands. This means that they can't be accessed directly (that is, invoking exit will not work), but can be invoked from the master interpreter.

This is often used in conjunction with an alias running in master interpreter that checks whether a particular operation can be performed; for example, before reading a file, a check can be made as to whether the interpreter should be allowed to read it.

Hiding a command can be done using the interp hide <interpPath> or<interpPath> hide commands. The command accepts the name of the command to hide as its argument. For example, in order to hide the command after we can do the following:

interp hide $childInterp after

The command interp expose can be used to make hidden commands available again. Similar to hide, it can be invoked as interp expose <interpPath> or<interpPath> expose. It takes one argument—the name of the hidden command. It can be used for commands hidden by both our code as well as by Tcl itself, such as open command for safe interpreters.

In order to make the after command available in the interpreter again, we can do the following:

$childInterp expose after

Commands that are hidden in a specified interpreter can be called using the interp invokehidden command; either invoked as interp invokehidden <interpPath> or<interpPath> invokehidden. It accepts the hidden command name as the first argument, followed by arguments to pass to the command.

Typically this is done so that a slave interpreter's command is hidden, then an alias is created that runs in the master interpreter and checks whether an operation can be allowed. If it is ok to continue, invokehidden is called to invoke the hidden command.

For example, let's consider modifying a socket command so that it only allows outgoing connections and only to specific hosts. We'll need to start by hiding the original socket command. The command socket is hidden by default for safe interpreters. For regular slave interpreters though, we can hide it using the following command:

$childInterp hide socket

Afer this, we need to create a procedure in the master interpreter that checks permissions and invokes the hidden socket command of the slave interpreter, only if it is ok to continue:

proc interpSocket {childInterp args} {

We accept any number of arguments since the socket command also accepts multiple switches.

We'll start by searching for the -server switch; if it is present, we show an error:

if {[lsearch $args "-server"] >= 0} {
error "Server sockets not allowed"
}

Now let's retrieve the host name specified for the command. Usually it is the argument preceding the port, which is the last argument:

set host [lindex $args end-1]

After this we check whether the host is either www.google.com or 127.0.0.1. If it is we invoke the command. If not, we give out an error:

if {$host == "www.google.com"} {
interp invokehidden $childInterp socket {*}$args
} elseif {$host == "127.0.0.1"} {
$childInterp invokehidden socket {*}$args
} else {
error "Connections to $host not allowed"
}
}

Finally, we need to make an alias that maps the socket command in the slave interpreter to the interpSocket command in the master interpreter.

interp alias $childInterp socket {} interpSocket $childInterp

After that, we can test our code by running the following code:

$childInterp eval {
set chan [socket www.google.com 80]
set chan [socket malicious-server 80]
}

It should terminate with error Connections to malicious-server not allowed or a similar error.

Note

The preceding example is located in the socket.tcl file in the 06safetcl directory in the source code examples for this chapter.

Default safe interpreter setup

Safe interpreters come with limited functionality by default. Many commands are hidden by default. Commands that allow network or filesystem access are disabled. They also cannot access environment variables via global env array; giving untrusted code access to a user's environment variables is not considered safe.

Safe interpreters can't also access standard channels—stdin, stdout, and stderr. We can share any of these channels using the interp share command, but by default they are not accessible.

Interpreters do not also have the possibility of loading additional packages by default; as commands such as open and source are unavailable, Tcl has no way to load any additional package.

Even though safe interpreters have access to the interp command, this command also has limited functionality. Safe interpreters can only create other safe slave interpreters, even if the -safe switch is not specified when using interp create, a new interpreter is created as safe anyway. Safe interpreters can't also change the limits (commands, time, and recursion limits) of its own interpreter. It is also not possible to call the interp invokehidden command, as safe interpreters can't get access to hidden commands.

Detailed information about commands available by default in safe interpreters, as well as what are additional limitations of safe interpreters, can be found at http://www.tcl.tk/man/tcl8.5/TclCmd/interp.htm#M42.

Using Safe Base

Safe interpreters cannot load any packages by default and their use is very limited. Tcl also comes with a Safe Base package that extends safe interpreter functionality to allow limited filesystem access as well as other additions that aid in package loading.

Safe Base uses safe interpreters created with the interp create -safe command. Safe Base creates additional commands, described further in this section, that allow more functionality while retaining the safety of the interpreter. Such interpreters can load basic Tcl packages, but cannot access parts of the filesystem other than the Tcl libraries that are included in Tcl itself.

We can create a safe interpreter using Safe Base by invoking the safe::interpCreate command. It accepts an optional interpreter name. It also accepts one or more options. These options are described later in this section.

set childInterp [safe::interpCreate]

This creates a safe Tcl interpreter that can only access the part of the filesystem that contains Tcl libraries. We can now safely load packages such as http, but trying to source any file outside of Tcl's libraries fails. For example:

$childInterp eval {source /tmp/myscript.tcl}

The preceding code will give a permission denied error.

We can also initialize any existing safe interpreter to offer Safe Base's extended functionality using the safe::interpInit command. It accepts the slave interpreter and options for the interpreter. For example:

set childInterp [interp create -safe]
safe::interpInit $childInterp

Safe Base supports the options -accessPath, -deleteHook, -statics, and -nested. The first option specifies a list of paths that the interpreter is allowed to access. Adding and removing directories from the list allows for managing paths where the interpreter can source and load files from.

The option -deleteHook defines the command to run just before this interpreter is deleted. This command will be run with the interpreter's name specified as an additional argument. This can be used for retrieving values or cleaning up data kept outside of the specified interpreter.

The option -statics specifies whether a package is allowed to load statically defined packages. The option -nested specifies whether this slave interpreter will be allowed to load packages into its own slave interpreters. These options are mainly meant for dealing with packages created in C, and can be left as they are for the majority of Safe Base uses.

These options can be changed or read at any time using the safe::interpConfigure command, followed by the slave interpreter name. If invoked with no additional arguments, it returns a name-value pair's list with all available options and their current values. Specifying just the option name returns the current value for that option. Specifying one or more name-value pairs sets these options to new values.

For example:

interp::safeConfigure $childInterp accessPath 
[linsert 
[interp::safeConfigure $childInterp accessPath] 0 
"/path/to/libraries"]

This command will read the current -accessPath option's value and insert /path/to/libraries as the first item in the path list.

Deleting an interpreter created using Safe Base can be done using the safe::interpDelete command. For example:

safe::interpDelete $childInterp

This will cause the delete hook to be run and the interpreter to be deleted.

Complete documentation for Safe Base and Safe Tcl functionality can be found at: http://www.tcl.tk/man/tcl/TclCmd/safe.htm

Safe Base and filesystem access

In order to provide loading of packages, Safe Base provides a few additional commands in addition to the ones safe interpreters offer. These are source, load, file, glob, encoding, and exit. The first two commands limit filesystem to only access packages in specified paths.

The scope of the glob command has been limited to only access directories in the interpreter's path. The command file is limited to dirname, join, extension, root, tail, pathname, and split subcommands. The command encoding cannot change system encoding or access / change paths where encodings are looked up. Finally, exit causes only the interpreter to be deleted, but the Tcl process and master interpreter is left intact.

More details about commands available in Safe Base-created safe interpreters can be found at http://www.tcl.tk/man/tcl/TclCmd/safe.htm#M21.

Unlike default slave interpreters, Safe Base offers limited file system access. By default only source and load command are available for accessing files. These allow for loading packages without providing direct access to a file system.

In addition, the list of directories that can be accessed is limited. The configuration option -accessPath provides a list of directories that can be accessed from within the interpreter. We can retrieve the current value, modify, and set it using safe::interpConfigure command.

We can also use the safe::interpAddToAccessPath command to add additional directories to the access list. It accepts the interpreter name and directory to add to the list. This is a helper function that checks if a path exists, and adds it if it does not exist. If the path is already in the access list, an error is produced.

For example, in order to add /tmp to the list of accessible directories, we can do:

safe:: interpAddToAccessPath $childInterp /tmp

Another feature of Safe Base is the ability to obscure actual directory names. In many cases, finding out the path to where an application or user's directory is set up can be sensitive information; for example, providing a path to the home directory can specify a user's name, which might not always be what the script should be able to access.

For this purpose, Safe Base Tcl has provided a mechanism for translating paths to obscure original paths.

The safe::interpFindInAccessPath command can be used to find a path in an interpreter and convert it into a safe representation. It accepts the interpreter name followed by the directory name.

For example, we can map the Tcl library directory to a safe representation by doing:

set safepath [safe::interpFindInAccessPath 
$childInterp $tcl_library]
puts "Directory $tcl_library is mapped to $safetcldir"

The variable tcl_library is defined by Tcl and specifies the directory used for Tcl base functionality. The output from this would be something similar to /opt/ActiveTcl-8.5/lib/tcl8.5 is mapped to $p(:0:).

We can then use this for commands such as source or load from within a Safe Base interpreter. For example, to manually load parray.tcl from the Tcl library directory we can do:

$childInterp eval [list source 
[file join $safetcldir parray.tcl]]

This will invoke a command similar to source $p(:0:)/parray.tcl. Implementation of the source command in Safe Base will map this to actual path.

Examples of using the safe interpreter and accessing the file system can be found in the safebase_filesystem.tcl file in the 06safetcl directory, in the source code examples for this chapter.

We can also extend the approach taken from source command and create an open command wrapper that only allows files specified in the -accessPath option to be opened

We can start by creating a wrapper that checks whether a path can be accessed or not. It accepts the slave interpreter name and directory name:

proc safeCheckAccessPath {childInterp dirname} {
if {[catch {
set path [safe::TranslatePath $childInterp $dirname]
safe::DirInAccessPath $childInterp $path
}]} {
return ""
} else {
return $path
}
}

The directory name is translated first, which expands obscured paths and a check is made as to whether it can be accessed or not. If any of these operations fail, an empty string is returned. Otherwise a valid path is returned.

This command uses Safe Base's internal commands safe::TranslatePath and safe::DirInAccessPath. The first command translates path; expanding obscured paths such as $p(:0:) and making sure it does not contain dangerous items. Second command checks if a path can be accessed and returns an error if it cannot be accessed.

Now we can create a safe open command wrapper. It takes the interpreter name, a list of allowed open modes, and any additional arguments. Open modes can be those such as r, w, r+, w+, and all others supported by the open command. Please see Chapter 2, Advanced Tcl Features for more details about open modes.

The command first splits the path and checks whether the directory can be accessed or not, and if specified, the access mode is in the list of allowed open modes. We also check if allowedModes is not set to *; this causes all modes to be available. If either directory is not in the allowed path or the mode is not allowed, an error occurs. Otherwise, the hidden open command is invoked in the slave interpreter, and a file handle is returned.

proc safeOpen {childInterp allowedModes name {access r} args} {
set dirname [safeCheckAccessPath $childInterp 
[file dirname $name]]
set filetail [file tail $name]
if {($dirname == "") ||
(([lsearch $allowedModes $access] < 0) &&
($allowedModes != "*"))} {
return -code error "Permission denied"
}
return [interp invokehidden $childInterp open 
[file join $dirname $filetail] $access {*}$args]

}

We can now create an alias that only allows reading files for reading by doing:

$childInterp alias open safeOpen $childInterp {r}

We can then test opening various files. The following command will succeed as, by default, the Tcl library directory is accessible:

$childInterp eval [list open $tcl_library/tclConfig.sh]

However, trying to read a file outside it will fail:

$childInterp eval [list open /etc/passwd]

We can also use an obscured filename using the safetcldir variable set earlier:

$childInterp eval [list open $safetcldir/tclConfig.sh]

In addition to this, any attempt to open a file for writing will fail, even if the file itself can be accessed for reading:

$childInterp eval [list open $tcl_library/tclConfig.sh w]

Note

Safe implementation of the open command is located in the safebase_open.tcl file in the 06safetcl directory in the source code examples for this chapter.

Role-based authorization and interpreters

Safe Base and safe interpreters can also be used to create role-based authorization to functionality. We can use it to create sandboxes based on what roles the user is able to access.

We'll also modify the concept a bit—instead of defining a role, we'll define levels for various items. For example, let's consider file access; we can think of cases where limited and read-only access is enough. Other tasks (such as system maintenance) might need to have full access to the entire filesystem.

Similarly for network functionality and limits, in some cases it is good to disable connectivity and set up a one minute limit on execution. In other cases it might be a good idea not to impose limits on any of these, especially for network-related activities, which can also take time due to lagged networks or hosts being down.

We'll use separate Tcl namespaces for defining various roles and a list of acceptable values. Commands for applying roles will be put in the roles namespace. Each role is defined as a child namespace; for example, roles::limits.

Each role will define the values variable in its namespace that will specify acceptable values. The default value for the role will be the first value in the list of acceptable values. For example:

set roles::limits::values [list none 5s 10s 60s 300s]

This defines that acceptable values for the limits role are none, 5s, 10s, 60s, and 300s. The value of none is the default value if not specified.

Applying roles to an interpreter is also simple. We'll create a command for applying any acceptable value for each of the roles. Its name will be in the form of<value>Apply, and it will be defined in the namespace for this role. A command to apply none value to the limits role would be defined as the roles::limits::noneApply command.

This way, creation of an interpreter will simply require getting values for all available roles and applying them.

Creating interpreters using roles

We can create a procedure that creates a safe interpreter based on name-value pairs of roles. Each role is defined by the role name (such as file access) and value (such as read-only).

Let's start with procedure definition:

proc roles::safeInterpCreate {roles} {

We'll create a list containing the actual roles definition, based on provided values and default values for roles not specified in the roles argument.

The procedure will start by listing all existing roles and setting the value to the default value.

set allroles [list]
# map current role definitions to all available roles
# and create a complete list of values for each role
foreach ns [namespace children ::roles] {
set role [namespace tail $ns]
set values [set ${ns}::values]
set value [lindex $values 0]

If a value of a current role is specified in the input list of roles, we check if it is any of acceptable values. If this is the case, we set the value to what was specified as the value for this role.

if {[dict exists $roles $role]} {
set v [dict get $roles $role]
if {[lsearch -exact $values $v] >= 0} {
set value $v
}
}

Finally we add the role name and value, either the default or that provided by the user, to the list of all roles and their values.

lappend allroles $role $value
}

The next step is to create an interpreter using Safe Base and apply all of the roles.

set childInterp [safe::interpCreate]
foreach {role value} $allroles {
::roles::${role}::${value}Apply $childInterp
}
return $childInterp
}

For many of the roles we might want to expose a command, assuming it has not already been exposed. For this reason, we also create a helper procedure roles::exposeCommand:

proc roles::exposeCommand {childInterp command} {
if {[lsearch -exact [$childInterp hidden] $command] >= 0} {
$childInterp expose $command
}
}

It checks whether a command is hidden or not and if it is, exposes it. This will cause hidden commands to be exposed and commands already available to be skipped.

Sample role definitions

Roles will usually depend on the types of tasks that scripts run in a sandbox would be performing. However, some roles such as managing access to the filesystem and network can be considered common.

One of the common roles could be managing file access, which can be set to:

  • no file access
  • read-only
  • read-write access to limited set of directories
  • full access to entire filesystem

Let's start by defining this role's namespace and values variable:

namespace eval roles::files {}
set roles::files::values [list none readonly readwrite full]

Now we need to implement a method to apply any of these values to an interpreter.

The filesystem access does not perform any operations at all, but needs to be created:

proc roles::files::noneApply {childInterp} {
}

Read-only access and read-write access use safeOpen, which was implemented earlier in this chapter. For this example, it has been moved to the roles::files namespace. Read-only access specifies r as the only mode available. Read-write does not limit open access modes.

Both read-only and read-write modes also expose the fconfigure command to allow specification of encoding, translation, and other parameters needed for reading from and/or writing to files.

proc roles::files::readonlyApply {childInterp} {
# create an alias for open to allow any operation
# within limited paths only
$childInterp alias ::open 
::roles::files::safeOpen $childInterp "r"
roles::exposeCommand $childInterp fconfigure
}
proc roles::files::readwriteApply {childInterp} {
# create an alias for open to allow any operation
# within limited paths only
$childInterp alias ::open 
::roles::files::safeOpen $childInterp "*"
roles::exposeCommand $childInterp fconfigure
}

Full access to the filesystem exposes open, fconfigure, and file commands in their full functionality.

The alias for the file command needs to be removed prior to exposing the original file command.

proc roles::files::fullApply {childInterp} {
roles::exposeCommand $childInterp open
roles::exposeCommand $childInterp fconfigure
$childInterp alias file {}
roles::exposeCommand $childInterp file
}

Defining the time limits roles is very similar. We define available values, which correspond to time limits that will be imposed on the interpreter.

namespace eval roles::limits {}
set roles::limits::values [list none 5s 10s 60s 300s]

Applying no limits simply does nothing:

proc roles::limits::noneApply {childInterp} {
}

Any other limit requires specifying an appropriate value for the -seconds option to the interp limit command and time limit type. We'll use the clock scan command for this:

proc roles::limits::5sApply {childInterp} {
$childInterp limit time -seconds [clock scan "+5 seconds"]
}
proc roles::limits::10sApply {childInterp} {
$childInterp limit time -seconds [clock scan "+10 seconds"]
}
proc roles::limits::60sApply {childInterp} {
$childInterp limit time -seconds [clock scan "+60 seconds"]
}
proc roles::limits::300sApply {childInterp} {
$childInterp limit time -seconds [clock scan "+300 seconds"]
}

Using role-based interpreters

We can now create a sample interpreter that can access all files, has limited network connectivity, and has a five-second time limit.

set childInterp [roles::safeInterpCreate [list 
files full 
network local 
limits 5s 
]]

We can now test if our new interpreter can connect to the www.packtpub.com port 80 by running:

if {[catch {
$childInterp eval {socket www.packtpub.com 80}
} error]} {
puts "Remote connection failed: $error"
} else {
puts "Remote connection succeeded"
}

The result should be a message saying Remote connection failed: Connection to www.packtpub.com disallowed or something similar.

We can also try to run an infinite loop, which should also fail:

$childInterp eval {set j 0; while {true} {incr j}}

Note

A complete example is located in the 07roles directory in the source code examples for this chapter.

The roles.tcl file is the main file, while role_file.tcl, roles_limit.tcl and roles_network.tcl files define sample roles for file, network access, and limits.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.209.131