Uncontrolled composite format strings in C#

External input controls a .NET composite format string

Description

.NET composite formatting interprets items in the form {index[,alignment][:formatString]}. The format selects which supplied arguments appear, how often they appear, and their alignment and individual formatting. An attacker can control this output structure if external input becomes the composite format argument to String.Format, StringBuilder.AppendFormat, TextWriter.Write or WriteLine, or Console.Write or WriteLine.

The impact differs from C printf vulnerabilities. Managed .NET composite formatting has no memory-write directive like %n and does not read arbitrary values from the call stack. Do not attribute C/C++ arbitrary memory access or direct code execution to this condition.

Potential impact

  • Changed or repeated indices can expose string representations of other arguments passed to the same call, including sensitive values supplied as format arguments.
  • Unclosed braces, out-of-range indices or invalid items can throw FormatException. Unhandled exceptions can cause failed requests or repeated service disruption.
  • Very large alignment widths can produce large output and consume memory. A custom IFormatProvider or ICustomFormatter may also expose application-specific formatting paths to the caller's choices.
  • Altered logs, responses or notifications can mislead downstream parsers or operators when the output informs security decisions or operational procedures.

Remediation

  1. Keep composite format structure in source-controlled literals and pass untrusted text only as value arguments. Developer-authored interpolated strings also separate the display structure from input values.
  2. Let users select identifiers such as compact or labelled, then map them to immutable literal formats instead of accepting raw format strings.
  3. If raw formats are unavoidable, accept only exact matches to a small fixed set of literals. Limit the types of supplied values and output size. Prefix or substring checks do not constrain the complete format.
  4. CompositeFormat.Parse validates and caches syntax, but does not remove the caller's control over argument selection and formatting within a valid format. Do not treat it as a security boundary.
  5. A helper called Sanitize, or removal of only one kind of brace, does not establish safety. Removing both opening and closing braces removes composite-format syntax but damages the text and is not a general solution.

Examples

Before

csharp
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("format")]
public sealed class FormatController : ControllerBase
{
    [HttpGet]
    public string Render(
        [FromQuery] string format,
        [FromQuery] string displayName)
    {
        string internalReference = HttpContext.TraceIdentifier;
        return string.Format(format, displayName, internalReference);
    }
}

A format of {1} can include the string representation of internalReference, which was not intended for display. Invalid braces or nonexistent argument indices throw FormatException.

After

Literal format

csharp
using System.Globalization;

return string.Format(
    CultureInfo.InvariantCulture,
    "사용자: {0}",
    displayName);

The application selects the format structure and culture provider; input is only a value. Single-string overloads such as Console.WriteLine(message) and TextWriter.Write(message) output the value without interpreting composite formatting.

Predefined format selection

csharp
using System.Globalization;

static string SelectFormat(string style) => style switch
{
    "compact" => "{0}",
    "labelled" => "사용자: {0}",
    _ => "사용자: {0}"
};

string format = SelectFormat(style);
return string.Format(CultureInfo.InvariantCulture, format, displayName);

The user selects a limited identifier, not a format string. Every branch returns a developer-authored literal.

Check that fixed formats match argument counts and types, and review custom formatters. .NET analyzer rule CA2241 and tests can support these checks.

References