16

Background

A while ago I started using F strings in Python but remembered seeing some security concerns with using them with user input so I have made a point of not using them for those situations.

Question

Are there security concerns to using python f strings with user input. For example, is it possible for a user to gain access to information that they shouldn't have access to.

Example

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

import sys

secrete = 'my secrete'

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

This program is basically a basic hello world that takes user input. Is it possible for an attacker to supply an input that would exfiltrate the secrete variable or any other valuable data?

2
  • It depends entirely on what you do with it. Your new string contains user input so if, for example, you pass it off to os.system you now have RCE. Commented Sep 15, 2020 at 18:31
  • ok, so I'm aware of that, but that would be the same with any user input method. Since the F string is computed at runtime, I'm wondering if you can force it to eval a statement like if I passed in 1+1. I did try that and it just interprets it as a string so no problem there.
    – MikeSchem
    Commented Sep 15, 2020 at 18:57

4 Answers 4

22

Python's f-strings are actually safer. Use them!


String formatting may be dangerous when a format string depends on untrusted data. So, when using str.format() or %-formatting, it's important to use static format strings, or to sanitize untrusted parts before applying the formatter function. In contrast, f-strings aren't actually plain strings, but more like syntactic sugar for concatenating strings and expressions. As such, an f-string's format is predetermined and doesn't allow dynamic (potentially untrusted) parts in the first place.

Old-style formatting with str.format()

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

Here, your Python interpreter doesn't know the difference between a data string and a format string. It just calls a function, str.format(), which runs a replacement algorithm on the format string value at the moment of execution. So, expectedly, the format is just a plain string with curly braces in it:

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

New-style formatting with f-strings

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

Here, f'hello {data_str}!' may look like a string constant, but it's not. The interpreter doesn't parse the part between {...} as part of the string that may be expanded later, but as a separate expression:

>>> 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

So, think of "hi {sys.argv[1]}" as (approximately) syntactical sugar for "hi " + sys.argv[1]. At run time, the interpreter doesn't even really know or care that you used an f-string. It just sees instructions to build a string from a the constant "hi " and the formatted value of sys.argv[1].

Vulnerable example

Here is a sample web app which uses str.format() in a vulnerable way:

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

Attack

When the res string is built, it usesself.path as part of the format string. Since self.path is user-controlled, we can use it to alter the format string and e.g. exfiltrate the global variable secret:

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

If this basic language feature were this flawed, it probably wouldn't be a feature at all. As long as the contents of the format string are controlled by the programmer at development time, there is nothing that a user can do to abuse them.

The contents of the curly braces are evaluated, but the result of that evaluation is not evaluated again (i.e. sys.argv[1] is evaluated to "1+1", but is not evaluated again, like you have seen).

The problem arises when a user is able to to inject data into a string before it is formatted; see this challenge example. While this is not for an f-string, it is a good demonstration of the attacks that are possible if the user is allowed to control the formatting.

1

The security concern about the f-string is that you don't want to use it as a template directly.

The ability to execute code in the f-string is much more powerful than calling .format() of a string template.

For example, below is what the f-string can do but call .format() can not:

>>> d = {'name': 'Jarvis', 'age': 1}
>>> f'{d["name"]}'  # access a dict via keys
'Jarvis'
>>> f'{d.keys()}'  # call functions
"dict_keys(['name', 'age'])"
>>> f'{(lambda x: x + 1)(d["age"])}'  # write a lambda
'2'

Python locks the ability of f-string by ensuring any f-string in the code cannot be delay evaluated. So actually, you cannot use an f-string as a dynamic template like from the user's input. For example, you cannot read a string from an HTTP request's query string and use it as an f-string.

The only exception is to explicitly use eval() on a string to make it an f-string. Like

f_string_tmpl = 'My name is {name}.'
eval(f'f\'{f_string_tmpl}\'', {}, {'name': 'Wang'})

Or we can say that the f-string is pretty safe, since unless we eval() we shall keep everything under control.

0

Take a look at the following simple example:

import sys

secret = "My secret"

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

If you run it with python test.py print\(secret\) or python test.py "print(secret)" the result is:

From argv: print(secret)

My secret
From code: None

The argument is simply treated as a string, it is not executed. However, I am not 100 % sure there is no way to force Python to somehow execute it. I am also not sure what would happen if the data came from some another input, like socket for instance.

6
  • 1
    @MikeSchem you have destroyed the example. Why have you removed the print from the print(f"From code: {secret}"). It has been crucial to show what is going on. Revert the changes because now it makes no sense.
    – Al Bundy
    Commented May 12, 2021 at 4:29
  • Ok, I was unsure what you were trying to demonstrate. I see now.
    – MikeSchem
    Commented May 12, 2021 at 18:33
  • Doesn't this sample prove that it does execute the print()?
    – MikeSchem
    Commented May 12, 2021 at 18:35
  • It executes the print, but not the one provided by the user as an argument.
    – Al Bundy
    Commented May 13, 2021 at 14:06
  • Are you sure? It looks like line 3 in your output is printing out the secrete.
    – MikeSchem
    Commented May 13, 2021 at 17:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .