|
| 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 |
0 commit comments