Skip to content

Commit 00dbada

Browse files
committed
Add java.util.Formatter related queries
1 parent de8a59e commit 00dbada

File tree

3 files changed

+144
-0
lines changed

3 files changed

+144
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Finds calls to formatting methods such as `String.format` where the format string
3+
* is created using String concatenation. This can be inefficient (for example for
4+
* loggers where depending on the log level the formatted message is not needed),
5+
* and can lead to exceptions if the format string created this way contains accidental
6+
* format placeholders.
7+
*
8+
* For example:
9+
* ```java
10+
* // Error-prone: Can fail if `arg1` contains `%`
11+
* String.format("first: " + arg1 + ", second: %s", arg2)
12+
*
13+
* // Should be rewritten to
14+
* String.format("first: %s, second: %s", arg1, arg2)
15+
* ```
16+
*
17+
* @kind problem
18+
*/
19+
20+
import java
21+
import semmle.code.java.StringFormat
22+
import semmle.code.java.dataflow.DataFlow
23+
24+
predicate isConstantString(Expr e) {
25+
e.(CompileTimeConstantExpr).getType() instanceof TypeString
26+
or
27+
// Or number expression which is for example used as 'width' in the format string,
28+
// e.g. `"%0" + width + "d"`
29+
e.getType() instanceof NumericType
30+
or
31+
// Or expression as a whole is not a constant, but its operands are
32+
isConstantString(e.(AddExpr).getLeftOperand()) and
33+
isConstantString(e.(AddExpr).getRightOperand())
34+
or
35+
isConstantString(e.(ConditionalExpr).getTrueExpr()) and
36+
isConstantString(e.(ConditionalExpr).getFalseExpr())
37+
}
38+
39+
from FormattingCall formatCall, AddExpr formatStringExpr
40+
where
41+
// Format string is created using String concat
42+
DataFlow::localExprFlow(formatStringExpr, formatCall.getFormatArgument()) and
43+
// Ignore if format string is constant and likely won't cause illegal format exceptions
44+
not isConstantString(formatStringExpr)
45+
select formatStringExpr, "Format string created using String concatenation, used by $@", formatCall,
46+
"this formatting call"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Finds calls which format a string, for example using `String.format`, but where
3+
* the format pattern string also comes from a formatted string. Such code is not only
4+
* potentially redundant, but it can also lead to exceptions if the first format call
5+
* accidentally adds `%`, which is interpreted as placeholder by the second format call.
6+
*
7+
* For example:
8+
* ```java
9+
* void checkArgument(boolean arg, String message, Object... messageArgs) {
10+
* if (!arg) {
11+
* throw new IllegalArgumentException(String.format(message, messageArgs));
12+
* }
13+
* }
14+
*
15+
* ...
16+
*
17+
* // Bug: `checkArgument` will also call `String.format`
18+
* // Fails for example if `value = "some%text"`
19+
* checkArgument(isValid, String.format("invalid value: %s", value));
20+
* ```
21+
*
22+
* @id todo
23+
* @kind problem
24+
*/
25+
26+
import java
27+
import semmle.code.java.StringFormat
28+
import semmle.code.java.dataflow.TaintTracking
29+
30+
from
31+
FormattingCall firstFormatCall, FormattingCall secondFormatCall, Expr secondFormatString,
32+
string message, FormattingCall argExpr, string argMessage
33+
where
34+
// Only consider `Formatter` formatting, don't consider mixed formatting variants,
35+
// e.g. `logger.error(String.format(...))` since logger might not support rich formatting
36+
not firstFormatCall.getSyntax().isLogger() and
37+
not secondFormatCall.getSyntax().isLogger() and
38+
secondFormatString = secondFormatCall.getFormatArgument() and
39+
TaintTracking::localExprTaint(firstFormatCall, secondFormatString) and
40+
// Ignore if first format call seems to use integer args to create the format layout for the second
41+
// call, e.g. if `firstFormatCall` is `String.format("%%%ds", count)` (used by netty/netty in a test)
42+
not forex(Expr arg | arg = firstFormatCall.getAnArgumentToBeFormatted() |
43+
arg.getType().(NumericType).hasName(["int", "Integer"])
44+
) and
45+
if secondFormatString = firstFormatCall
46+
then (
47+
message = "Formatting here is redundant because $@ performs formatting itself" and
48+
argExpr = secondFormatCall and
49+
argMessage = "the called method"
50+
) else (
51+
message = "This format string argument has already been formatted $@ before" and
52+
argExpr = firstFormatCall and
53+
argMessage = "here"
54+
)
55+
select secondFormatString, message, argExpr, argMessage
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Finds formatting calls without arguments, such as `String.format("some message")`.
3+
*
4+
* This is not only redundant and can be simplified (for `PrintStream` and `PrintWriter`
5+
* instead of `printf` `print` can be used), but can also cause an `IllegalFormatException`
6+
* if the string happens to contain a malformed `%`. If the string can be influenced
7+
* by an untrusted user, this could also be exploited for a denial-of-service attack.
8+
*
9+
* @kind problem
10+
*/
11+
12+
import java
13+
14+
predicate containsLineSeparator(Expr e) {
15+
exists(e.(CompileTimeConstantExpr).getStringValue().indexOf("%n"))
16+
or
17+
// Or String concat where one operand contains "%n" (but the whole expression is not a constant)
18+
e.getType() instanceof TypeString and containsLineSeparator(e.(AddExpr).getAnOperand())
19+
}
20+
21+
from MethodAccess call, Method m
22+
where
23+
m = call.getMethod() and
24+
(
25+
m.getDeclaringType() instanceof TypeString and
26+
m.hasName(["format", "formatted"])
27+
or
28+
m.getDeclaringType()
29+
.getASourceSupertype*()
30+
.hasQualifiedName("java.io", ["PrintStream", "PrintWriter"]) and
31+
m.hasName(["format", "printf"])
32+
// Don't consider java.io.Console methods because there seems to be no direct alternative for writing
33+
// a non-formatted string
34+
// Don't consider java.util.Formatter because if only Formatter is available, using `formatter.out()` and
35+
// writing to that is more cumbersome and possibly not as easy to understand
36+
) and
37+
// And no varargs arguments have been provided
38+
call.getNumArgument() < m.getNumberOfParameters() and
39+
// And does not use `%n` in format string to get OS-dependent line separator
40+
not containsLineSeparator(call.getAnArgument()) and
41+
// Also cover `String.formatted` where the qualifier is the format string
42+
not containsLineSeparator(call.getQualifier())
43+
select call, "Formatting call without arguments can be simplified"

0 commit comments

Comments
 (0)