Skip to content

Commit fdca896

Browse files
committed
Ruby: improve handling of [g]sub!
rb/incomplete-sanitization has a few cases where we find flow from one one string substitution call to another, e.g. a.sub(...).sub(...) But this didn't find typical chained uses of the destructive variants, e.g. a.sub!(...) a.sub!(...) We now handle those cases by tracking flow from the post-update node for the receiver of the first call.
1 parent bbb8177 commit fdca896

File tree

3 files changed

+40
-24
lines changed

3 files changed

+40
-24
lines changed

ruby/ql/src/queries/security/cwe-116/IncompleteSanitization.ql

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import codeql.ruby.DataFlow
1919
import codeql.ruby.controlflow.CfgNodes
2020
import codeql.ruby.frameworks.core.String
2121
import codeql.ruby.Regexp as RE
22+
import codeql.ruby.dataflow.internal.DataFlowPrivate as DataFlowPrivate
2223

2324
/** Gets a character that is commonly used as a meta-character. */
2425
string metachar() { result = "'\"\\&<>\n\r\t*|{}[]%$".charAt(_) }
@@ -89,7 +90,8 @@ predicate allBackslashesEscaped(DataFlow::Node node) {
8990
m =
9091
[
9192
"sub", "gsub", "slice", "[]", "strip", "lstrip", "rstrip", "chomp", "chop", "downcase",
92-
"upcase",
93+
"upcase", "sub!", "gsub!", "slice!", "strip!", "lstrip!", "rstrip!", "chomp!", "chop!",
94+
"downcase!", "upcase!",
9395
]
9496
|
9597
mc = node.asExpr() and
@@ -100,6 +102,14 @@ predicate allBackslashesEscaped(DataFlow::Node node) {
100102
or
101103
// general data flow
102104
allBackslashesEscaped(node.getAPredecessor())
105+
or
106+
// general data flow from a (destructive) [g]sub!
107+
exists(DataFlowPrivate::PostUpdateNode post, StringSubstitutionCall sub |
108+
sub.isDestructive() and
109+
allBackslashesEscaped(sub) and
110+
post.getPreUpdateNode() = sub.getReceiver() and
111+
post.getASuccessor() = node
112+
)
103113
}
104114

105115
/**
@@ -110,11 +120,25 @@ predicate removesFirstOccurence(StringSubstitutionCall sub, string str) {
110120
not sub.isGlobal() and sub.replaces(str, "")
111121
}
112122

113-
/** Gets a method call where `node` is the receiver. */
114-
DataFlow::Node getAMethodCall(DataFlow::LocalSourceNode node) {
123+
/**
124+
* Gets a method call where the receiver is the result of a string subtitution
125+
* call.
126+
*/
127+
DataFlow::Node getAMethodCall(StringSubstitutionCall call) {
115128
exists(DataFlow::Node receiver |
116129
receiver.asExpr() = result.asExpr().(ExprNodes::MethodCallCfgNode).getReceiver() and
117-
node.flowsTo(receiver)
130+
(
131+
// for a non-destructive string substitution, is there flow from it to the
132+
// receiver of another method call?
133+
not call.isDestructive() and call.(DataFlow::LocalSourceNode).flowsTo(receiver)
134+
or
135+
// for a destructive string substitution, is there flow from its
136+
// post-update receivver to the receiver of another method call?
137+
call.isDestructive() and
138+
exists(DataFlowPrivate::PostUpdateNode post | post.getPreUpdateNode() = call.getReceiver() |
139+
post.(DataFlow::LocalSourceNode).flowsTo(receiver)
140+
)
141+
)
118142
)
119143
}
120144

ruby/ql/test/query-tests/security/cwe-116/IncompleteSanitization/IncompleteSanitization.expected

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@
3838
| tst.rb:82:3:82:26 | call to sub! | This replaces only the first occurrence of ... + .... |
3939
| tst.rb:87:3:87:21 | call to sub | This replaces only the first occurrence of indirect. |
4040
| tst.rb:88:3:88:22 | call to sub! | This replaces only the first occurrence of indirect. |
41-
| tst.rb:141:3:141:27 | call to gsub! | This does not escape backslash characters in the input. |
42-
| tst.rb:150:3:150:24 | call to gsub! | This does not escape backslash characters in the input. |
43-
| tst.rb:160:3:160:23 | call to gsub! | This does not escape backslash characters in the input. |
44-
| tst.rb:191:3:191:20 | call to gsub! | This does not escape backslash characters in the input. |
4541
| tst.rb:215:3:215:16 | call to sub | This replaces only the first occurrence of "<". |
4642
| tst.rb:215:3:215:29 | call to sub | This replaces only the first occurrence of ">". |
4743
| tst.rb:217:3:217:19 | call to sub | This replaces only the first occurrence of "[". |
@@ -50,13 +46,9 @@
5046
| tst.rb:218:3:218:35 | call to sub | This replaces only the first occurrence of "}". |
5147
| tst.rb:223:3:223:16 | call to sub | This replaces only the first occurrence of "]". |
5248
| tst.rb:223:3:223:29 | call to sub | This replaces only the first occurrence of "[". |
53-
| tst.rb:227:3:227:17 | call to sub! | This replaces only the first occurrence of "[". |
54-
| tst.rb:228:3:228:17 | call to sub! | This replaces only the first occurrence of "]". |
55-
| tst.rb:237:3:237:18 | call to sub! | This replaces only the first occurrence of """. |
56-
| tst.rb:238:3:238:18 | call to sub! | This replaces only the first occurrence of """. |
57-
| tst.rb:240:3:240:18 | call to sub! | This replaces only the first occurrence of "'". |
58-
| tst.rb:241:3:241:18 | call to sub! | This replaces only the first occurrence of "'". |
5949
| tst.rb:248:3:248:17 | call to sub | This replaces only the first occurrence of "\\n". |
6050
| tst.rb:249:3:249:27 | call to sub | This replaces only the first occurrence of "\\n". |
51+
| tst.rb:258:3:258:18 | call to sub! | This replaces only the first occurrence of "\\n". |
52+
| tst.rb:263:3:263:18 | call to sub! | This replaces only the first occurrence of "\\n". |
6153
| tst.rb:268:3:268:20 | call to sub! | This replaces only the first occurrence of "/../". |
6254
| tst.rb:269:3:269:20 | call to sub | This replaces only the first occurrence of "/../". |

ruby/ql/test/query-tests/security/cwe-116/IncompleteSanitization/tst.rb

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def good5a(s)
138138

139139
def good5b(s)
140140
s.gsub!(/\\/, "\\\\")
141-
s.gsub!(/['"]/, '\\\\\&') # OK, but still flagged
141+
s.gsub!(/['"]/, '\\\\\&') # OK
142142
end
143143

144144
def good6a(s)
@@ -147,7 +147,7 @@ def good6a(s)
147147

148148
def good6b(s)
149149
s.gsub!(/[\\]/, '\\\\')
150-
s.gsub!(/[\"]/, '\\"') # OK, but still flagged
150+
s.gsub!(/[\"]/, '\\"') # OK
151151
end
152152

153153
def good7a(s)
@@ -157,7 +157,7 @@ def good7a(s)
157157

158158
def good7b(s)
159159
s.gsub! /[\\]/, '\\\\'
160-
s.gsub! /[\"]/, '\\"' # OK, but still flagged
160+
s.gsub! /[\"]/, '\\"' # OK
161161
end
162162

163163
def good8a(s)
@@ -188,7 +188,7 @@ def good10b(s)
188188
s.gsub! '\\', '\\\\'
189189
s.slice! 1..(-1)
190190
s.gsub! /\\"/, '"'
191-
s.gsub! /'/, "\\'" # OK, but still flagged
191+
s.gsub! /'/, "\\'" # OK
192192
"'" + s + "'"
193193
end
194194

@@ -225,7 +225,7 @@ def good13a(s)
225225

226226
def good13b(s1)
227227
s1.sub! '[', ''
228-
s1.sub! ']', '' # OK, but still flagged
228+
s1.sub! ']', '' # OK
229229
end
230230

231231
def good14a(s)
@@ -235,10 +235,10 @@ def good14a(s)
235235

236236
def good14b(s1, s2)
237237
s1.sub!('"', '')
238-
s1.sub!('"', '') # OK, but still flagged
238+
s1.sub!('"', '') # OK
239239

240240
s2.sub!("'", "")
241-
s2.sub!("'", "") # OK, but still flagged
241+
s2.sub!("'", "") # OK
242242
end
243243

244244
def newlines_a(a, b, c)
@@ -255,12 +255,12 @@ def newlines_b(a, b, c)
255255
output.sub!("\n", "") # OK
256256

257257
d = a.dup
258-
d.sub!("\n", "")
259-
d.sub!(b, c) # NOT OK, but not flagged
258+
d.sub!("\n", "") # NOT OK
259+
d.sub!(b, c)
260260

261261
e = a.dup
262262
d.sub!(b, c)
263-
d.sub!("\n", "") # NOT OK, but not flagged
263+
d.sub!("\n", "") # NOT OK
264264
end
265265

266266
def bad_path_sanitizer(p1, p2)

0 commit comments

Comments
 (0)