A format string in a TRACE logger can read the process environment
A TRACE handler that echoes a request with snprintf(response, size, request_line) does more than log attacker-controlled text. It turns the request line into a printf format string and can leak stack data in an authorized test, including pointers into argv and envp, through positional %p and %s conversions. The one-line fix is snprintf(response, size, "%s", request_line).
This is CWE-134, use of an externally controlled format string
. The bug often hides in diagnostics because developers treat the third argument as plain text. For the printf family, it is the format string: a small program that says what to read and, with %n, where to write.
A bounded copy does not make the format safe #
The length argument to snprintf bounds the output buffer. It does not stop format directives from being evaluated:
/* Vulnerable: request_line controls the format language. */
int len = snprintf(response, sizeof response, request_line);
/* Fixed: request_line is data for a constant format. */
int len = snprintf(response, sizeof response, "%s", request_line);
The same rule applies to printf(input), fprintf(log, input), and wrappers around them. When no formatting is needed, prefer fputs.
A small local regression fixture makes the difference visible without reading sensitive memory. Save this as format-log.c in a disposable directory:
#include <stdio.h>
#include <string.h>
static void render(char *out, size_t size, const char *input, int fixed) {
if (fixed)
snprintf(out, size, "%s", input);
else
snprintf(out, size, input);
}
int main(void) {
char vulnerable[128], fixed[128];
const char *probe = "TRACE /%p HTTP/1.1";
render(vulnerable, sizeof vulnerable, probe, 0);
render(fixed, sizeof fixed, probe, 1);
printf("vulnerable=%s\nfixed=%s\n", vulnerable, fixed);
return strcmp(fixed, probe) != 0 || strcmp(vulnerable, probe) == 0;
}
Run it locally:
cc -O0 -Wall -Wextra format-log.c -o format-log
./format-log
The vulnerable line expands %p; the fixed line preserves it literally. The pointer value is intentionally unspecified.
How to exercise the disclosure safely #
On an authorized disposable target, start with a normal request and a literal-percent control. Then use %p or positional %N$p to map candidate argument slots. Dereferencing a candidate with %N$s asks the process to treat that value as a string pointer, so expect child crashes and test one slot per fresh connection. Never run a broad %s sweep against production.
Environment strings commonly sit near the initial argument and environment vectors. A pointer leak can therefore become a secret leak when a service receives credentials through environment variables. Slot numbers are build- and runtime-specific; a number from one binary is not a portable exploit.
Fork-per-connection servers make repeated probing easier than expected. Children inherit the parent’s already-randomized mappings so that addresses can remain stable across child connections until the parent restarts. ASLR still applies to the parent; fork() simply copies that layout.
Treat %n as a separate, higher-risk write primitive. A read-only demonstration is enough to prove the logging flaw and should be the limit of most authorized validation.
Fix the sink and reduce the impact #
Make every format string a constant and enable compiler diagnostics such as -Wformat=2 -Wformat-security. Add a regression request containing %p, %s, and %n as literal text, then assert that the logger preserves every byte. Static analysis should flag tainted data reaching any printf-family format parameter.
Do not rely on input filtering. Blocking a few percent directives is easy to get wrong and leaves other format sinks exposed. Fix the call boundary.
Finally, keep long-lived credentials out of network-facing process environments where possible. Use narrowly scoped, short-lived credentials and isolate the component. That limits the damage from any memory disclosure, but it does not replace fixing the format string.
Search your diagnostic and TRACE paths for nonliteral printf formats before assuming they are harmless logging code.