Skip to content
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

C#: Add cs/invalid-string-formatting to the codeql quality suite. #19148

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
171 changes: 118 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
)
this.hasName("TraceError")
or
declType.hasFullyQualifiedName("System.Diagnostics", "Trace") and
(
this.hasName("TraceError") or
this.hasName("TraceInformation") or
this.hasName("TraceWarning")
)
this.hasName("TraceInformation")
or
this.hasName("TraceInformation") and
declType.hasFullyQualifiedName("System.Diagnostics", "TraceSource")
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 Down
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") }
}
65 changes: 52 additions & 13 deletions csharp/ql/src/API Abuse/FormatInvalid.ql
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,75 @@
*/

import csharp
import semmle.code.csharp.frameworks.system.Text
import semmle.code.csharp.frameworks.Format
import FormatInvalid::PathGraph
import FormatFlow::PathGraph

abstract private class FormatStringParseCall extends MethodCall {
abstract Expr getFormatExpr();
}

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

private class ParseFormatStringCall extends FormatStringParseCall {
ParseFormatStringCall() {
this.getTarget() = any(SystemTextCompositeFormatClass x).getParseMethod()
}

override Expr getFormatExpr() { result = this.getArgument(0) }
}

module FormatInvalidConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node n) { n.asExpr() instanceof StringLiteral }

predicate isSink(DataFlow::Node n) { exists(FormatCall c | n.asExpr() = c.getFormatExpr()) }
predicate isSink(DataFlow::Node n) {
exists(FormatStringParseCall c | n.asExpr() = c.getFormatExpr())
}
}

module FormatInvalid = DataFlow::Global<FormatInvalidConfig>;

module FormatLiteralConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node n) { n.asExpr() instanceof StringLiteral }

predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
// Add flow via `System.Text.CompositeFormat.Parse`.
exists(ParseFormatStringCall call |
pred.asExpr() = call.getFormatExpr() and
succ.asExpr() = call
)
}

predicate isSink(DataFlow::Node n) { exists(FormatCall c | n.asExpr() = c.getFormatExpr()) }
}

module FormatLiteral = DataFlow::Global<FormatLiteralConfig>;

module FormatFlow =
DataFlow::MergePathGraph<FormatInvalid::PathNode, FormatLiteral::PathNode,
FormatInvalid::PathGraph, FormatLiteral::PathGraph>;

private predicate invalidFormatString(
InvalidFormatString src, FormatInvalid::PathNode source, FormatInvalid::PathNode sink, string msg,
FormatCall call, string callString
FormatStringParseCall call, string callString
) {
source.getNode().asExpr() = src and
sink.getNode().asExpr() = call.getFormatExpr() and
FormatInvalid::flowPath(source, sink) and
call.hasInsertions() and
msg = "Invalid format string used in $@ formatting call." and
callString = "this"
}

private predicate unusedArgument(
FormatCall call, FormatInvalid::PathNode source, FormatInvalid::PathNode sink, string msg,
FormatCall call, FormatLiteral::PathNode source, FormatLiteral::PathNode sink, string msg,
ValidFormatString src, string srcString, Expr unusedExpr, string unusedString
) {
exists(int unused |
source.getNode().asExpr() = src and
sink.getNode().asExpr() = call.getFormatExpr() and
FormatInvalid::flowPath(source, sink) and
FormatLiteral::flowPath(source, sink) and
unused = call.getASuppliedArgument() and
not unused = src.getAnInsert() and
not src.getValue() = "" and
Expand All @@ -53,13 +91,13 @@ private predicate unusedArgument(
}

private predicate missingArgument(
FormatCall call, FormatInvalid::PathNode source, FormatInvalid::PathNode sink, string msg,
FormatCall call, FormatLiteral::PathNode source, FormatLiteral::PathNode sink, string msg,
ValidFormatString src, string srcString
) {
exists(int used, int supplied |
source.getNode().asExpr() = src and
sink.getNode().asExpr() = call.getFormatExpr() and
FormatInvalid::flowPath(source, sink) and
FormatLiteral::flowPath(source, sink) and
used = src.getAnInsert() and
supplied = call.getSuppliedArguments() and
used >= supplied and
Expand All @@ -69,16 +107,17 @@ private predicate missingArgument(
}

from
Element alert, FormatInvalid::PathNode source, FormatInvalid::PathNode sink, string msg,
Element extra1, string extra1String, Element extra2, string extra2String
Element alert, FormatFlow::PathNode source, FormatFlow::PathNode sink, string msg, Element extra1,
string extra1String, Element extra2, string extra2String
where
invalidFormatString(alert, source, sink, msg, extra1, extra1String) and
invalidFormatString(alert, source.asPathNode1(), sink.asPathNode1(), msg, extra1, extra1String) and
extra2 = extra1 and
extra2String = extra1String
or
unusedArgument(alert, source, sink, msg, extra1, extra1String, extra2, extra2String)
unusedArgument(alert, source.asPathNode2(), sink.asPathNode2(), msg, extra1, extra1String, extra2,
extra2String)
or
missingArgument(alert, source, sink, msg, extra1, extra1String) and
missingArgument(alert, source.asPathNode2(), sink.asPathNode2(), msg, extra1, extra1String) and
extra2 = extra1 and
extra2String = extra1String
select alert, source, sink, msg, extra1, extra1String, extra2, extra2String
Loading