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
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
>>> 
🌐
GitHub
github.com › tonybaloney › pycharm-security › issues › 229
STR100: Calling format with insecure string. Found in '.format(text)'. · Issue #229 · tonybaloney/pycharm-security
Describe the bug I have a class that has a method titled format. This extension is marking calls to my custom method as insecure. I believe this is a bug because this extension is intended to only run on .format of strings. To Reproduce ...
Author   tonybaloney

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
🌐
Wikipedia
en.wikipedia.org › wiki › Uncontrolled_format_string
Uncontrolled format string - Wikipedia
December 21, 2025 - In particular, the varargs mechanism ... off the call stack as they wish, trusting the early arguments to indicate how many additional arguments are to be popped, and of what types. Format string bugs can occur in other programming languages besides C, such as Perl, although they appear with less frequency ...
🌐
GitHub
github.com › Ericsson › secure_coding_one_stop_shop_for_python › blob › main › CWE-664 › CWE-134 › README.md
secure_coding_one_stop_shop_for_python/CWE-664/CWE-134/README.md at main · Ericsson/secure_coding_one_stop_shop_for_python
In Python, the use of string formatting combined with the ability to access a function's __globals__ attribute can exposing internal variables and methods unless properly guarded.
Author   Ericsson
🌐
University of Washington
homes.cs.washington.edu › ~djg › papers › format_string.pdf pdf
Preventing Format-String Attacks via Automatic and Efficient Dynamic Checking
With our approach, however, the · protection is automatic—the user does not have to change ... Other compile-time approaches are less complete. For ex- ample, Alan DeKok’s PScan [9] finds printf call sites where · the format string is both non-static and the final parame-
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.

Top answer
1 of 2
5

The Clang & GCC compilers check that format strings and the supplied arguments conform, they cannot do this if the format string is not a literal - hence the error message you see as you are obtaining the format string from the bundle.

To address this issue there is an attribute, format_arg(n) (docs), to mark functions which take a format string; alter it in some way without changing the actual format specifiers, e.g translate it; and then return it. Cocoa provides the convenient macro NS_FORMAT_ARG(n) for this attribute.

To fix your problem you need to do two things:

  1. Wrap up the call to NSBundle in a function with this attribute specified; and

  2. Change your "key" to include the format specifiers.

Second first, your strings file should contain:

"name %@" = "My name is %@"

so the key has the same format specifiers as the result (if you need to reorder the specifiers for a particular language you use positional format specifiers).

Now define a simple function to do the lookup, attributing it as a format translation function. Note we mark it as static inline, using the macro NS_INLINE as a hint to the compiler to both inline it into your macro expansion; the static allows you to include it in multiple files without symbol clashes:

NS_INLINE NSString *localize(NSString *string) NS_FORMAT_ARGUMENT(1);
NSString *localize(NSString *string)
{
   return [[NSBundle mainBundle] localizedStringForKey:string value:@"" table:nil];
}

And your macro becomes:

#define GetLocalStr(key, ...) [NSString stringWithFormat:localize(key), ##__VA_ARGS__]

Now when you:

GetLocalStr(@"name %@", @"Foo")

You will get both the localised format string and format checking.

Update

After Greg's comment I went back and checked - I had reproduced your error and so assumed it was down to a missing attribute. However as Greg points out localizedStringForKey:value:table: already has the attribute, so why the error? What I had absentmindedly done in reproducing your error was:

NSLog( GetLocalStr( @"name %@", @"Foo" ) );

and the compiler pointed at the macro definition and not that line - I should have spotted the compiler was misleading me.

So where does that leave you? Maybe you've done something similar? The key is that a format string must either be a literal or the result of a function/method attributed as a format translating function. And don't forget, you must also had the format specifier to your key as above.

Update 2

After your additional comments what you need to use is function, rather than a macro, along with the format attribute, for which Cocoa provides the convenient NS_FORMAT_FUNCTION(f,a) macro. This attribute informs the compiler that the function is a formatting one, the value of f is the number of the format string and a is the number of the first argument to the format. This gives the function declaration:

NSString *GetLocalStr(NSString *key, ...) NS_FORMAT_FUNCTION(1,2);

and the definition (assuming ARC):

NSString *GetLocalStr(NSString *key, ...)
{
   va_list args;
   va_start(args, key);
   NSString *format = [[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil];
   NSString *result = [[NSString alloc] initWithFormat:format arguments:args];
   va_end (args);
   return result;
}

(which is essentially the same as @A-Live's).

Uses of this will be checked appropriately, for example:

int x;
...
NSString *s1 = GetLocalStr(@"name = %d", x); // OK
NSString *s2 = GetLocalStr(@"name = %d");    // compile warning - More '%" conversions than data arguments
NSString *s3 = GetLocalStr(@"name", x);      // compile warning - Data argument not used by format string
NSString *s4 = GetLocalStr(@"name");         // OK
2 of 2
5

This variant produces no warnings (as there's always a va_list):

NSString* GetLocalStr1(NSString *formatKey, ...)  {
    va_list ap;
    va_start(ap, formatKey);
    NSString * format = [[NSBundle mainBundle] localizedStringForKey:formatKey value:@"" table:nil];
    NSString *result =  [[NSString alloc] initWithFormat:format arguments:ap];
    va_end (ap);
    return [result autorelease];
}

...

__unused NSString * str = GetLocalStr1( @"name", @"Foo" );
__unused NSString * str1 = GetLocalStr1( @"TestNoArgs" );
__unused NSString * str2 = GetLocalStr1( @"TestArgs", @"Foo" );

NSLog(@"%@", str);
NSLog(@"%@", str1);
NSLog(@"%@", str2);

Result:

my name is Foo

TestNoArgs

Hello world Foo

It doesn't answer the question exactly but might help you to avoid warnings until the solution is found.

Find elsewhere
🌐
Security Boulevard
securityboulevard.com › home › cybersecurity › threats & breaches › vulnerabilities › the problem of string concatenation and format string vulnerabilities
The Problem of String Concatenation and Format String Vulnerabilities - Security Boulevard
June 27, 2019 - So what happens if you have a printf call without parameters, as below? ... It will simply grab whatever data is on the stack and print it in hex format. This may include stack and return addresses, stack cookies (a security mechanism that aims to prevent buffer overflow exploitation), the content of variables and function parameters, and everything else that is immensely useful for an attacker. So if the format string in printf is user controllable, that’s incredibly dangerous.
🌐
CodeQL
codeql.github.com › codeql-query-help › csharp › cs-uncontrolled-format-string
Uncontrolled format string — CodeQL query help documentation
Click to see the query in the CodeQL repository · Passing untrusted format strings to String.Format can throw exceptions and cause a denial of service. For example, if the format string references a missing argument, or an argument of the wrong type, then System.FormatException is thrown
🌐
Invicti
invicti.com › blog › web-security › format-string-vulnerabilities
What Are Format String Vulnerabilities?
As long as user_input is guaranteed to contain no format specifiers, this is fine. But if that value is controlled by the user, an attacker can exploit format string syntax to trigger a variety of dangerous behaviors.
🌐
Stack Overflow
stackoverflow.com › questions › 75930454 › warning-format-string-is-not-a-string-literal-potentially-insecure-in-c
file - Warning: "format string is not a string literal (potentially insecure)" in C - Stack Overflow
... If you want to know what might go wrong, take a look at xkcd-Bobby Tables That does not exactly match for printf but you should get the idea... ... Not causing your error but scanf("%s", fname); should be changed to contain a length limit: ...
🌐
GeeksforGeeks
geeksforgeeks.org › vulnerability-in-str-format-in-python
Vulnerability in str.format() in Python | GeeksforGeeks
January 6, 2025 - Person Class: This class defines a template for creating people with s1 and s2 attributes. fun Function: This function dynamically formats a string using the person object.
🌐
OWASP Foundation
owasp.org › www-community › attacks › Format_string_attack
Format string attack | OWASP Foundation
The attack could be executed when the application doesn’t properly validate the submitted input. In this case, if a Format String parameter, like %x, is inserted into the posted data, the string is parsed by the Format Function, and the conversion specified in the parameters is executed.
🌐
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 - For as long as only C interpreter objects are passed to the format string you are somewhat safe because the worst you can discover is some internal reprs like the fact that something is an integer class above.
🌐
Podalirius
podalirius.net › en › articles › python-format-string-vulnerabilities
Python format string vulnerabilities · Podalirius
March 24, 2021 - Even though python format strings can be very useful in scripts, they should be used with caution as they can be prone to vulnerabilities.
🌐
BreakInSecurity
axcheron.github.io › exploit-101-format-strings
Exploit 101 - Format Strings - BreakInSecurity
April 22, 2018 - By doing so, the executable will cache the memory address in the GOT, so that it doesn’t have to ask libc each time an external function is called. The goal here will be to overwrite the address of exit() in the GOT with the address of hello(). There are 4 steps here : ... $ objdump -R format4 format4: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049888 R_386_GLOB_DAT __gmon_start__ 0804988c R_386_GLOB_DAT stdin@GLIBC_2.0 0804989c R_386_JUMP_SLOT printf@GLIBC_2.0 080498a0 R_386_JUMP_SLOT _exit@GLIBC_2.0 080498a4 R_386_JUMP_SLOT fgets@GLIBC_2.0 080498a8 R_386_JUMP_SLOT puts@GLIBC_2.0 080498ac R_386_JUMP_SLOT exit@GLIBC_2.0 # Address of exit() 080498b0 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0 $ objdump -t format4 | grep hello 080484cb g F .text 0000002e hello