如何防止Django应用程序通过系统调用执行任意命令?

5
我正在开发一个Django应用程序,必须对服务器上的外部程序进行系统调用。在创建系统调用命令时,应用程序从表单中获取值,并将它们用作调用的参数。我想这意味着人们可以使用虚假参数并编写shell执行的任意命令(例如,只需放置一个分号,然后是rm -rf *)。
这很糟糕。虽然大多数用户不是恶意的,但这是潜在的安全问题。如何处理这些潜在的利用点?
编辑(以澄清):用户将看到一个表单,其中各个字段用于每个参数和选项。但是,一些字段将作为开放文本字段提供。所有这些字段都会组合在一起并传递给subprocess.check_call()。从技术上讲,这与仅向用户提供命令提示符没有太大区别。这肯定是相当常见的,那么其他开发人员怎样净化输入,以避免出现Bobby Tables
5个回答

11

根据我理解的问题,我假设您不允许用户在shell上指定要运行的命令,而只是要对这些命令提供参数。在这种情况下,您可以使用subprocess模块,并使用shell(即在subprocess.Popen构造函数中指定默认的shell=False参数),以避免shell注入攻击。

哦,并且永远不要使用os.system()来处理任何包含来自用户的输入的字符串。


通过阅读http://blog.littleimpact.de/index.php/2008/08/11/avoiding-shell-injection-in-ruby-python-and-php/,我发现只要shell=False(默认设置),我就没问题了。(也就是说,只会运行一个命令。)这也是你的意思吗? - gotgenes

6
永远不要相信用户。来自Web浏览器的任何数据都应被视为污染数据。绝对不要尝试通过JS验证数据或通过限制FORM字段中可以输入的内容来验证数据。您需要在将数据传递给外部应用程序之前在服务器上进行测试。
在您编辑后更新:无论您如何在前端向用户呈现表单,后端都应将其视为来自一组文本框,并在其周围闪烁着大文本“在此处插入任何内容!”

好观点。我不会假设客户端会负责并验证;在运行命令之前,我会在服务器端进行验证。但为了让其他人受益,你说这个还是很值得的。 - gotgenes

4
要完成这个任务,你需要按照以下步骤进行操作。如果你不知道“选项”和“参数”是什么,请阅读optparse背景
每个“命令”或“请求”实际上都是一个模型的实例。使用所有可能提供的参数定义您的请求模型。
1. 对于简单选项,您必须提供一个具有特定CHOICES列表的字段。对于“开启”或“关闭”选项(在命令行中为-x),您应该提供一个CHOICE列表,其中包含两个易于理解的值(“执行X”和“不执行X”)。 2. 对于带有值的选项,您必须提供一个接受选项值的字段。您必须编写一个表单来验证此字段。稍后我们将回到选项值验证。 3. 对于参数,您需要一个第二个模型(具有对第一个模型的FK)。这可以是一个简单的FilePath字段,也可以更复杂。同样,您可能需要提供一个表单来验证此模型的实例。
选项验证因选项类型而异。您必须将可接受的值缩小为最窄的字符集,并编写一个绝对确定仅传递有效字符的解析器。
您的选项将分为与optparse中的选项类型相同的类别--字符串、int、long、choice、float和complex。请注意,Django的模型和表单已经定义了int、long、float和complex的验证规则。Choice是一种特殊的字符串,已被Django的模型和表单支持。
剩下的是“字符串”。定义允许的字符串。编写这些字符串的正则表达式。使用正则表达式进行验证。大多数情况下,您永远不可能接受引号("'或`)以任何形式。
最后一步。您的模型有一个方法,它将命令作为一系列字符串发出,准备好供subprocess.Popen使用。
编辑
这是我们应用程序的支柱。它非常常见,我们有一个单一的模型,具有多个表单,每个表单都是运行特殊批处理命令的特定方式。模型非常通用。表单是构建模型对象的特定方式。这就是Django设计的方式,并且有助于与Django精心设计的设计模式相匹配。
任何“可用作开放文本字段”的字段都是错误的。每个“打开”的字段必须具有指定允许的内容的正则表达式。如果无法形式化正则表达式,则必须重新考虑正在进行的操作。
绝对无法使用正则表达式约束的字段不能成为命令行参数。句号。必须将其存储到文件或数据库列中,然后再使用。

就像这样。

class MySubprocessCommandClass( models.Model ):
    myOption_1 = models.CharField( choice = OPTION_1_CHOICES, max_length=2 )
    myOption_2 = models.CharField( max_length=20 )
    etc.
    def theCommand( self ):
        return [ "theCommand", "-p", self.myOption_1, "-r", self.myOption_2, etc. ]

您的表单是该模型的ModelForm。

您不需要手动执行save()方法保存模型实例。我们执行保存操作是为了创建一个准确记录所执行操作的日志。


感谢您的回答。Rick Copeland的回答表明,在subprocess模块中包含了安全性,只要shell=False,就会执行一个且仅一个命令;在初始命令之后,所有其他部分都被视为命令的参数。由于我控制着开头的一个命令,所以我是否真的需要为参数设计正则表达式呢? - gotgenes
另外,我很好奇,为什么需要一个模型?正如您所说的那样,我很难将所有内容放在一起。我们的表单已经表示为forms.Form的子类;目前我们正在使用它进行验证。这如何与模型联系起来?你提到的命令是指django.core.management.base中的(Base)Command类吗? - gotgenes
与管理命令层次结构无关。不,这个命令是完全独立的。 - S.Lott
非常感谢通过示例代码的澄清!这非常有帮助。 - gotgenes

3
答案是,不要让用户输入shell命令!允许执行任意shell命令是没有任何借口的。
此外,如果你真的必须允许用户提供外部命令的参数,请不要使用shell。在C语言中,你可以使用execvp()直接向命令提供参数,但在django中,我不确定该如何做到这一点(但我相信有办法)。当然,你仍然应该进行一些参数清理工作,特别是如果该命令有可能造成任何损害。

0

根据您的命令范围,您可以自定义表单,以便在单独的表单字段中输入参数。这些参数可以更轻松地解析适合的值。 此外,请注意反引号和其他特定于shell的内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接