Overview | Index by: file name | procedure name | procedure call | annotation
policy.tcl (annotations | original source)

#
#    Copyright (C) 2010 Alexandros Stergiakis <alsterg@gmail.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

#//#
# Procedures to safe interpreter policies in Slave interpreters.
#
# In this file:
#   * Security policy definitions per-AUTHMODE.
#   * Policy switching.
#   * Activation/deactivation of in-line scripting.
#   * Wrapper procedures that enforce session policy:
#
# The security policy defines which Tcl commands are accessible from within the
# safe interpreter in which the command prompt string is evaluated, which includes
# MikroConf commands and Tcl scripts.
#
# When we change AUTHMODE (lower to higher privilege or vise versa) we delete
# the previous "Slave" interpreter and create a new one to evaluatate user in-line code
# and MikroConf commands. This means that all interpreter state is lost:
# Any declared user procedures that are not saved, variables, renames etc are lost
# after changing AUTHMODE. The same happens when deactivate and reactivate inline scripting,
# which translates to changing to a very very limitted AUTHMODE where only MikroConf
# commands are accepted.
#
# Note that deactivation of inline scripting only affects
# command execution and command substitution. It does not affect variable
# substitution which is always possible.
#
#//#

namespace eval policy {
namespace export policy scripting
# Using ::procedure::load and ::pty::ptyexec

# Define policy for the different AUTHMODEs. Every policy defines a modification
# offset from the initial condition of the safe interpreter. If a policy for an
# AUTHMODE is not found, the "user" policy will be used instead.
#
# We start from a safe interpreter, so that we don't have to worry much about
# future unsafe Tcl commands.
#
# The initial state of the safe interpreter of Tcl 8.5.1 is (subject to change in future versions):
#       aliases: clock
#       hidden: file socket open unload pwd glob exec encoding fconfigure load source exit cd
#       visible: tell subst eof list pid time eval lassign lrange fblocked lsearch gets case lappend
#                   proc break variable llength return linsert error catch clock info split array if concat
#                   join lreplace fcopy global switch update close for append lreverse format read
#                   package set binary namespace scan apply trace seek while chan flush after
#                   vwait dict continue uplevel foreach lset rename fileevent regexp lrepeat upvar
#                   expr unset regsub interp puts incr lindex lsort string
#
# So if you want to expose some of the hidden commands, enter them in the "expose" list.
# If you want to hide one of the visible, append it in the "hide" list.
# To establish an alias for a by-default hidden command just enter it in the "alias" list. If
# by default is visible, enter it also in the "hide" list. "bgexec" and "unknown" procedures are
# always available even when inline scripting is turned off.
#
# Note that "info body" cannot be used to view the body of a protected
# command, as well as any aliases, hidden and builins (set, puts, ...).
# Also note that the 'history' command is not created on a slave interpreter by default,
# it exists only on the Master.
#
# All alias targets are expected to be found inside the Session thread/interp. If an alias
# specification has the form:
#        alias {rename}
# then the target will be automatically set to "::policy::slave_rename". However if the form is:
#        alias {{rename ::_rename}}
# then the target will be set to whatever the second argument is.
#
# Finally if a request for an action cannot be performed (e.g. hide an already hidden
# command) MikroConf will log it and cause an error (which is not caught in purpose).

variable Policy {
    bg {
        hide {clock chan interp rename package}
        expose {}
        alias {unknown proc gets read fileevent fconfigure flush}
    }
    priv {
        hide {clock chan interp package}
        expose {cd exec file glob load open pwd socket}
        alias {rename proc unknown gets read fileevent fconfigure flush}
    }
    user {
        hide {clock chan interp package}
        expose {}
        alias {rename proc unknown gets read fileevent fconfigure}
    }
}

# The above recommened minimums have the following rationale:
#       clock: We hide it because there is a MikroConf command with same name.
#       interp: We don't want the user to create new interpreters, because
#                   these new interpreters don't inherit the restrictions of
#                   the parent for Command Execution Speed.
#       info : We need to invoke at any time "$SLAVE invokehidden info ...".
#       unknown : This command is executed when the entered text is
#                 not recognized as a Tcl command. An attempt is made to
#                 recognize the command as a MikroConf command, in which case it
#                 executes it and returns whatever it is returned by the command.
#                 Otherwise, if the command is a single word 
#                   then the default behavior is to regard it as an attempt to
#                   connect to a host with that host name. Otherwise an error 
#                   message is displayed.
#       rename & proc : We provide secure versions of these commands
#               that name restrictions on user-defined procedures. 'proc' should be aliased in
#               bg Policy for user-registered procedures to be evaluated.
#       fconfigure & flush: We want to protect standard I/O from changing
#               its properties (buffering, blocking etc) regardless of the scope of
#               these changes (Slave alone, Slave-Session, or Slave-Session-Master)
#       fileevent : We have already a fileevent for stdin. We don't want the user
#               to register other fileevents for any standard I/O that would replace (or
#               compete) with the one already in place.
#       bgerror : Should always be available to report background errors, which
#               can occur with scripting off or on.
#       gets : Due to the nature of the I/O redirection from shell to MikroConf and
#                 back to shell, we cannot use Tcl's gets. Tcl's gets assumes that stdin
#                is in blocking mode, with echo enabled (as well as line buffering
#                and cannonical mode). We can not enable echo on MikroConf
#                because it is the shell that set the real terminal in raw & no-echo mode.
#               Therefore, we simulate echo and the standard behavior of 'gets'.
#       package : Due to a name collision with the same name MikroConf command.
#                 Afterall, all available packages and commands will be loaded by default.
#
#               
# Note: The following commands are left visible in purpose, but can be
#            made hidden or aliases.
#   close : The standard I/O can be closed on Slave, but this does not
#               affect the Session interpreter which is sharing these I/O. Of
#               course after closing the standard I/O, the user session will be
#               largely impaired, but it won't affect parallel or future sessions,
#               of the users ability to terminate the session with the escape
#               sequence.
#   after : Since Session & Slave interpreters share the same event
#               loop, and since this event loop will be activated (vwait) from
#               Session, any after command will be automatically scheduled
#               for execution inside Slave. This means that scheduled commands
#               can pop up while the user is in an undergoing async interaction
#               with a MikroConf command. This can cause scheduled
#               actions with 'after' and async actions from an on-going
#               MikroConf command to be intermingled. Although this can
#               distract/confuse the user, it has no other side-effects. 
#   interp : The 'interp' command of a safe interpreter cannot be used
#                to change the state of the safe interpreter itself.

# Creates a safe interpreter used to evaluate user's in-line scripting and applies a security policy to it.
#
# @assume The command "info" is available in a newly created safe interpreter.
#
# @param authmode The AUTHMODE of which the policy we are switching to.
proc policy {authmode} {    
    global sid
    variable Policy 
    Session $sid SLAVE INLINE
    Session -rw $sid COMMANDS ALIASES HIDDEN

    log::Debug "Enforcing Tcl security policy in session \"$sid\""
    
    if {[interp exists $SLAVE]} {
        interp delete $SLAVE
    }    
    interp create -safe $SLAVE

    # Share standard I/O.
    # Note: If the user closes them in Slave, they remain open in Session interpreter.
    interp share {} stdin $SLAVE
    interp share {} stdout $SLAVE
    interp share {} stderr $SLAVE
    
    # Record initial state in these variables.
    set COMMANDS [$SLAVE eval [list info commands]] ;# This requires "info" to be available after safe interp creation
    set ALIASES [$SLAVE aliases]
    set HIDDEN [$SLAVE hidden]

    if {! $INLINE} {
        foreach cmd $COMMANDS {
            interp hide $SLAVE $cmd
        }
        foreach cmd $ALIASES {
            interp alias $SLAVE $cmd {}
        }
        interp alias $SLAVE unknown {} ::policy::slave_unknown $SLAVE
        interp alias $SLAVE bgerror {} ::policy::slave_bgerror $SLAVE
        return
    }
    
    # If a policy is not explicitly defined for this AUTHMODE, then use the most restrictive "user" policy.
    if {! [dict exists $Policy $authmode]} {
        set authmode "user"
    }
    
    set tohide [dict get $Policy $authmode hide]
    set toexpose [dict get $Policy $authmode expose]
    set toalias [dict get $Policy $authmode alias]

    foreach cmd [::struct::set union $COMMANDS $toalias] {
        if {$cmd in $tohide} { ;# Note: aliases can be hidden, but only if there is no original command of same name already hidden.
            interp hide $SLAVE $cmd
        } elseif {$cmd in $toexpose} {
            interp alias $SLAVE $cmd {} ::policy::slave_all $SLAVE $cmd
        } elseif {[set pos [lsearch -exact -index 0 $toalias $cmd]] >= 0} {
            catch { interp hide $SLAVE $cmd }
            if {[llength [lindex $toalias $pos]] == 1} {
                interp alias $SLAVE $cmd {} ::policy::slave_${cmd} $SLAVE
            } else { ;# == 2
                interp alias $SLAVE $cmd {} [lindex $toalias $pos 1] $SLAVE
            }
        } else {
            interp hide $SLAVE $cmd
            interp alias $SLAVE $cmd {} ::policy::slave_all $SLAVE $cmd
        }
    }
    
    # Load registered user procedures in trusted sessions only.
    if {$authmode eq "priv" || $authmode eq "bg"} {
        loadprocs
    }
}

# Load all registered procedures to this session.
proc loadprocs {} {
    global sid
    Session $sid SLAVE

    log::Debug "Loading procedures in session \"$sid\""
    
    foreach name [procedures getlist] {
        lassign [procedures getproc $name] args body
        if {[catch {
            $SLAVE invokehidden proc $name $args $body
        } errMsg errStack]} {
            log::Alert -stack $errStack "Failed to load user procedures in user session $sid: " $errMsg
        }
    }
}

# Activates/Deactivates/Resets in-line scripting on the fly.
# Scripting is deactivated by recreating the Slave interpeter, having all Tcl commands,
# and procedures hidden, except for "unknown" and "bgerror".
#
# @param action "on" or "off".
proc scripting {action} {
    global sid
    Session $sid SLAVE AUTHMODE
    Session -rw $sid INLINE

    switch -exact -- $action {
        "off" {
            set INLINE 0
        }
        "on" {
            set INLINE 1
        }
        default {
            log::Error -error "invalid subcommand"
        }
    }
    
    policy $AUTHMODE
}

################################
# Wrappers section
################################

# Few words regarding wrapper commands:
#
# The wrappers enforce some security checks on the respective commands. For example,
# preventing the user from messing up with stdout, stderr and stdin. However, the command itself is
# eventually executed in the Slave interpreter after having (potentially) being modified in
# the Session. We do this by invoking hidden commands on the Slave/Safe interpreter.

# All commands in Slave are aliases to Session wrappers. If a command doesn't have
# a unique wrapper, it is linked to slave_all wrapper, which in turn calls the hidden command
# on the Slave. By having all commands hidden we can always recover them, whereas
# at the same time the user can rename overwrite aliases like normal. Pictorically, when
# the user executes a buildin Tcl command on the Slave, the following sequence of events
# occurs:
#
#   Slave: fconfigure "..." --> Session: slave_fconfigure "...." --> Slave: invoke hidden fconfigure "..."
#

# Wrapper for all the commands that don't have a wrapper of their own.
#
# @param slave The name of the interpreter from which this procedure was called
# @param name The name of the command executed on the Slave interpreter
# @param args The remaining command arguments (if any)
proc slave_all {slave name args} {
    $slave invokehidden $name {*}$args
}

# Wrapper for 'gets' to use the version of the Session.
#
# @param slave The name of the interpreter from which this procedure was called
# @param args The remaining command arguments (if any)
proc slave_gets {slave args} {
    nbgets {*}$args
}

# Wrapper for 'fconfigure'
# We don't allow the user to mess up with the fconfigure settings of stdout, stderr and stdin.
#
# @assume The syntax of the command will continue have channelId as its first argument.
#
# @param slave The name of the interpreter from which this procedure was called
# @param channelId The channel to operate on
# @param args The remaining command arguments (if any)
proc slave_fconfigure {slave channelId args} {    
    if {$charnnelId in [list stdout stderr stdin]} {
        error "action not permitted"
    }
    
    $slave invokehidden fconfigure $channelId {*}$args
}

# Wrapper for 'read'
# We don't allow the user to read from stdin.
#
# @assume The syntax of the command will not change in future versions of Tcl, or we need to update this code
#
# @param slave The name of the interpreter from which this procedure was called
# @param args The standard command arguments for read (if any)
proc slave_read {slave args} {
    if {"stdin" in $args} {
        error "action not permitted"
    }
    
    $slave invokehidden read {*}$args
}

# Wrapper for 'flush'
# We don't allow the user to mess up with stdout, stderr and stdin.
#
# @assume The syntax of the command will continue have channelId as its first argument.
#
# @param slave The name of the interpreter from which this procedure was called
# @param channelId The channel to operate on
# @param args The remaining command arguments (if any)
proc slave_flush {slave channelId} {    
    if {$charnnelId in [list stdout stderr stdin]} {
        error "action not permitted"
    }
    
    $slave invokehidden flush $channelId
}

# Wrapper for 'fileevent'
# We don't allow the user to mess up with the fconfigure settings of stdout, stderr and stdin.
#
# @assume The syntax of the command will continue have channelId as its first argument.
#
# @param slave The name of the interpreter from which this procedure was called
# @param channelId The channel to operate on
# @param args The remaining command arguments (if any)
proc slave_fileevent {slave channelId args} {    
    if {$charnnelId in [list stdout stderr stdin]} {
        error "action not permitted"
    }
    
    $slave invokehidden fileevent $channelId {*}$args
}

# Wrapper for 'rename' that enforce naming restrictions on user-defined procedures.
#
# @param slave The name of the interpreter from which this procedure was called
# @param args The regular Tcl rename arguments
proc slave_rename {slave args} {
    global sid
    
    if {[llength $args] != 2} {
        error {wrong # args: should be "rename oldName newName"}
    }
    lassign $args from to

    if {! [regexp {^[A-Z_0-9].*$} $from] || ! [regexp {^[A-Z_0-9].*$} $to]} {
        error "procedure names should start with a capital latin letter, underscore or number"
    }

    $slave invokehidden rename $from $to
}

# Get the $policy policy for $authmode authmode.
#
# @param authmode Name of authentication mode.
# @param policy Name of policy.
# @return A list describing the policy.
proc get_policy {authmode policy} {
    variable Policy
    if {[dict exists $Policy $authmode]} {
        return [dict get $Policy $authmode $policy]
    } else {
        return [dict get $Policy "user" $policy]
    }
}

# Wrapper for 'proc' that enforce naming restrictions on user-defined procedures.
#
# @param slave The name of the interpreter from which this procedure was called
# @param args The regular Tcl proc arguments
proc slave_proc {slave args} {
    global sid
    
    if {[llength $args] != 3} {
        error {wrong # args: should be "proc name args body"}
    }
    lassign $args name params body
    
    if {! [regexp {^[A-Z_0-9].*$} $name]} {
        error "procedure name should start with a capital latin letter, underscore or number"
    } 
    
    $slave invokehidden proc $name $params $body
}

# This is the unknown procedure on the Slave, linked here as an alias.
# If it is executed it means that the provided command is neither a build-in
# Tcl command, nor a user-defined procedure. Hence we first check to see if
# its a MikroConf command. If not recognized as MikroConf command, then if
# it is a single argument, unknown assumes that it is a hostname that
# we want to connect to, otherwise it displays an error.
#
# @param slave The name of the interpreter from which this procedure was called
# @param args The command name and arguments that are not known to the interpreter
# @error
proc slave_unknown {slave args} {
    global sid ptid
   
    if {[tsv::get $sid TERM]} { return } ;# Do nothing. Skip any command.
    
    # If recognized as a MikroConf command, give it to Master for execution
    if {[sysconf match $sid -count $args 0]} {
        return [Thread::bgsend $ptid [list sysconf execute $sid $args]]
    }
    
    Session $sid TRYCONNECT
    if {$TRYCONNECT} {
        if {[llength $args] != 1} {
            error "invalid command name \"[lindex $args 0]\""
        }

        # Note: "exec" bypasses shell interpretation and substitutions, so we don't have to
        #       worry of specially crafted input to exec that tries to circumvent security. An
        #       example is: exec -- telnet {; cat /etc/passwd}.
        #       Telnet will literally try to connect to host {; cat /etc/passwd}, and it will fail.
        # We don't propagate a possible error in purpose, because the error is printed 
        # by telnet on stderr, and we don't want to print a second error message. 
        ptyexec telnet $args
    } else {
        error "invalid command name \"[lindex $args 0]\""
    }
}

# Error handling procedure for slave interpreters.
#
# @param slave The slave interpreter that generated the error
# @param msg The error message to display
proc slave_bgerror {slave msg} {
    # errorInfo and errorCode hold info regarding the error, but we don't use them
    puts "background error occured: $msg"
}

} ;# End of Namespace

Overview | Index by: file name | procedure name | procedure call | annotation
File generated 2010-03-13 at 22:28.