Skip to content

Commit 600f585

Browse files
authored
Merge pull request #21296 from yoff/python/bool-comparison-guards
Python: Handle guards being compared to boolean literals
2 parents 4280d35 + 89e5a9b commit 600f585

File tree

6 files changed

+94
-31
lines changed

6 files changed

+94
-31
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* When a guard such as `isSafe(x)` is defined, we now also automatically handle `isSafe(x) == true` and `isSafe(x) != false`.

python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,14 +595,42 @@ ControlFlowNode guardNode(ConditionBlock conditionBlock, boolean flipped) {
595595
result = conditionBlock.getLastNode() and
596596
flipped = false
597597
or
598-
// Recursive case: if a guard node is a `not`-expression,
598+
// Recursive cases:
599+
// if a guard node is a `not`-expression,
599600
// the operand is also a guard node, but with inverted polarity.
600601
exists(UnaryExprNode notNode |
601602
result = notNode.getOperand() and
602603
notNode.getNode().getOp() instanceof Not
603604
|
604605
notNode = guardNode(conditionBlock, flipped.booleanNot())
605606
)
607+
or
608+
// if a guard node is compared to a boolean literal,
609+
// the other operand is also a guard node,
610+
// but with polarity depending on the literal (and on the comparison).
611+
exists(CompareNode cmpNode, Cmpop op, ControlFlowNode b, boolean should_flip |
612+
(
613+
cmpNode.operands(result, op, b) or
614+
cmpNode.operands(b, op, result)
615+
) and
616+
not result.getNode() instanceof BooleanLiteral and
617+
(
618+
// comparing to the boolean
619+
(op instanceof Eq or op instanceof Is) and
620+
// we should flip if the value compared against, here the value of `b`, is false
621+
should_flip = b.getNode().(BooleanLiteral).booleanValue().booleanNot()
622+
or
623+
// comparing to the negation of the boolean
624+
(op instanceof NotEq or op instanceof IsNot) and
625+
// again, we should flip if the value compared against, here the value of `not b`, is false.
626+
// That is, if the value of `b` is true.
627+
should_flip = b.getNode().(BooleanLiteral).booleanValue()
628+
)
629+
|
630+
// we flip `flipped` according to `should_flip` via the formula `flipped xor should_flip`.
631+
flipped in [true, false] and
632+
cmpNode = guardNode(conditionBlock, flipped.booleanXor(should_flip))
633+
)
606634
}
607635

608636
/**
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/python-all
4+
extensible: barrierGuardModel
5+
data:
6+
- ['AntiSSRF', 'Member[URIValidator].Member[in_domain,in_azure_keyvault_domain,in_azure_storage_domain].Argument[0]', "true", 'request-forgery']

python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ private import semmle.python.Concepts
1010
private import semmle.python.dataflow.new.RemoteFlowSources
1111
private import semmle.python.dataflow.new.BarrierGuards
1212
private import semmle.python.ApiGraphs
13+
private import semmle.python.frameworks.data.internal.ApiGraphModels
1314

1415
/**
1516
* Provides default sources, sinks and sanitizers for detecting
@@ -177,35 +178,7 @@ module ServerSideRequestForgery {
177178
)
178179
}
179180

180-
/** A validation of a URI using the `AntiSSRF` library, considered as a full-ssrf sanitizer. */
181-
private class UriValidator extends FullUrlControlSanitizer {
182-
UriValidator() { this = DataFlow::BarrierGuard<uri_validator/3>::getABarrierNode() }
183-
}
184-
185-
import semmle.python.dataflow.new.internal.DataFlowPublic
186-
187-
private predicate uri_validator(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) {
188-
exists(DataFlow::CallCfgNode call, string funcs |
189-
funcs in ["in_domain", "in_azure_keyvault_domain", "in_azure_storage_domain"] and
190-
call = API::moduleImport("AntiSSRF").getMember("URIValidator").getMember(funcs).getACall() and
191-
call.getArg(0).asCfgNode() = node
192-
|
193-
// validator call directly (e.g., if URIValidator.in_domain(...) )
194-
g = call.asCfgNode() and
195-
branch = true
196-
or
197-
// validator used in a comparison
198-
exists(Cmpop op, Node n, ControlFlowNode l |
199-
n.getALocalSource() = call and g.(CompareNode).operands(n.asCfgNode(), op, l)
200-
|
201-
// validator == true or validator == false or validator is True or validator is False
202-
(op instanceof Eq or op instanceof Is) and
203-
branch = l.getNode().(BooleanLiteral).booleanValue()
204-
or
205-
// validator != false or validator != true or validator is not True or validator is not False
206-
(op instanceof NotEq or op instanceof IsNot) and
207-
branch = l.getNode().(BooleanLiteral).booleanValue().booleanNot()
208-
)
209-
)
181+
private class ExternalRequestForgerySanitizer extends FullUrlControlSanitizer {
182+
ExternalRequestForgerySanitizer() { ModelOutput::barrierNode(this, "request-forgery") }
210183
}
211184
}

python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,12 @@ isSanitizer
2222
| test_logical.py:176:24:176:24 | ControlFlowNode for s |
2323
| test_logical.py:185:24:185:24 | ControlFlowNode for s |
2424
| test_logical.py:193:24:193:24 | ControlFlowNode for s |
25+
| test_logical.py:199:28:199:28 | ControlFlowNode for s |
26+
| test_logical.py:206:28:206:28 | ControlFlowNode for s |
27+
| test_logical.py:211:28:211:28 | ControlFlowNode for s |
28+
| test_logical.py:214:28:214:28 | ControlFlowNode for s |
29+
| test_logical.py:219:28:219:28 | ControlFlowNode for s |
30+
| test_logical.py:226:28:226:28 | ControlFlowNode for s |
31+
| test_logical.py:231:28:231:28 | ControlFlowNode for s |
32+
| test_logical.py:234:28:234:28 | ControlFlowNode for s |
2533
| test_reference.py:31:28:31:28 | ControlFlowNode for s |

python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/test_logical.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,49 @@ def test_with_exception_neg():
192192

193193
ensure_not_tainted(s)
194194

195+
def test_comparison_with_bool():
196+
s = TAINTED_STRING
197+
198+
if is_safe(s) == True:
199+
ensure_not_tainted(s)
200+
else:
201+
ensure_tainted(s) # $ tainted
202+
203+
if is_safe(s) == False:
204+
ensure_tainted(s) # $ tainted
205+
else:
206+
ensure_not_tainted(s)
207+
208+
if is_safe(s) != True:
209+
ensure_tainted(s) # $ tainted
210+
else:
211+
ensure_not_tainted(s)
212+
213+
if is_safe(s) != False:
214+
ensure_not_tainted(s)
215+
else:
216+
ensure_tainted(s) # $ tainted
217+
218+
if is_safe(s) is True:
219+
ensure_not_tainted(s)
220+
else:
221+
ensure_tainted(s) # $ tainted
222+
223+
if is_safe(s) is False:
224+
ensure_tainted(s) # $ tainted
225+
else:
226+
ensure_not_tainted(s)
227+
228+
if is_safe(s) is not True:
229+
ensure_tainted(s) # $ tainted
230+
else:
231+
ensure_not_tainted(s)
232+
233+
if is_safe(s) is not False:
234+
ensure_not_tainted(s)
235+
else:
236+
ensure_tainted(s) # $ tainted
237+
195238
# Make tests runable
196239

197240
test_basic()
@@ -211,3 +254,4 @@ def test_with_exception_neg():
211254
test_with_exception_neg()
212255
except:
213256
pass
257+
test_comparison_with_bool()

0 commit comments

Comments
 (0)