使用带有用户输入的 Python F 字符串是否存在任何安全问题

信息安全 开发 Python 格式字符串
2021-09-08 08:10:13

背景

不久前,我开始在 Python 中使用 F 字符串,但记得在将它们与用户输入一起使用时会遇到一些安全问题,因此我特意在这些情况下不使用它们。

问题

在用户输入中使用 python f 字符串是否存在安全问题。例如,用户是否可以访问他们不应该访问的信息。

例子

$ ./hello.py mike
hi mike
#!/usr/bin/python3

import sys

secrete = 'my secrete'

print(F"hi {sys.argv[1]}")

这个程序基本上是一个接受用户输入的基本 hello world。攻击者是否有可能提供会泄露秘密变量或任何其他有价值数据的输入?

3个回答

Python 的 f 字符串实际上更安全。使用它们!


当格式字符串依赖于不受信任的数据时,字符串格式可能很危险。因此,在使用 str.format()or %-formatting 时,使用静态格式字符串或在应用 formatter 函数之前清理不受信任的部分非常重要。相比之下,f 字符串实际上并不是纯字符串,而更像是用于连接字符串和表达式的语法糖。因此,f-string 的格式是预先确定的,并且首先不允许动态(可能不受信任)部分。

旧式格式与 str.format()

>>> data_str = 'bob'
>>> format_str = 'hello {name}!'
>>> format_str.format(name=data_str)
'hello bob!'

在这里,您的 Python 解释器不知道数据字符串和格式字符串之间的区别。它只是调用一个函数 ,str.format()该函数在执行时对格式字符串值运行替换算法。因此,可以预期,格式只是一个带有花括号的普通字符串:

>>> import dis
>>> dis.dis("'hello {name}!'")
  1           0 LOAD_CONST               0 ('hello {name}!')
              2 RETURN_VALUE

使用 f 字符串的新型格式

>>> data_str = 'bob'
>>> f'hello {data_str}!'
'hello bob!'

在这里,f'hello {data_str}!'可能看起来像一个字符串常量,但它不是。解释器不会将之间{...}的部分解析为稍后可能扩展的字符串的一部分,而是作为单独的表达式:

>>> dis.dis("f'hello {name}!'")
  1           0 LOAD_CONST               0 ('hello ')
              2 LOAD_NAME                0 (name)
              4 FORMAT_VALUE             0
              6 LOAD_CONST               1 ('!')
              8 BUILD_STRING             3
             10 RETURN_VALUE

因此,将 ."hi {sys.argv[1]}"视为(大约)语法糖"hi " + sys.argv[1]在运行时,解释器甚至并不真正知道或关心您使用了 f 字符串。它只看到从常量"hi "和格式化值构建字符串的指令sys.argv[1]

易受攻击的例子

这是一个str.format()以易受攻击的方式使用的示例 Web 应用程序:

from http.server import HTTPServer, BaseHTTPRequestHandler

secret = 'abc123'

class Handler(BaseHTTPRequestHandler):
    name = 'funtimes'
    msg = 'welcome to {site.name}'
    def do_GET(self):
        res = ('<title>' + self.path + '</title>\n' + self.msg).format(site=self)
        self.send_response(200)
        self.send_header('content-type', 'text/html')
        self.end_headers()
        self.wfile.write(res.encode())

HTTPServer(('localhost', 8888), Handler).serve_forever()
$ python3 example.py

$ curl 'http://localhost:8888/test'
<title>/test</title>
welcome to funtimes

攻击

构建字符串时res,它会self.path用作格式字符串的一部分。由于self.path是用户控制的,我们可以使用它来更改格式字符串,例如泄露全局变量secret

$ curl -g 'http://localhost:8888/XXX{site.do_GET.__globals__[secret]}'
<title>/XXXabc123</title>
welcome to funtimes

如果这个基本的语言特性有这么大的缺陷,它可能根本就不是一个特性。只要格式字符串的内容在开发时由程序员控制,用户就无法滥用它们。

花括号的内容被评估,但该评估的结果不会再次评估(即sys.argv[1]评估为“1 + 1”,但不会再次评估,就像你看到的那样)。

当用户能够在格式化之前将数据注入字符串时,就会出现问题。请参阅此挑战示例虽然这不适用于 f 字符串,但它很好地演示了如果允许用户控制格式可能会发生的攻击。

看看下面这个简单的例子:

import sys

secret = "My secret"

print(f"From argv: {sys.argv[1]}\n")
print(f"From code: {print(secret)}")

如果你运行它python test.py print\(secret\)或者python test.py "print(secret)"结果是:

From argv: print(secret)

My secret
From code: None

该参数被简单地视为一个字符串,它不会被执行。但是,我不是100% 确定没有办法强制 Python 以某种方式执行它。我也不确定如果数据来自另一个输入(例如套接字)会发生什么。