2
$\begingroup$

I stumbled upon the MSDN Best Practices for Code Review.

Under the section “Untrusted Inputs”, I found following which I didn't understand properly. Why is the following considered safe?

printf("%s", buffer);

What kinds of guarantees does the C language give regarding the behavior of this snippet? Given that both printf(buffer) and printf("%s", buffer) are accepted by the compiler, why is one considered safer than the other — shouldn't the unsafe version be rejected?


Copied following text from the source:

Untrusted Inputs

If the input buffer comes from an untrusted source, this can result in a security attack, as the input can contain formatting specifiers that will pull data off the stack. Additionally, it can cause buffer overflows by expanding the resulting string beyond expected limits:

printf(buffer);

These should always be converted to the following:

printf("%s", buffer);

$\endgroup$
1
  • $\begingroup$ This isn't a discussion forum, it's a question and answer site. Please ask a specific question. $\endgroup$ Dec 10, 2012 at 10:25

2 Answers 2

7
$\begingroup$

Since you asked on a computer science site, I'll give you a computer science answer. This might not be the most directly helpful from a programmer's point of view, though understanding this will definitely make you a better programmer.

The first argument to the printf function is a char *, i.e. a pointer to a byte¹. However, not all pointers to bytes are valid arguments to printf. The type system of C is not precise enough to express the type of the argument.

It's well-known that the first argument to printf must be a string. A string, in C, is a zone of memory (i.e. a region of bytes with contiguous addresses) which is not empty and where the last byte has the value 0. The string is designated through a pointer to its first byte. A string is isomorphic to an array of nonzero bytes.

A null pointer, or a pointer value that does not point to a valid object, are not valid first arguments to printf. C does not offer any guarantees when a program is invalid. If you pass an invalid argument to printf (or any other function), **the behavior is undefined--. That means the behavior of the program is at the whim of the implementation. It may cause your program to crash. Or to print nonsense. Or to make demons fly out of your nose². Or to execute some code which was specially chosen by the attacker who specified the first argument — in a security context, undefined behavior is typically whatever the attacker has decided it to be (that's what exploit writing is all about).

Not all strings are valid first arguments to printf. The most obvious restriction is that the string must be a format string. In a format string, the character³ % has a special meaning. For example, "%B" and "%0s" are not valid format strings in standard C. A string which isn't a well-formed format string isn't a valid first argument to printf.

It gets worse. Not all valid format strings are valid first arguments to printf. For example, the call printf("%s") is not well-formed: the format specifier %s consumes an argument, but there is none; the behavior of this call is undefined. In practice, in many implementations, printf will grab the value at the top of the stack and assume that it's a valid string; this may crash, print out confidential information, print out data that whatever is reading the output of the program won't understand, or any combination thereof.

The type of printf is really a dependent type: the number and type of subsequent arguments depends on the value of the first argument. This is very far beyond the capabilities of C's type system⁴. Like just about everywhere in C, the onus is on the programmer to write a correct program.

Conceptually, a call like printf(buffer) is wrong, because a buffer is an arbitrary string, whereas the argument to printf must be a printf format. All printf formats are strings (the type of formats is a subtype of the type of strings), but not all strings are printf formats. The call printf(buffer) is only valid if the string buffer happens to be a printf format with zero arguments. This means no format specified other than %%, in standard C; in other words, the valid values of buffer in printf(buffer) are the strings where every % is followed by another %.

If you have a string lying around, how do you know whether it's a valid printf format? If the string is a literal written syntactically within the printf argument list, then it is very easy for the programmer or a code reviewer to check whether the format is correct. Any good compiler can check the format string and report a compilation error if it is incorrect, too. If the string comes from a variable, verifying that it is correct is a lot more difficult.

The correctness of the printf call becomes more and more difficult to ascertain as the value of the format string is specified further and further from the point of use. This is where trust comes into play. In order for the program to behave as desired, the format string must be valid for the call. When you claim that your program behaves as desired, you rely on the format string being valid — in other words, you trust the format string. This is only valid if you trust the entity where the format string originates to provide a valid format string. If the format string comes from a source that is external to your program, that source is often untrusted. Even when it is trusted, tracing that source is a heavy burden, which makes relying on such an external source bad engineering practice.

As a parting piece, why is printf("%s", buffer) valid? The %s format specifier expects a string, so this call is valid if and only if buffer is a valid string. This is as safe as it gets in C.

¹ It's in fact a little more complicated, because char isn't exactly a byte, but for our purposes here it's close enough.
² Sadly, many computers lack the hardware device that would make this very educational behavior possible.
³ By “character”, I mean the byte whose interpretation in the execution character set is the % (percent) character.
There are languages where printf is provided and has a type that describes its behavior. The obvious way to do it is in dynamically typed languages like Lisp, Perl, etc., where the behavior of printf if the arguments are not well-formed is to signal an error. OCaml typechecks printf statically; it requires that the format string be known at the compile time, as the compiler parses it and deduces the type of the remaining arguments.

$\endgroup$
1
  • $\begingroup$ Hi Gilles, Thanks a lot for explaining it in so much detail. I have to read further to understand it better and it's nice to get so much information out of this post. I never expected such a reply. Loved the reference of Nasal Demons. You rock!! $\endgroup$
    – lal
    Dec 11, 2012 at 9:37
0
$\begingroup$

buffer might contain a %n character, which could cause malicious memory writes.

See http://www.kernel.org/doc/man-pages/online/pages/man3/sprintf.3.html or man 3 printf for more details.

$\endgroup$
1
  • 1
    $\begingroup$ Thanks Josh. Found it on the link you shared: Code such as printf(foo); often indicates a bug, since foo may contain a % character. If foo comes from untrusted user input, it may contain %n, causing the printf() call to write to memory and creating a security hole. $\endgroup$
    – lal
    Dec 10, 2012 at 6:05

Not the answer you're looking for? Browse other questions tagged or ask your own question.