diff --git a/python/ql/lib/change-notes/2026-02-08-guards-compared-to-boolean-literals.md b/python/ql/lib/change-notes/2026-02-08-guards-compared-to-boolean-literals.md new file mode 100644 index 000000000000..bf626c2958c5 --- /dev/null +++ b/python/ql/lib/change-notes/2026-02-08-guards-compared-to-boolean-literals.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* When a guard such as `isSafe(x)` is defined, we now also automatically handle `isSafe(x) == true` and `isSafe(x) != false`. diff --git a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll index 4d112bcdcddd..cfffa615c8fe 100644 --- a/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll +++ b/python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll @@ -553,7 +553,8 @@ ControlFlowNode guardNode(ConditionBlock conditionBlock, boolean flipped) { result = conditionBlock.getLastNode() and flipped = false or - // Recursive case: if a guard node is a `not`-expression, + // Recursive cases: + // if a guard node is a `not`-expression, // the operand is also a guard node, but with inverted polarity. exists(UnaryExprNode notNode | result = notNode.getOperand() and @@ -561,6 +562,33 @@ ControlFlowNode guardNode(ConditionBlock conditionBlock, boolean flipped) { | notNode = guardNode(conditionBlock, flipped.booleanNot()) ) + or + // if a guard node is compared to a boolean literal, + // the other operand is also a guard node, + // but with polarity depending on the literal (and on the comparison). + exists(CompareNode cmpNode, Cmpop op, ControlFlowNode b, boolean bool | + ( + cmpNode.operands(result, op, b) or + cmpNode.operands(b, op, result) + ) and + not result.getNode() instanceof BooleanLiteral and + ( + // comparing to the boolean + (op instanceof Eq or op instanceof Is) and + // `bool` is the value being compared against, here the value of `b` + b.getNode().(BooleanLiteral).booleanValue() = bool + or + // comparing to the negation of the boolean + (op instanceof NotEq or op instanceof IsNot) and + // again, `bool` is the value being compared against, but here it is the value of `not b` + b.getNode().(BooleanLiteral).booleanValue() = bool.booleanNot() + ) + | + // if `bool` is true, we should preserve `flipped`, otherwise we should flip it + // `flipped xor (not bool)` achieves that. + flipped in [true, false] and + cmpNode = guardNode(conditionBlock, flipped.booleanXor(bool.booleanNot())) + ) } /** diff --git a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected index 86d49b2b249b..89849279d446 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected +++ b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected @@ -22,4 +22,12 @@ isSanitizer | test_logical.py:176:24:176:24 | ControlFlowNode for s | | test_logical.py:185:24:185:24 | ControlFlowNode for s | | test_logical.py:193:24:193:24 | ControlFlowNode for s | +| test_logical.py:199:28:199:28 | ControlFlowNode for s | +| test_logical.py:206:28:206:28 | ControlFlowNode for s | +| test_logical.py:211:28:211:28 | ControlFlowNode for s | +| test_logical.py:214:28:214:28 | ControlFlowNode for s | +| test_logical.py:219:28:219:28 | ControlFlowNode for s | +| test_logical.py:226:28:226:28 | ControlFlowNode for s | +| test_logical.py:231:28:231:28 | ControlFlowNode for s | +| test_logical.py:234:28:234:28 | ControlFlowNode for s | | test_reference.py:31:28:31:28 | ControlFlowNode for s | diff --git a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/test_logical.py b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/test_logical.py index 26e69b8fc050..ff215f97f41b 100644 --- a/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/test_logical.py +++ b/python/ql/test/library-tests/dataflow/tainttracking/customSanitizer/test_logical.py @@ -192,6 +192,49 @@ def test_with_exception_neg(): ensure_not_tainted(s) +def test_comparison_with_bool(): + s = TAINTED_STRING + + if is_safe(s) == True: + ensure_not_tainted(s) + else: + ensure_tainted(s) # $ tainted + + if is_safe(s) == False: + ensure_tainted(s) # $ tainted + else: + ensure_not_tainted(s) + + if is_safe(s) != True: + ensure_tainted(s) # $ tainted + else: + ensure_not_tainted(s) + + if is_safe(s) != False: + ensure_not_tainted(s) + else: + ensure_tainted(s) # $ tainted + + if is_safe(s) is True: + ensure_not_tainted(s) + else: + ensure_tainted(s) # $ tainted + + if is_safe(s) is False: + ensure_tainted(s) # $ tainted + else: + ensure_not_tainted(s) + + if is_safe(s) is not True: + ensure_tainted(s) # $ tainted + else: + ensure_not_tainted(s) + + if is_safe(s) is not False: + ensure_not_tainted(s) + else: + ensure_tainted(s) # $ tainted + # Make tests runable test_basic() @@ -211,3 +254,4 @@ def test_with_exception_neg(): test_with_exception_neg() except: pass +test_comparison_with_bool()