|
1 |
| -* |
| 1 | +* overview |
2 | 2 | The gist https://gist.github.com/hohn/b4c32ce35b6bdc2ade04c911985c7d46
|
3 | 3 |
|
4 | 4 | * fix: Get("X-Redirect") is "X-Auth-Request-Redirect"
|
|
13 | 13 |
|
14 | 14 | 3. a taint flow configuration to connect the source and sink
|
15 | 15 |
|
| 16 | +* Getting started |
16 | 17 | from..select
|
17 | 18 |
|
18 | 19 | introduce Function
|
|
27 | 28 |
|
28 | 29 | cartesian product in the select
|
29 | 30 |
|
| 31 | +* Source |
30 | 32 | mapping from ast to Go and back
|
31 | 33 | look at this
|
32 | 34 | https://codeql.github.com/docs/codeql-language-guides/abstract-syntax-tree-classes-for-working-with-go-programs/
|
|
40 | 42 |
|
41 | 43 | pick up the type from the select and then...
|
42 | 44 |
|
| 45 | + #+BEGIN_SRC java |
43 | 46 | from CallExpr c, string funcName
|
44 | 47 | where funcName = c.getCalleeName()
|
45 | 48 | and funcName = "Get"
|
46 | 49 | select funcName
|
| 50 | + #+END_SRC |
| 51 | + |
47 | 52 |
|
48 | 53 | Narrow some
|
| 54 | + #+BEGIN_SRC java |
49 | 55 | from CallExpr c, string funcName
|
50 | 56 | where funcName = c.getCalleeName()
|
51 | 57 | and funcName = "Get"
|
52 | 58 | select funcName, c, c.getAnArgument()
|
| 59 | + #+END_SRC |
53 | 60 |
|
54 | 61 | Point out the AST parts we now get in the query; at the above url, find
|
55 | 62 | | Ident.Ident | QualifiedName | SelectorExpr |
|
|
62 | 69 |
|
63 | 70 | Let's also narrow by argument name
|
64 | 71 |
|
| 72 | + #+BEGIN_SRC java |
65 | 73 | from CallExpr c, string funcName, QualifiedName qn, Expr arg
|
66 | 74 | where funcName = c.getCalleeName()
|
67 | 75 | and funcName = "Get"
|
68 | 76 | and c.getCalleeExpr() = qn
|
69 | 77 | and arg = c.getAnArgument()
|
70 | 78 | select funcName, c, qn, arg
|
| 79 | + #+END_SRC |
71 | 80 |
|
72 | 81 | arg has type Expr, view results, see quoted strings, check ast reference for ",
|
73 | 82 | find
|
|
76 | 85 | introduce the cast operator, which now gives access to StringLit member
|
77 | 86 | predicates -- especially .getValue()
|
78 | 87 |
|
| 88 | + #+BEGIN_SRC java |
79 | 89 | from CallExpr c, string funcName, QualifiedName qn, Expr arg
|
80 | 90 | where funcName = c.getCalleeName()
|
81 | 91 | and funcName = "Get"
|
82 | 92 | and c.getCalleeExpr() = qn
|
83 | 93 | and arg = c.getAnArgument()
|
84 | 94 | select funcName, c, qn, arg, arg.(StringLit).getValue()
|
| 95 | + #+END_SRC |
85 | 96 |
|
86 | 97 | Turn into predicate
|
87 | 98 |
|
|
99 | 110 | select funcName, c, qn, arg
|
100 | 111 | #+END_SRC
|
101 | 112 |
|
102 |
| - |
103 |
| - |
104 |
| - |
105 |
| - |
106 |
| - |
107 | 113 | ======================
|
108 | 114 | Query done. Now generalize sources.
|
109 | 115 | Look under semmle/go/security for general-purpose APIs
|
|
112 | 118 |
|
113 | 119 | Hacky way: look for "Header" in the libraries.
|
114 | 120 |
|
115 |
| - |
116 | 121 | ======================
|
117 | 122 |
|
118 | 123 | For advanced session / later:
|
119 | 124 |
|
120 | 125 | UntrustedFlowSource
|
121 | 126 |
|
| 127 | + #+BEGIN_SRC java |
122 | 128 | from CallExpr c, string funcName, Expr e
|
123 | 129 | where funcName = c.getCalleeName() and
|
124 | 130 | e = c.getAnArgument()
|
125 | 131 | and e.(StringLit).getStringValue().matches("X-%")
|
126 | 132 | select funcName, c, e
|
| 133 | + #+END_SRC |
| 134 | + |
| 135 | + |
| 136 | + |
| 137 | +* SINK |
| 138 | + |
| 139 | +ask this - a data sink with the structure |
| 140 | +strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") |
| 141 | + |
| 142 | +you can show that strings.HasPrefix(redirect, "/") is CallExpr by looking at AST viewer |
| 143 | + |
| 144 | +add where clause - demo the chaining of the predicates. i.e .getTarget() returns Function but we want string "HasPrefix" so we . again and go through member predicates and see if there's anything that suits what we're looking for. |
| 145 | + |
| 146 | +We are matching just this - _.HasPrefix(_, _) |
| 147 | + |
| 148 | +#+BEGIN_SRC text |
| 149 | +from CallExpr call |
| 150 | +where |
| 151 | + call.getTarget().getName() = "HasPrefix" |
| 152 | +select call |
| 153 | +#+END_SRC |
| 154 | + |
| 155 | + |
| 156 | +Explain that strings.HasPrefix checkes that a given string argumenet begins with a |
| 157 | +certain prefix |
| 158 | + |
| 159 | +For prefix check do the chaining of the member predicates (oo principles), first |
| 160 | +put it in the select and then move it down to the where |
| 161 | + |
| 162 | + |
| 163 | +We are matching just this - _.HasPrefix(checked, _) |
| 164 | +#+BEGIN_SRC java |
| 165 | +import go |
| 166 | + |
| 167 | +from CallExpr call, Expr checked, |
| 168 | +where |
| 169 | + call.getTarget().getName() = "HasPrefix" and |
| 170 | + call.getArgument(0) = checked and |
| 171 | +select call, checked |
| 172 | + |
| 173 | +#+END_SRC |
| 174 | + |
| 175 | +- .getStringValue will always work i.e if int it gets changed to string. |
| 176 | +We dont want this. We want the prefix to be a string so we cast. It's not a cast is just restricts our set |
| 177 | + |
| 178 | +#+BEGIN_SRC java |
| 179 | +import go |
| 180 | +from CallExpr call, Expr checked, string prefix |
| 181 | +where |
| 182 | + call.getTarget().getName() = "HasPrefix" and |
| 183 | + checked = call.getArgument(0) and |
| 184 | + prefix = call.getArgument(1).(StringLit).getStringValue() |
| 185 | +select call, checked, prefix |
| 186 | + |
| 187 | +#+END_SRC |
| 188 | + |
| 189 | +- Write class for HasPrefix |
| 190 | +- Mention that a class is a type |
| 191 | +- Inheritence |
| 192 | +- Characteristic predicate |
| 193 | +- the this value - similar to O-O constructors |
| 194 | +- Replace CallExpr in from to HasPrefix |
| 195 | +- Mention that you can only refine the set not widen it |
| 196 | + |
| 197 | +#+BEGIN_SRC java |
| 198 | +import go |
| 199 | + |
| 200 | +class HasPrefix extends CallExpr { |
| 201 | + Expr checked; |
| 202 | + string prefix; |
| 203 | + |
| 204 | + HasPrefix() { |
| 205 | + this.getTarget().getName() = "HasPrefix" and |
| 206 | + checked = this.getArgument(0) and |
| 207 | + prefix = this.getArgument(1).(StringLit).getStringValue() |
| 208 | + } |
| 209 | +} |
| 210 | +#+END_SRC |
| 211 | + |
| 212 | +Characteristic predicate has to initialise field in the class. It produces a table with all the fields set |
| 213 | + |
| 214 | +#+BEGIN_SRC java |
| 215 | +//strings.HasPrefix(redirect, "/") && //!strings.HasPrefix(redirect, "//") |
| 216 | +from HasPrefix call, Expr checked, string prefix |
| 217 | +where |
| 218 | + call.getArgument(0) = checked and |
| 219 | + call.getArgument(1).getStringValue() = prefix |
| 220 | +select call, checked, prefix |
| 221 | + |
| 222 | + |
| 223 | +class HasPrefix extends CallExpr { |
| 224 | + Expr checked; |
| 225 | + string prefix; |
| 226 | + |
| 227 | + HasPrefix() { |
| 228 | + this.getTarget().getName() = "HasPrefix" and |
| 229 | + checked = this.getArgument(0) and |
| 230 | + prefix = this.getArgument(1).(StringLit).getStringValue() |
| 231 | + } |
| 232 | + |
| 233 | + Expr getBaseString() { result = checked } |
| 234 | + |
| 235 | + string getSubString() { result = prefix } |
| 236 | +} |
| 237 | +from HasPrefix call, Expr checked, string prefix |
| 238 | +where call.getBaseString() = checked and call.getSubString() = prefix |
| 239 | +select call, checked, prefix |
| 240 | +#+END_SRC |
| 241 | + |
| 242 | +- introduce result, predicates with values |
| 243 | +- only reason we gave those names to the predicate is later compatibility |
| 244 | + |
| 245 | +- Revisit what we are trying to find. We are looking for cases where the variable is checked against some prefixes but not others. This means we will have to reuse the logic of the previous query later, but with different string prefixes. |
| 246 | + |
| 247 | +- We can use predicates! |
| 248 | +- Use variable decl in from to predicate params |
| 249 | +- Use where for predicate logic |
| 250 | + |
| 251 | +: import go |
| 252 | + |
| 253 | +- We have Variables and we have read and write accesses to them. |
| 254 | +- For write, a Control Flow node |
| 255 | + |
| 256 | +#+BEGIN_SRC java |
| 257 | +from HasPrefix call, Expr checked, string prefix, Variable var |
| 258 | +where |
| 259 | + call.getBaseString() = checked and |
| 260 | + call.getSubString() = prefix and |
| 261 | + checked = var.getARead().asExpr() |
| 262 | +select call, checked, prefix, var |
| 263 | +#+END_SRC |
| 264 | + |
| 265 | + |
| 266 | +- A class is for modelling single logical items whilst predicates are good for connecting them. |
| 267 | + |
| 268 | +#+BEGIN_SRC java |
| 269 | +predicate prefixCheck(HasPrefix call, Expr checked, string prefix, Variable var) { |
| 270 | + call.getBaseString() = checked and |
| 271 | + call.getSubString() = prefix and |
| 272 | + checked = var.getARead().asExpr() |
| 273 | +} |
| 274 | + |
| 275 | +from HasPrefix call, Expr checked, string prefix, Variable var |
| 276 | +where prefixCheck(call, checked, prefix, var) |
| 277 | +select call, checked, prefix, var |
| 278 | +#+END_SRC |
| 279 | + |
| 280 | + |
| 281 | +//strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") |
| 282 | +//We are matching just this - _.HasPrefix(checked, "prefix string") |
| 283 | +from HasPrefix call, Expr checked, Variable var |
| 284 | +where prefixCheck(call, checked, "/", var) and prefixCheck(_, _, "//", var) |
| 285 | +select call, checked, var |
| 286 | +- this finds one of the incomplete checks |
| 287 | +- the correct check is The string is prefix-checked against / but not both // and /\, suggesting it will eventually be used as a redirect (a sink). |
| 288 | + |
| 289 | +we want / & // & /\\ |
| 290 | +so logically / & (not // or not /\\) |
| 291 | + |
| 292 | +#+BEGIN_SRC java |
| 293 | +predicate insufficientPrefixCheck(HasPrefix call, Expr checked, Variable var) { |
| 294 | + prefixCheck(call, checked, "/", var) and |
| 295 | + (not prefixCheck(_, _, "//", var) or not prefixCheck(_, _, "/\\", var)) |
| 296 | +} |
| 297 | + |
| 298 | +//strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") |
| 299 | +// we want / & // & /\\ |
| 300 | +// so logically / & (not // or not /\\) |
| 301 | +from HasPrefix call, Expr checked, Variable var |
| 302 | +where insufficientPrefixCheck(call, checked, var) |
| 303 | +select call, checked, var |
| 304 | + |
| 305 | +#+END_SRC |
| 306 | + |
| 307 | +GLOBAL FLOW |
| 308 | + |
| 309 | +#+BEGIN_SRC java |
| 310 | +import go |
| 311 | + |
| 312 | +class Config extends TaintTracking::Configuration { |
| 313 | + Config() { this = "Config" } |
| 314 | + |
| 315 | + override predicate isSource(DataFlow::Node source) { xAuthSource(source.asExpr(), _, _, _) } |
| 316 | + |
| 317 | + override predicate isSink(DataFlow::Node sink) { insufficientPrefixCheck(_, sink.asExpr(), _) } |
| 318 | +} |
| 319 | + |
| 320 | +class HasPrefix extends CallExpr { |
| 321 | + Expr checked; |
| 322 | + string prefix; |
| 323 | + |
| 324 | + HasPrefix() { |
| 325 | + this.getTarget().getName() = "HasPrefix" and |
| 326 | + checked = this.getArgument(0) and |
| 327 | + prefix = this.getArgument(1).(StringLit).getStringValue() |
| 328 | + } |
| 329 | + |
| 330 | + Expr getBaseString() { result = checked } |
| 331 | + |
| 332 | + string getSubString() { result = prefix } |
| 333 | +} |
| 334 | + |
| 335 | +predicate prefixCheck(HasPrefix call, Expr checked, string prefix, Variable var) { |
| 336 | + call.getBaseString() = checked and |
| 337 | + call.getSubString() = prefix and |
| 338 | + checked = var.getARead().asExpr() |
| 339 | +} |
| 340 | + |
| 341 | +predicate insufficientPrefixCheck(HasPrefix call, Expr checked, Variable var) { |
| 342 | + prefixCheck(call, checked, "/", var) and |
| 343 | + (not prefixCheck(_, _, "//", var) or not prefixCheck(_, _, "/\\", var)) |
| 344 | +} |
| 345 | + |
| 346 | +predicate xAuthSource(CallExpr c, string funcName, QualifiedName qn, Expr arg) { |
| 347 | + funcName = c.getCalleeName() and |
| 348 | + funcName = "Get" and |
| 349 | + c.getCalleeExpr() = qn and |
| 350 | + arg = c.getAnArgument() and |
| 351 | + arg.(StringLit).getValue() = "X-Auth-Request-Redirect" |
| 352 | +} |
| 353 | + |
| 354 | +//strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") |
| 355 | +// we want / & // & /\\ |
| 356 | +// so logically / & (not // or not /\\) |
| 357 | +// from HasPrefix call, Expr checked, Variable var |
| 358 | +// where insufficientPrefixCheck(call, checked, var) |
| 359 | +// select call, checked, var |
| 360 | +from DataFlow::Node source, DataFlow::Node sink, Config c |
| 361 | +where c.hasFlow(source, sink) |
| 362 | +select sink, source, sink, "Untrusted value reaches insufficient redirect check" |
| 363 | + |
| 364 | +#+END_SRC |
| 365 | + |
| 366 | +OPTIONAL |
| 367 | +- Mention that there could be other ways of searching for string prefixes in Go. |
| 368 | +- Take strings.HasPrefix(redirect, "/") and search for it in vscode |
| 369 | +- Explain how you don't want to reinvent the wheel, and that it's always good to check the qll libraries to see what's already provided out of the box |
| 370 | +- Go through the StringOps.qll and notice how the HasPrefix class extends DataFlow::Node and that the return types of the interesting predicates are also DataFlow::Node |
| 371 | +- Change your query and arrive at this |
| 372 | + |
| 373 | +#+BEGIN_SRC |
| 374 | +import go |
| 375 | + |
| 376 | +class HasPrefix extends CallExpr { |
| 377 | + HasPrefix() { this.getTarget().getName() = "HasPrefix" } |
| 378 | +} |
| 379 | + |
| 380 | +//strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") |
| 381 | +from StringOps::HasPrefix call, DataFlow::Node checked, DataFlow::Node prefix |
| 382 | +where |
| 383 | + call.getBaseString() = checked and |
| 384 | + call.getSubstring() = prefix |
| 385 | +select call, checked, prefix |
| 386 | +#+END_SRC |
| 387 | + |
| 388 | +- Notice that the first result is selection of ProxyPrefix which you're not |
| 389 | + interested in, you're interested in String values '/' or '//' |
| 390 | + |
| 391 | +- THIS IS A BIT OF A STRETCH BUT |
| 392 | +find this through exploration call.getSubstring().asExpr().getStringValue() = prefix |
| 393 | +Technically, it can be justified, because you've already shown that it was an Expr you just want the equivalent of the old query |
| 394 | + |
| 395 | +- Run query. Notice the second result. That wouldn't have been there if you didn't use StringOps::HasPrefix. Re-emphasise the need to have exploration mindset when writing queries. Try to leverage the libraries as much as possible |
| 396 | + |
| 397 | +- Notice that all the checked results correspond to a Variable. Model this. First do checked = v and then .getARead |
| 398 | + |
| 399 | +#+BEGIN_SRC |
| 400 | +import go |
| 401 | + |
| 402 | +class HasPrefix extends CallExpr { |
| 403 | + HasPrefix() { this.getTarget().getName() = "HasPrefix" } |
| 404 | +} |
| 405 | + |
| 406 | +//strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") |
| 407 | +from StringOps::HasPrefix call, DataFlow::Node checked, string prefix, Variable v |
| 408 | +where |
| 409 | + call.getBaseString() = checked and |
| 410 | + checked = v.getARead() and |
| 411 | + call.getSubstring().asExpr().getStringValue() = prefix |
| 412 | +select call, checked, prefix |
| 413 | +#+END_SRC |
| 414 | + |
| 415 | + |
0 commit comments