Skip to content

C#: Improve precision of cs/uncontrolled-format-string. #19271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fb45a09
C#: Add cs/invalid-string-formatting to the codeql quality suite.
michaelnebel Mar 28, 2025
629817d
C#: Convert testing of cs/invalid-string-formatting to inline expecta…
michaelnebel Apr 3, 2025
8c5ab8a
C#: Re-factor FormatMethod.
michaelnebel Apr 4, 2025
29e1c97
C#: Add more format testcases.
michaelnebel Apr 4, 2025
3f6fd7e
C#: Remove false positive example.
michaelnebel Apr 4, 2025
7095cca
C#: Remove some false positives and add more true positives for cs/in…
michaelnebel Apr 4, 2025
c025334
C#: Update test expected output.
michaelnebel Apr 4, 2025
523fff3
C#: Add more invalid-string-formatting testcases.
michaelnebel Apr 9, 2025
c481754
C#: Improve format logic to take CompositeFormat and generics into ac…
michaelnebel Apr 8, 2025
a018ab1
C#: Update test expected output.
michaelnebel Apr 9, 2025
9f961ed
C#: Add FP for string.Format using params collection.
michaelnebel Apr 9, 2025
2a21ab0
C#: Generalize array logic to params collection like types.
michaelnebel Apr 9, 2025
b17b561
C#: Update test expected output.
michaelnebel Apr 9, 2025
c365ce0
C#: Hide the abstract FormatMethod class.
michaelnebel Apr 9, 2025
ac43abc
C#: Update string format item parameter expected test case.
michaelnebel Apr 9, 2025
5c6b1dd
C#: Add change note.
michaelnebel Apr 10, 2025
cfbf94f
C#: Convert cs/uncontrolled-format-string tests to use test inline ex…
michaelnebel Apr 10, 2025
95f947f
C#: Add a testcase for CompositeFormat.Parse for cs/uncontrolled-form…
michaelnebel Apr 10, 2025
7594e64
C#: Include CompositeFormat.Parse as Format like method.
michaelnebel Apr 10, 2025
85aa0ab
C#: Update test expected output.
michaelnebel Apr 10, 2025
2118c8e
C#: Add change note.
michaelnebel Apr 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion csharp/ql/lib/semmle/code/csharp/commons/Collections.qll
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ private class ParamsConstructedCollectionTypes extends ParamsCollectionTypeImpl
unboundbase instanceof SystemCollectionsGenericIReadOnlyListTInterface or
unboundbase instanceof SystemSpanStruct or
unboundbase instanceof SystemReadOnlySpanStruct
)
) and
not this instanceof SystemStringClass
}

override Type getElementType() { result = base.getTypeArgument(0) }
Expand Down
199 changes: 146 additions & 53 deletions csharp/ql/lib/semmle/code/csharp/frameworks/Format.qll
Original file line number Diff line number Diff line change
Expand Up @@ -8,67 +8,120 @@ private import semmle.code.csharp.frameworks.System
private import semmle.code.csharp.frameworks.system.Text

/** A method that formats a string, for example `string.Format()`. */
class FormatMethod extends Method {
FormatMethod() {
exists(Class declType | declType = this.getDeclaringType() |
abstract private class FormatMethodImpl extends Method {
/**
* Gets the argument containing the format string. For example, the argument of
* `string.Format(IFormatProvider, String, Object)` is `1`.
*/
abstract int getFormatArgument();

/**
* Gets the argument number of the first supplied insert.
*/
int getFirstArgument() { result = this.getFormatArgument() + 1 }
}

final class FormatMethod = FormatMethodImpl;

/** A class of types used for formatting. */
private class FormatType extends Type {
FormatType() {
this instanceof StringType or
this instanceof SystemTextCompositeFormatClass
}
}

private class StringAndStringBuilderFormatMethods extends FormatMethodImpl {
StringAndStringBuilderFormatMethods() {
(
this.getParameter(0).getType() instanceof SystemIFormatProviderInterface and
this.getParameter(1).getType() instanceof StringType and
this.getParameter(1).getType() instanceof FormatType
or
this.getParameter(0).getType() instanceof StringType
) and
(
this = any(SystemStringClass c).getFormatMethod()
or
this = any(SystemTextStringBuilderClass c).getAppendFormatMethod()
)
}

override int getFormatArgument() {
if this.getParameter(0).getType() instanceof SystemIFormatProviderInterface
then result = 1
else result = 0
}
}

private class SystemMemoryExtensionsFormatMethods extends FormatMethodImpl {
SystemMemoryExtensionsFormatMethods() {
this = any(SystemMemoryExtensionsClass c).getTryWriteMethod() and
this.getParameter(1).getType() instanceof SystemIFormatProviderInterface and
this.getParameter(2).getType() instanceof SystemTextCompositeFormatClass
}

override int getFormatArgument() { result = 2 }

override int getFirstArgument() { result = this.getFormatArgument() + 2 }
}

private class SystemConsoleAndSystemIoTextWriterFormatMethods extends FormatMethodImpl {
SystemConsoleAndSystemIoTextWriterFormatMethods() {
this.getParameter(0).getType() instanceof StringType and
this.getNumberOfParameters() > 1 and
exists(Class declType | declType = this.getDeclaringType() |
this.hasName(["Write", "WriteLine"]) and
(
this = any(SystemStringClass c).getFormatMethod()
declType.hasFullyQualifiedName("System", "Console")
or
this = any(SystemTextStringBuilderClass c).getAppendFormatMethod()
declType.hasFullyQualifiedName("System.IO", "TextWriter")
)
or
this.getParameter(0).getType() instanceof StringType and
)
}

override int getFormatArgument() { result = 0 }
}

private class SystemDiagnosticsDebugAssert extends FormatMethodImpl {
SystemDiagnosticsDebugAssert() {
this.hasName("Assert") and
this.getDeclaringType().hasFullyQualifiedName("System.Diagnostics", "Debug") and
this.getNumberOfParameters() = 4
}

override int getFormatArgument() { result = 2 }
}

private class SystemDiagnosticsFormatMethods extends FormatMethodImpl {
SystemDiagnosticsFormatMethods() {
this.getParameter(0).getType() instanceof StringType and
this.getNumberOfParameters() > 1 and
exists(Class declType |
declType = this.getDeclaringType() and
declType.getNamespace().getFullName() = "System.Diagnostics"
|
declType.hasName("Trace") and
(
this = any(SystemStringClass c).getFormatMethod()
or
this = any(SystemTextStringBuilderClass c).getAppendFormatMethod()
or
(this.hasName("Write") or this.hasName("WriteLine")) and
(
declType.hasFullyQualifiedName("System", "Console")
or
declType.hasFullyQualifiedName("System.IO", "TextWriter")
or
declType.hasFullyQualifiedName("System.Diagnostics", "Debug") and
this.getParameter(1).getType() instanceof ArrayType
)
or
declType.hasFullyQualifiedName("System.Diagnostics", "Trace") and
(
this.hasName("TraceError") or
this.hasName("TraceInformation") or
this.hasName("TraceWarning")
)
this.hasName("TraceError")
or
this.hasName("TraceInformation") and
declType.hasFullyQualifiedName("System.Diagnostics", "TraceSource")
this.hasName("TraceInformation")
or
this.hasName("Print") and
declType.hasFullyQualifiedName("System.Diagnostics", "Debug")
this.hasName("TraceWarning")
)
or
this.hasName("Assert") and
declType.hasFullyQualifiedName("System.Diagnostics", "Debug") and
this.getNumberOfParameters() = 4
declType.hasName("TraceSource") and this.hasName("TraceInformation")
or
declType.hasName("Debug") and
(
this.hasName("Print")
or
this.hasName(["Write", "WriteLine"]) and
this.getParameter(1).getType() instanceof ArrayType
)
)
}

/**
* Gets the argument containing the format string. For example, the argument of
* `string.Format(IFormatProvider, String, Object)` is `1`.
*/
int getFormatArgument() {
if this.getParameter(0).getType() instanceof SystemIFormatProviderInterface
then result = 1
else
if
this.hasName("Assert") and
this.getDeclaringType().hasFullyQualifiedName("System.Diagnostics", "Debug")
then result = 2
else result = 0
}
override int getFormatArgument() { result = 0 }
}

pragma[nomagic]
Expand Down Expand Up @@ -194,24 +247,36 @@ class FormatCall extends MethodCall {
int getFormatArgument() { result = this.getTarget().(FormatMethod).getFormatArgument() }

/** Gets the argument number of the first supplied insert. */
int getFirstArgument() { result = this.getFormatArgument() + 1 }
int getFirstArgument() { result = this.getTarget().(FormatMethod).getFirstArgument() }

/** Holds if this call has one or more insertions. */
predicate hasInsertions() { exists(this.getArgument(this.getFirstArgument())) }

/** Holds if the arguments are supplied in an array, not individually. */
predicate hasArrayExpr() {
/**
* DEPRECATED: use `hasCollectionExpr` instead.
*
* Holds if the arguments are supplied in an array, not individually.
*/
deprecated predicate hasArrayExpr() {
this.getNumberOfArguments() = this.getFirstArgument() + 1 and
this.getArgument(this.getFirstArgument()).getType() instanceof ArrayType
}

/**
* Holds if the arguments are supplied in a collection, not individually.
*/
predicate hasCollectionExpr() {
this.getNumberOfArguments() = this.getFirstArgument() + 1 and
this.getArgument(this.getFirstArgument()).getType() instanceof ParamsCollectionType
}

/**
* Gets the number of supplied arguments (excluding the format string and format
* provider). Does not return a value if the arguments are supplied in an array,
* in which case we generally can't assess the size of the array.
*/
int getSuppliedArguments() {
not this.hasArrayExpr() and
not this.hasCollectionExpr() and
result = this.getNumberOfArguments() - this.getFirstArgument()
}

Expand All @@ -224,3 +289,31 @@ class FormatCall extends MethodCall {
result = this.getArgument(this.getFirstArgument() + index)
}
}

/**
* A method call to a method that parses a format string, for example a call
* to `string.Format()`.
*/
abstract private class FormatStringParseCallImpl extends MethodCall {
/**
* Gets the expression used as the format string.
*/
abstract Expr getFormatExpr();
}

final class FormatStringParseCall = FormatStringParseCallImpl;

private class OrdinaryFormatCall extends FormatStringParseCallImpl instanceof FormatCall {
override Expr getFormatExpr() { result = FormatCall.super.getFormatExpr() }
}

/**
* A method call to `System.Text.CompositeFormat.Parse`.
*/
class ParseFormatStringCall extends FormatStringParseCallImpl {
ParseFormatStringCall() {
this.getTarget() = any(SystemTextCompositeFormatClass x).getParseMethod()
}

override Expr getFormatExpr() { result = this.getArgument(0) }
}
14 changes: 13 additions & 1 deletion csharp/ql/lib/semmle/code/csharp/frameworks/System.qll
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ class SystemStringClass extends StringType {
/** Gets a `Format(...)` method. */
Method getFormatMethod() {
result.getDeclaringType() = this and
result.hasName("Format") and
result.getName().regexpMatch("Format(<.*>)?") and
result.getNumberOfParameters() in [2 .. 5] and
result.getReturnType() instanceof StringType
}
Expand Down Expand Up @@ -751,6 +751,18 @@ class SystemNotImplementedExceptionClass extends SystemClass {
SystemNotImplementedExceptionClass() { this.hasName("NotImplementedException") }
}

/** The `System.MemoryExtensions` class. */
class SystemMemoryExtensionsClass extends SystemClass {
SystemMemoryExtensionsClass() { this.hasName("MemoryExtensions") }

/** Gets a `TryWrite` method. */
Method getTryWriteMethod() {
result.getDeclaringType() = this and
result.getName().regexpMatch("TryWrite(<.*>)?") and
result.getParameter(0).getType().getUnboundDeclaration() instanceof SystemSpanStruct
}
}

/** The `System.DateTime` struct. */
class SystemDateTimeStruct extends SystemStruct {
SystemDateTimeStruct() { this.hasName("DateTime") }
Expand Down
15 changes: 14 additions & 1 deletion csharp/ql/lib/semmle/code/csharp/frameworks/system/Text.qll
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ class SystemTextStringBuilderClass extends SystemTextClass {
SystemTextStringBuilderClass() { this.hasName("StringBuilder") }

/** Gets the `AppendFormat` method. */
Method getAppendFormatMethod() { result = this.getAMethod("AppendFormat") }
Method getAppendFormatMethod() {
exists(string name |
name.regexpMatch("AppendFormat(<.*>)?") and
result = this.getAMethod(name)
)
}
}

/** The `System.Text.Encoding` class. */
Expand All @@ -38,3 +43,11 @@ class SystemTextEncodingClass extends SystemTextClass {
/** Gets the `GetChars` method. */
Method getGetCharsMethod() { result = this.getAMethod("GetChars") }
}

/** The `System.Text.CompositeFormat` class */
class SystemTextCompositeFormatClass extends SystemTextClass {
SystemTextCompositeFormatClass() { this.hasName("CompositeFormat") }

/** Gets the `Parse` method. */
Method getParseMethod() { result = this.getAMethod("Parse") }
}
Loading