10.7. Tcl

Tcl 代表 “工具命令语言”,发音为 “tickle”。Tcl 分为两个部分:语言和库。该语言是一种简单的语言,最初旨在向交互式程序发出命令,并包含基本的编程功能。该库可以嵌入到应用程序中。您可以在诸如 Tcl.tkTcl WWW Info 网页以及 comp.lang.tcl FAQ 启动页面 http://www.tclfaq.wservice.com/tcl-faq 等网站找到更多关于 Tcl 的信息。感谢 Wojciech Kocjan 提供了关于在安全应用程序中使用 Tcl 的一些详细信息。

对于某些安全应用程序,Tcl 中特别有趣的组件是 Safe-Tcl(它在 Tcl 中创建一个沙箱)和 Safe-TK(它为 Safe Tcl 实现一个沙箱化的可移植 GUI),以及 WebWiseTclTk 工具包,它允许从万维网上的任何位置自动定位和加载 Tcl 包。您可以从 http://www.cbl.ncsu.edu/software/WebWiseTclTk 找到更多关于后者的信息。我不清楚它受到了多少代码审查。

Tcl 最初的设计目标是成为一种小型、简单的语言,这导致最初的语言在某种程度上具有局限性且速度较慢。有关原始语言的局限性弱点的示例,请参阅 Richard Stallman 的“为什么你不应该使用 Tcl”。例如,Tcl 最初设计为仅支持一种数据类型(字符串)。值得庆幸的是,这些问题已经随着时间的推移得到了解决。特别是,版本 8.0 增加了对更多数据类型的支持(整数在内部存储为整数,列表存储为列表等等)。这提高了它的能力,特别是提高了它的速度。

与几乎所有脚本语言一样,Tcl 也有一个 “eval” 命令,用于解析和执行任意 Tcl 命令。与所有此类脚本语言一样,这个 eval 命令需要特别小心地使用,否则攻击者可能会在输入中插入字符以导致恶意事件发生。例如,攻击者可能能够插入对 Tcl 具有特殊含义的字符,例如嵌入的空格(包括空格和换行符)、双引号、花括号、方括号、美元符号、反斜杠、分号或井号(或创建输入以导致在处理过程中创建这些字符)。这也适用于任何将数据传递给 eval 的函数(取决于 eval 的调用方式)。

这是一个小例子,可能会使这个概念更清晰;首先,让我们定义一个小函数,然后直接交互式地调用它 - 请注意,这些用法是没问题的
 proc something {a b c d e} {
       puts "A='$a'"
       puts "B='$b'"
       puts "C='$c'"
       puts "D='$d'"
       puts "E='$e'"
 }
 
 % # This works normally:
 % something "test 1" "test2" "t3" "t4" "t5"
 A='test 1'
 B='test2'
 C='t3'
 D='t4'
 E='t5'
 
 % # Imagine that str1 is set by an attacker:
 % set str1 {test 1 [puts HELLOWORLD]}
 
 % # This works as well
 % something $str1 t2 t3 t4 t5
 A='test 1 [puts HELLOWORLD]'
 B='t2'
 C='t3'
 D='t4'
 E='t5'
然而,继续这个例子,让我们看看如何不正确和正确地调用 “eval”。如果你以不正确(危险)的方式调用 eval,它允许攻击者滥用它。然而,通过使用像 list 或 lrange 这样的命令来正确地对输入进行分组,你可以避免这个问题
 % # This is the WRONG way - str1 is interpreted.
 % eval something $str1 t2 t3
 HELLOWORLD
 A='test'
 B='1'
 C=''
 D='t2'
 E='t3'
 
 % # Here's one solution, using "list".
 % eval something [list $str1 t2 t3 t4 t5]
 A='test 1 [puts HELLOWORLD]'
 B='t2'
 C='t3'
 D='t4'
 E='t5'
 
 % # Here's another solution, using lrange:
 % eval something [lrange $str1 0 end] t2
 A='test'
 B='1'
 C='[puts'
 D='HELLOWORLD]'
 E='t2'
当将参数连接到被调用的函数时,使用 lrange 非常有用,例如,对于使用回调的更复杂的库。在 Tcl 中,eval 通常用于创建一个函数的单参数版本,该函数接受可变数量的参数,并且在这种方式下使用时需要小心。这是另一个例子(假设您已经定义了一个 “printf” 函数)
 proc vprintf {str arglist} {
      eval printf [list $str] [lrange $arglist 0 end]
 }
 
 % printf "1+1=%d  2+2=%d" 2 4
 % vprintf "1+1=%d  2+2=%d" {2 4}

从根本上说,当传递一个最终将被评估的命令时,你必须将 Tcl 命令作为正确构建的列表传递,而不是作为(可能连接的)字符串。例如,“after” 命令在给定的毫秒数后运行一个 Tcl 命令;如果 $param1 中的数据可以被攻击者控制,那么这段 Tcl 代码是极其错误的
  # DON'T DO THIS if param1 can be controlled by an attacker
  after 1000 "someCommand someparam $param1"
这是错误的,因为如果攻击者可以控制 $param1 的值,攻击者就可以控制程序。例如,如果攻击者可以使 $param1 具有 '[exit]',那么程序将退出。此外,如果 $param1 是 '; exit',它也会退出。

因此,正确的替代方案是
 after 1000 [list someCommand someparam $param1]
甚至更好的方案是像下面这样的
 set cmd [list someCommand someparam]
 after 1000 [concat $cmd $param1]

这是另一个例子,展示了你不应该做什么,假设 $params 是可能由恶意用户控制的数据
 set params "%-20s TESTSTRING"
 puts "'[eval format $params]'"
将导致
 'TESTSTRING       '
但是,当不受信任的用户发送带有嵌入换行符的数据时,就像这样
 set params "%-20s TESTSTRING\nputs HELLOWORLD"
 puts "'[eval format $params]'"
结果将是这样(请注意,攻击者的代码被执行了!)
 HELLOWORLD
 'TESTINGSTRING       '
Wojciech Kocjan 建议在这种情况下最简单的解决方案是使用 lrange 将其转换为列表,这样做
 set params "%-20s TESTINGSTRING\nputs HELLOWORLD"
 puts "'[eval format [lrange $params 0 end]]'"
结果将是
 'TESTINGSTRING       '
请注意,此解决方案假定潜在的恶意文本被连接到文本的末尾;与所有语言一样,请确保攻击者无法控制格式文本。

作为一种风格,在使用 if、while、for、expr 以及任何其他使用 expr/eval/subst 解析参数的命令时,始终使用花括号。这样做将避免在使用 Tcl 时常见的错误,称为意外的双重替换(又称双重替换)。这最好通过示例来解释;以下代码是不正确的
 while ![eof $file] {
     set line [gets $file]
 }
代码是不正确的,因为 “![eof $file]” 文本将在首次执行 while 命令时由 Tcl 解析器评估,而不是像应该的那样在每次迭代中重新评估。相反,这样做
 while {![eof $file]} {
      set line [gets $file]
 }
请注意,条件和要执行的操作都用花括号括起来。虽然在某些情况下花括号是多余的,但它们永远不会有害,当你未能包含在需要花括号的地方(例如,在进行细微更改时),通常会导致细微且难以发现的错误。

有关良好的 Tcl 风格的更多信息,请参见诸如 Ray Johnson 的 Tcl 风格指南 等文档。

过去,我曾说过我不建议使用 Tcl 编写必须调解安全边界的程序。自那时以来,Tcl 似乎有所改进,因此虽然我不能保证 Tcl 会满足您的需求,但我也不能保证任何其他语言也适用于您。再次感谢 Wojciech Kocjan 提供了关于如何编写用于安全应用程序的 Tcl 代码的一些建议。