Skip to content

Commit 6afd570

Browse files
authored
Merge pull request #658 from smowton/smowton/feature/q-format-directive-is-safe
Note that the %q format directive escapes newlines, and therefore prevents log injection
2 parents 6b4a505 + 6f598a6 commit 6afd570

File tree

10 files changed

+421
-79
lines changed

10 files changed

+421
-79
lines changed

ql/lib/semmle/go/StringOps.qll

Lines changed: 76 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,75 @@ module StringOps {
166166
}
167167
}
168168

169+
/** Provides predicates and classes for working with Printf-style formatters. */
170+
module Formatting {
171+
/**
172+
* Gets a regular expression for matching simple format-string components, including flags,
173+
* width and precision specifiers, not including explicit argument indices.
174+
*/
175+
pragma[noinline]
176+
private string getFormatComponentRegex() {
177+
exists(
178+
string literal, string opt_flag, string width, string prec, string opt_width_and_prec,
179+
string operator, string verb
180+
|
181+
literal = "([^%]|%%)+" and
182+
opt_flag = "[-+ #0]?" and
183+
width = "\\d+|\\*" and
184+
prec = "\\.(\\d+|\\*)" and
185+
opt_width_and_prec = "(" + width + ")?(" + prec + ")?" and
186+
operator = "[bcdeEfFgGoOpqstTxXUv]" and
187+
verb = "(%" + opt_flag + opt_width_and_prec + operator + ")"
188+
|
189+
result = "(" + literal + "|" + verb + ")"
190+
)
191+
}
192+
193+
/**
194+
* A function that performs string formatting in the same manner as `fmt.Printf` etc.
195+
*/
196+
abstract class Range extends Function {
197+
/**
198+
* Gets the parameter index of the format string.
199+
*/
200+
abstract int getFormatStringIndex();
201+
202+
/**
203+
* Gets the parameter index of the first parameter to be formatted.
204+
*/
205+
abstract int getFirstFormattedParameterIndex();
206+
}
207+
208+
/**
209+
* A call to a `fmt.Printf`-style string formatting function.
210+
*/
211+
class StringFormatCall extends DataFlow::CallNode {
212+
string fmt;
213+
Range f;
214+
215+
StringFormatCall() {
216+
this = f.getACall() and
217+
fmt = this.getArgument(f.getFormatStringIndex()).getStringValue() and
218+
fmt.regexpMatch(getFormatComponentRegex() + "*")
219+
}
220+
221+
/**
222+
* Gets the `n`th component of this format string.
223+
*/
224+
string getComponent(int n) { result = fmt.regexpFind(getFormatComponentRegex(), n, _) }
225+
226+
/**
227+
* Gets the `n`th argument formatted by this format call, where `formatDirective` specifies how it will be formatted.
228+
*/
229+
DataFlow::Node getOperand(int n, string formatDirective) {
230+
formatDirective = this.getComponent(n) and
231+
formatDirective.charAt(0) = "%" and
232+
formatDirective.charAt(1) != "%" and
233+
result = this.getArgument((n / 2) + f.getFirstFormattedParameterIndex())
234+
}
235+
}
236+
}
237+
169238
/**
170239
* A data-flow node that performs string concatenation.
171240
*
@@ -233,29 +302,6 @@ module StringOps {
233302
}
234303
}
235304

236-
/**
237-
* Gets a regular expression for matching simple format-string components, including flags,
238-
* width and precision specifiers, but not including `*` specifiers or explicit argument
239-
* indices.
240-
*/
241-
pragma[noinline]
242-
private string getFormatComponentRegex() {
243-
exists(
244-
string literal, string opt_flag, string width, string prec, string opt_width_and_prec,
245-
string operator, string verb
246-
|
247-
literal = "([^%]|%%)+" and
248-
opt_flag = "[-+ #0]?" and
249-
width = "\\d+|\\*" and
250-
prec = "\\.(\\d+|\\*)" and
251-
opt_width_and_prec = "(" + width + ")?(" + prec + ")?" and
252-
operator = "[bcdeEfFgGoOpqstTxXUv]" and
253-
verb = "(%" + opt_flag + opt_width_and_prec + operator + ")"
254-
|
255-
result = "(" + literal + "|" + verb + ")"
256-
)
257-
}
258-
259305
/**
260306
* A call to `fmt.Sprintf`, considered as a string concatenation.
261307
*
@@ -272,42 +318,25 @@ module StringOps {
272318
* node nor a string value. This is because verbs like `%q` perform additional string
273319
* transformations that we cannot easily represent.
274320
*/
275-
private class SprintfConcat extends Range, DataFlow::CallNode {
276-
string fmt;
277-
278-
SprintfConcat() {
279-
exists(Function sprintf | sprintf.hasQualifiedName("fmt", "Sprintf") |
280-
this = sprintf.getACall() and
281-
fmt = this.getArgument(0).getStringValue() and
282-
fmt.regexpMatch(getFormatComponentRegex() + "*")
283-
)
284-
}
285-
286-
/**
287-
* Gets the `n`th component of this format string.
288-
*/
289-
private string getComponent(int n) {
290-
result = fmt.regexpFind(getFormatComponentRegex(), n, _)
291-
}
321+
private class SprintfConcat extends Range instanceof Formatting::StringFormatCall {
322+
SprintfConcat() { this = any(Function f | f.hasQualifiedName("fmt", "Sprintf")).getACall() }
292323

293324
override DataFlow::Node getOperand(int n) {
294-
exists(int i, string part | part = "%s" or part = "%v" |
295-
part = this.getComponent(n) and
296-
i = n / 2 and
297-
result = this.getArgument(i + 1)
298-
)
325+
result = Formatting::StringFormatCall.super.getOperand(n, ["%s", "%v"])
299326
}
300327

301328
override string getOperandStringValue(int n) {
302329
result = Range.super.getOperandStringValue(n)
303330
or
304-
exists(string cmp | cmp = this.getComponent(n) |
331+
exists(string cmp | cmp = Formatting::StringFormatCall.super.getComponent(n) |
305332
(cmp.charAt(0) != "%" or cmp.charAt(1) = "%") and
306333
result = cmp.replaceAll("%%", "%")
307334
)
308335
}
309336

310-
override int getNumOperand() { result = max(int i | exists(this.getComponent(i))) + 1 }
337+
override int getNumOperand() {
338+
result = max(int i | exists(Formatting::StringFormatCall.super.getComponent(i))) + 1
339+
}
311340
}
312341

313342
/**

ql/lib/semmle/go/frameworks/ElazarlGoproxy.qll

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import go
6+
private import semmle.go.StringOps
67

78
/**
89
* Provides classes for working with concepts relating to the [github.com/elazarl/goproxy](https://pkg.go.dev/github.com/elazarl/goproxy) package.
@@ -108,8 +109,16 @@ module ElazarlGoproxy {
108109
}
109110
}
110111

112+
private class ProxyLogFunction extends StringOps::Formatting::Range, Method {
113+
ProxyLogFunction() { this.hasQualifiedName(packagePath(), "ProxyCtx", ["Logf", "Warnf"]) }
114+
115+
override int getFormatStringIndex() { result = 0 }
116+
117+
override int getFirstFormattedParameterIndex() { result = 1 }
118+
}
119+
111120
private class ProxyLog extends LoggerCall::Range, DataFlow::MethodCallNode {
112-
ProxyLog() { this.getTarget().hasQualifiedName(packagePath(), "ProxyCtx", ["Logf", "Warnf"]) }
121+
ProxyLog() { this.getTarget() instanceof ProxyLogFunction }
113122

114123
override DataFlow::Node getAMessageComponent() { result = this.getAnArgument() }
115124
}

ql/lib/semmle/go/frameworks/Glog.qll

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,53 @@
44
*/
55

66
import go
7+
private import semmle.go.StringOps
78

89
/**
910
* Provides models of commonly used functions in the `github.com/golang/glog` packages and its
1011
* forks.
1112
*/
1213
module Glog {
13-
private class GlogCall extends LoggerCall::Range, DataFlow::CallNode {
14+
private class GlogFunction extends Function {
1415
int firstPrintedArg;
1516

16-
GlogCall() {
17-
exists(string pkg, Function f, string fn, string level |
17+
GlogFunction() {
18+
exists(string pkg, string fn, string level |
1819
pkg = package(["github.com/golang/glog", "gopkg.in/glog", "k8s.io/klog"], "") and
1920
level = ["Error", "Exit", "Fatal", "Info", "Warning"] and
2021
(
2122
fn = level + ["", "f", "ln"] and firstPrintedArg = 0
2223
or
2324
fn = level + "Depth" and firstPrintedArg = 1
24-
) and
25-
this = f.getACall()
25+
)
2626
|
27-
f.hasQualifiedName(pkg, fn)
27+
this.hasQualifiedName(pkg, fn)
2828
or
29-
f.(Method).hasQualifiedName(pkg, "Verbose", fn)
29+
this.(Method).hasQualifiedName(pkg, "Verbose", fn)
3030
)
3131
}
3232

33+
/**
34+
* Gets the index of the first argument that may be output, including a format string if one is present.
35+
*/
36+
int getFirstPrintedArg() { result = firstPrintedArg }
37+
}
38+
39+
private class StringFormatter extends StringOps::Formatting::Range instanceof GlogFunction {
40+
StringFormatter() { this.getName().matches("%f") }
41+
42+
override int getFormatStringIndex() { result = super.getFirstPrintedArg() }
43+
44+
override int getFirstFormattedParameterIndex() { result = super.getFirstPrintedArg() + 1 }
45+
}
46+
47+
private class GlogCall extends LoggerCall::Range, DataFlow::CallNode {
48+
GlogFunction callee;
49+
50+
GlogCall() { this = callee.getACall() }
51+
3352
override DataFlow::Node getAMessageComponent() {
34-
result = this.getArgument(any(int i | i >= firstPrintedArg))
53+
result = this.getArgument(any(int i | i >= callee.getFirstPrintedArg()))
3554
}
3655
}
3756
}

ql/lib/semmle/go/frameworks/Logrus.qll

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** Provides models of commonly used functions in the `github.com/sirupsen/logrus` package. */
22

33
import go
4+
private import semmle.go.StringOps
45

56
/** Provides models of commonly used functions in the `github.com/sirupsen/logrus` package. */
67
module Logrus {
@@ -22,14 +23,31 @@ module Logrus {
2223
result.regexpMatch("With(Context|Error|Fields?|Time)")
2324
}
2425

25-
private class LogCall extends LoggerCall::Range, DataFlow::CallNode {
26-
LogCall() {
26+
private class LogFunction extends Function {
27+
LogFunction() {
2728
exists(string name | name = getALogResultName() or name = getAnEntryUpdatingMethodName() |
28-
this.getTarget().hasQualifiedName(packagePath(), name) or
29-
this.getTarget().(Method).hasQualifiedName(packagePath(), ["Entry", "Logger"], name)
29+
this.hasQualifiedName(packagePath(), name) or
30+
this.(Method).hasQualifiedName(packagePath(), ["Entry", "Logger"], name)
3031
)
3132
}
33+
}
34+
35+
private class LogCall extends LoggerCall::Range, DataFlow::CallNode {
36+
LogCall() { this = any(LogFunction f).getACall() }
3237

3338
override DataFlow::Node getAMessageComponent() { result = this.getAnArgument() }
3439
}
40+
41+
private class StringFormatters extends StringOps::Formatting::Range instanceof LogFunction {
42+
int argOffset;
43+
44+
StringFormatters() {
45+
this.getName().matches("%f") and
46+
if this.getName() = "Logf" then argOffset = 1 else argOffset = 0
47+
}
48+
49+
override int getFormatStringIndex() { result = argOffset }
50+
51+
override int getFirstFormattedParameterIndex() { result = argOffset + 1 }
52+
}
3553
}

ql/lib/semmle/go/frameworks/Spew.qll

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import go
6+
private import semmle.go.StringOps
67

78
/**
89
* Provides models of commonly used functions in the `github.com/davecgh/go-spew/spew` package.
@@ -11,21 +12,37 @@ module Spew {
1112
/** Gets the package path `github.com/davecgh/go-spew/spew`. */
1213
private string packagePath() { result = package("github.com/davecgh/go-spew", "spew") }
1314

14-
private class SpewCall extends LoggerCall::Range, DataFlow::CallNode {
15+
private class SpewFunction extends Function {
1516
int firstPrintedArg;
1617

17-
SpewCall() {
18+
SpewFunction() {
1819
exists(string fn |
1920
fn in ["Dump", "Errorf", "Print", "Printf", "Println"] and firstPrintedArg = 0
2021
or
2122
fn in ["Fdump", "Fprint", "Fprintf", "Fprintln"] and firstPrintedArg = 1
2223
|
23-
this.getTarget().hasQualifiedName(packagePath(), fn)
24+
this.hasQualifiedName(packagePath(), fn)
2425
)
2526
}
2627

28+
int getFirstPrintedArg() { result = firstPrintedArg }
29+
}
30+
31+
private class StringFormatter extends StringOps::Formatting::Range instanceof SpewFunction {
32+
StringFormatter() { this.getName().matches("%f") }
33+
34+
override int getFormatStringIndex() { result = super.getFirstPrintedArg() }
35+
36+
override int getFirstFormattedParameterIndex() { result = super.getFirstPrintedArg() + 1 }
37+
}
38+
39+
private class SpewCall extends LoggerCall::Range, DataFlow::CallNode {
40+
SpewFunction target;
41+
42+
SpewCall() { this = target.getACall() }
43+
2744
override DataFlow::Node getAMessageComponent() {
28-
result = this.getArgument(any(int i | i >= firstPrintedArg))
45+
result = this.getArgument(any(int i | i >= target.getFirstPrintedArg()))
2946
}
3047
}
3148

ql/lib/semmle/go/frameworks/Zap.qll

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import go
6+
private import semmle.go.StringOps
67

78
/**
89
* Provides models of commonly used functions in the `go.uber.org/zap` package.
@@ -14,24 +15,36 @@ module Zap {
1415
/** Gets a suffix for a method on `zap.SugaredLogger`. */
1516
private string getSuffix() { result in ["", "f", "w"] }
1617

18+
private class ZapFunction extends Method {
19+
ZapFunction() {
20+
exists(string fn | fn in ["DPanic", "Debug", "Error", "Fatal", "Info", "Panic", "Warn"] |
21+
this.hasQualifiedName(packagePath(), "Logger", fn)
22+
or
23+
this.hasQualifiedName(packagePath(), "SugaredLogger", fn + getSuffix())
24+
)
25+
or
26+
this.hasQualifiedName(packagePath(), "Logger", ["Named", "With", "WithOptions"])
27+
or
28+
this.hasQualifiedName(packagePath(), "SugaredLogger", ["Named", "With"])
29+
}
30+
}
31+
32+
private class ZapFormatter extends StringOps::Formatting::Range instanceof ZapFunction {
33+
ZapFormatter() { this.getName().matches("%f") }
34+
35+
override int getFormatStringIndex() { result = 0 }
36+
37+
override int getFirstFormattedParameterIndex() { result = 1 }
38+
}
39+
1740
/**
1841
* A call to a logger function in Zap.
1942
*
2043
* Functions which add data to be included the next time a direct logging
2144
* function is called are included.
2245
*/
2346
private class ZapCall extends LoggerCall::Range, DataFlow::MethodCallNode {
24-
ZapCall() {
25-
exists(string fn | fn in ["DPanic", "Debug", "Error", "Fatal", "Info", "Panic", "Warn"] |
26-
this.getTarget().hasQualifiedName(packagePath(), "Logger", fn)
27-
or
28-
this.getTarget().hasQualifiedName(packagePath(), "SugaredLogger", fn + getSuffix())
29-
)
30-
or
31-
this.getTarget().hasQualifiedName(packagePath(), "Logger", ["Named", "With", "WithOptions"])
32-
or
33-
this.getTarget().hasQualifiedName(packagePath(), "SugaredLogger", ["Named", "With"])
34-
}
47+
ZapCall() { this = any(ZapFunction f).getACall() }
3548

3649
override DataFlow::Node getAMessageComponent() { result = this.getAnArgument() }
3750
}

0 commit comments

Comments
 (0)