Skip to content

Commit 015ef4c

Browse files
committed
Add use of strings.Replacer to replace sanitizer
1 parent 2b1a789 commit 015ef4c

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

go/ql/lib/semmle/go/StringOps.qll

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,68 @@ module StringOps {
206206

207207
override string getReplacedString() { result = this.getArgument(1).getStringValue() }
208208
}
209+
210+
/**
211+
* A call to `strings.NewReplacer`.
212+
*/
213+
private class StringsNewReplacerCall extends DataFlow::CallNode {
214+
StringsNewReplacerCall() { this.getTarget().hasQualifiedName("strings", "NewReplacer") }
215+
216+
/**
217+
* Gets an argument to this call corresponding to a string that will be
218+
* replaced.
219+
*/
220+
DataFlow::Node getAReplacedArgument() {
221+
exists(int n | n % 2 = 0 and result = this.getArgument(n))
222+
}
223+
}
224+
225+
/**
226+
* A configuration for tracking flow from a call to `strings.NewReplacer` to
227+
* the receiver of a call to `strings.Replacer.Replace` or
228+
* `strings.Replacer.WriteString`.
229+
*/
230+
private class StringsNewReplacerConfiguration extends DataFlow2::Configuration {
231+
StringsNewReplacerConfiguration() { this = "StringsNewReplacerConfiguration" }
232+
233+
override predicate isSource(DataFlow::Node source) {
234+
source instanceof StringsNewReplacerCall
235+
}
236+
237+
override predicate isSink(DataFlow::Node sink) {
238+
exists(DataFlow::MethodCallNode call |
239+
sink = call.getReceiver() and
240+
call.getTarget().hasQualifiedName("strings", "Replacer", ["Replace", "WriteString"])
241+
)
242+
}
243+
}
244+
245+
/**
246+
* A call to `strings.Replacer.Replace` or `strings.Replacer.WriteString`.
247+
*/
248+
private class StringsReplacerReplaceOrWriteString extends Range {
249+
string replacedString;
250+
251+
StringsReplacerReplaceOrWriteString() {
252+
exists(
253+
StringsNewReplacerConfiguration config, StringsNewReplacerCall source,
254+
DataFlow::Node sink, DataFlow::MethodCallNode call
255+
|
256+
config.hasFlow(source, sink) and
257+
sink = call.getReceiver() and
258+
replacedString = source.getAReplacedArgument().getStringValue() and
259+
(
260+
call.getTarget().hasQualifiedName("strings", "Replacer", "Replace") and
261+
this = call.getResult()
262+
or
263+
call.getTarget().hasQualifiedName("strings", "Replacer", "WriteString") and
264+
this = call.getArgument(1)
265+
)
266+
)
267+
}
268+
269+
override string getReplacedString() { result = replacedString }
270+
}
209271
}
210272

211273
/** Provides predicates and classes for working with Printf-style formatters. */

go/ql/lib/semmle/go/security/StringBreakCustomizations.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ module StringBreak {
8787
* An expression that is equivalent to `strings.ReplaceAll(s, old, new)`,
8888
* considered as a sanitizer for unsafe quoting.
8989
*/
90-
class ReplaceSanitizer extends Sanitizer, StringOps::ReplaceAll {
90+
class ReplaceSanitizer extends StringOps::ReplaceAll, Sanitizer {
9191
Quote quote;
9292

9393
ReplaceSanitizer() { this.getReplacedString().matches("%" + quote + "%") }

go/ql/test/query-tests/Security/CWE-089/StringBreakGood.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package main
22

33
import (
4+
"bytes"
45
"encoding/json"
5-
sq "github.com/Masterminds/squirrel"
66
"strings"
7+
8+
sq "github.com/Masterminds/squirrel"
79
)
810

911
// Good because there is no concatenation with quotes:
@@ -37,3 +39,28 @@ func saveGood3(id string, version interface{}) {
3739
Values(id, sq.Expr("'"+escaped+"'")).
3840
Exec()
3941
}
42+
43+
var globalReplacer = strings.NewReplacer("\"", "", "'", "")
44+
45+
// Good because quote characters are removed before concatenation:
46+
func saveGood4(id string, version interface{}) {
47+
versionJSON, _ := json.Marshal(version)
48+
escaped := globalReplacer.Replace(string(versionJSON))
49+
sq.StatementBuilder.
50+
Insert("resources").
51+
Columns("resource_id", "version_md5").
52+
Values(id, sq.Expr("'"+escaped+"'")).
53+
Exec()
54+
}
55+
56+
// Good because quote characters are removed before concatenation:
57+
func saveGood5(id string, version interface{}) {
58+
versionJSON, _ := json.Marshal(version)
59+
buf := new(bytes.Buffer)
60+
globalReplacer.WriteString(buf, string(versionJSON))
61+
sq.StatementBuilder.
62+
Insert("resources").
63+
Columns("resource_id", "version_md5").
64+
Values(id, sq.Expr("'"+buf.String()+"'")).
65+
Exec()
66+
}

0 commit comments

Comments
 (0)