From dc58f6fa8748fe947f6a039d5dc332e4cc48cadf Mon Sep 17 00:00:00 2001 From: dilanbhalla Date: Thu, 25 Jun 2020 11:39:09 -0700 Subject: [PATCH 0001/1429] function/class synatax --- .../Classes/NamingConventionsClasses.qhelp | 30 +++++++++++++++++++ .../src/Classes/NamingConventionsClasses.ql | 17 +++++++++++ .../NamingConventionsFunctions.qhelp | 30 +++++++++++++++++++ .../Functions/NamingConventionsFunctions.ql | 17 +++++++++++ .../Naming/NamingConventionsClasses.expected | 1 + .../Naming/NamingConventionsClasses.py | 11 +++++++ .../Naming/NamingConventionsClasses.qlref | 1 + .../NamingConventionsFunctions.expected | 1 + .../general/NamingConventionsFunctions.py | 9 ++++++ .../general/NamingConventionsFunctions.qlref | 1 + 10 files changed, 118 insertions(+) create mode 100644 python/ql/src/Classes/NamingConventionsClasses.qhelp create mode 100644 python/ql/src/Classes/NamingConventionsClasses.ql create mode 100644 python/ql/src/Functions/NamingConventionsFunctions.qhelp create mode 100644 python/ql/src/Functions/NamingConventionsFunctions.ql create mode 100644 python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.expected create mode 100644 python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.py create mode 100644 python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.qlref create mode 100644 python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.expected create mode 100644 python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.py create mode 100644 python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.qlref diff --git a/python/ql/src/Classes/NamingConventionsClasses.qhelp b/python/ql/src/Classes/NamingConventionsClasses.qhelp new file mode 100644 index 00000000000..7a6c0ca13dd --- /dev/null +++ b/python/ql/src/Classes/NamingConventionsClasses.qhelp @@ -0,0 +1,30 @@ + + + + + +

A class name that begins with a lowercase letter does not follow standard +naming conventions. This decreases code readability. For example, class background. +

+ +
+ + +

+Write the class name beginning with an uppercase letter. For example, class Background. +

+ +
+ + + +
  • + Guido van Rossum, Barry Warsaw, Nick Coghlan PEP 8 -- Style Guide for Python Code + +
  • + +
    + +
    diff --git a/python/ql/src/Classes/NamingConventionsClasses.ql b/python/ql/src/Classes/NamingConventionsClasses.ql new file mode 100644 index 00000000000..a1744f5d3e6 --- /dev/null +++ b/python/ql/src/Classes/NamingConventionsClasses.ql @@ -0,0 +1,17 @@ +/** + * @name Misnamed class + * @description A class name that begins with a lowercase letter decreases readability. + * @kind problem + * @problem.severity recommendation + * @precision medium + * @id python/misnamed-type + * @tags maintainability + */ + +import python + +from Class c +where + c.inSource() and + not c.getName().substring(0, 1).toUpperCase() = c.getName().substring(0, 1) +select c, "Class names should start in uppercase." diff --git a/python/ql/src/Functions/NamingConventionsFunctions.qhelp b/python/ql/src/Functions/NamingConventionsFunctions.qhelp new file mode 100644 index 00000000000..15014045542 --- /dev/null +++ b/python/ql/src/Functions/NamingConventionsFunctions.qhelp @@ -0,0 +1,30 @@ + + + + + +

    A function name that begins with an uppercase letter does not follow standard +naming conventions. This decreases code readability. For example, Jump. +

    + +
    + + +

    +Write the function name beginning with an lowercase letter. For example, jump. +

    + +
    + + + +
  • + Guido van Rossum, Barry Warsaw, Nick Coghlan PEP 8 -- Style Guide for Python Code + +
  • + + + + diff --git a/python/ql/src/Functions/NamingConventionsFunctions.ql b/python/ql/src/Functions/NamingConventionsFunctions.ql new file mode 100644 index 00000000000..3ed619bc084 --- /dev/null +++ b/python/ql/src/Functions/NamingConventionsFunctions.ql @@ -0,0 +1,17 @@ +/** + * @name Misnamed function + * @description A function name that begins with an uppercase letter decreases readability. + * @kind problem + * @problem.severity recommendation + * @precision medium + * @id python/misnamed-function + * @tags maintainability + */ + +import python + +from Function f +where + f.inSource() and + not f.getName().substring(0, 1).toLowerCase() = f.getName().substring(0, 1) +select f, "Function names should start in lowercase." diff --git a/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.expected b/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.expected new file mode 100644 index 00000000000..8e6dea7fce4 --- /dev/null +++ b/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.expected @@ -0,0 +1 @@ +| NamingConventionsClasses.py:2:1:2:14 | Class badName | Class names should start in uppercase. | diff --git a/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.py b/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.py new file mode 100644 index 00000000000..c07bdb57234 --- /dev/null +++ b/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.py @@ -0,0 +1,11 @@ +# BAD, do not start class or interface name with lowercase letter +class badName: + + def hello(self): + print("hello") + +# Good, class name starts with capital letter +class GoodName: + + def hello(self): + print("hello") \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.qlref b/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.qlref new file mode 100644 index 00000000000..01ad29859da --- /dev/null +++ b/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.qlref @@ -0,0 +1 @@ +Classes/NamingConventionsClasses.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.expected b/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.expected new file mode 100644 index 00000000000..d87fe6c16f3 --- /dev/null +++ b/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.expected @@ -0,0 +1 @@ +| NamingConventionsFunctions.py:4:5:4:25 | Function HelloWorld | Function names should start in lowercase. | diff --git a/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.py b/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.py new file mode 100644 index 00000000000..fb3e89ab8e9 --- /dev/null +++ b/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.py @@ -0,0 +1,9 @@ +class Test: + + # BAD, do not start function name with uppercase letter + def HelloWorld(self): + print("hello world") + + # GOOD, function name starts with lowercase letter + def hello_world(self): + print("hello world") \ No newline at end of file diff --git a/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.qlref b/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.qlref new file mode 100644 index 00000000000..da4780ecbe9 --- /dev/null +++ b/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.qlref @@ -0,0 +1 @@ +Functions/NamingConventionsFunctions.ql \ No newline at end of file From dc73fcc4e856cbfc3ea5967013dcf858679563f8 Mon Sep 17 00:00:00 2001 From: dilanbhalla Date: Wed, 1 Jul 2020 09:54:58 -0700 Subject: [PATCH 0002/1429] moved to experimental --- .../{ => experimental}/Classes/NamingConventionsClasses.qhelp | 0 .../src/{ => experimental}/Classes/NamingConventionsClasses.ql | 0 .../Functions/NamingConventionsFunctions.qhelp | 0 .../{ => experimental}/Functions/NamingConventionsFunctions.ql | 0 .../query-tests/Classes/Naming/NamingConventionsClasses.expected | 0 .../query-tests/Classes/Naming/NamingConventionsClasses.py | 0 .../query-tests/Classes/Naming/NamingConventionsClasses.qlref | 1 + .../Functions/general/NamingConventionsFunctions.expected | 0 .../query-tests/Functions/general/NamingConventionsFunctions.py | 0 .../Functions/general/NamingConventionsFunctions.qlref | 1 + .../query-tests/Classes/Naming/NamingConventionsClasses.qlref | 1 - .../Functions/general/NamingConventionsFunctions.qlref | 1 - 12 files changed, 2 insertions(+), 2 deletions(-) rename python/ql/src/{ => experimental}/Classes/NamingConventionsClasses.qhelp (100%) rename python/ql/src/{ => experimental}/Classes/NamingConventionsClasses.ql (100%) rename python/ql/src/{ => experimental}/Functions/NamingConventionsFunctions.qhelp (100%) rename python/ql/src/{ => experimental}/Functions/NamingConventionsFunctions.ql (100%) rename python/ql/test/{ => experimental}/query-tests/Classes/Naming/NamingConventionsClasses.expected (100%) rename python/ql/test/{ => experimental}/query-tests/Classes/Naming/NamingConventionsClasses.py (100%) create mode 100644 python/ql/test/experimental/query-tests/Classes/Naming/NamingConventionsClasses.qlref rename python/ql/test/{ => experimental}/query-tests/Functions/general/NamingConventionsFunctions.expected (100%) rename python/ql/test/{ => experimental}/query-tests/Functions/general/NamingConventionsFunctions.py (100%) create mode 100644 python/ql/test/experimental/query-tests/Functions/general/NamingConventionsFunctions.qlref delete mode 100644 python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.qlref delete mode 100644 python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.qlref diff --git a/python/ql/src/Classes/NamingConventionsClasses.qhelp b/python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp similarity index 100% rename from python/ql/src/Classes/NamingConventionsClasses.qhelp rename to python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp diff --git a/python/ql/src/Classes/NamingConventionsClasses.ql b/python/ql/src/experimental/Classes/NamingConventionsClasses.ql similarity index 100% rename from python/ql/src/Classes/NamingConventionsClasses.ql rename to python/ql/src/experimental/Classes/NamingConventionsClasses.ql diff --git a/python/ql/src/Functions/NamingConventionsFunctions.qhelp b/python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp similarity index 100% rename from python/ql/src/Functions/NamingConventionsFunctions.qhelp rename to python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp diff --git a/python/ql/src/Functions/NamingConventionsFunctions.ql b/python/ql/src/experimental/Functions/NamingConventionsFunctions.ql similarity index 100% rename from python/ql/src/Functions/NamingConventionsFunctions.ql rename to python/ql/src/experimental/Functions/NamingConventionsFunctions.ql diff --git a/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.expected b/python/ql/test/experimental/query-tests/Classes/Naming/NamingConventionsClasses.expected similarity index 100% rename from python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.expected rename to python/ql/test/experimental/query-tests/Classes/Naming/NamingConventionsClasses.expected diff --git a/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.py b/python/ql/test/experimental/query-tests/Classes/Naming/NamingConventionsClasses.py similarity index 100% rename from python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.py rename to python/ql/test/experimental/query-tests/Classes/Naming/NamingConventionsClasses.py diff --git a/python/ql/test/experimental/query-tests/Classes/Naming/NamingConventionsClasses.qlref b/python/ql/test/experimental/query-tests/Classes/Naming/NamingConventionsClasses.qlref new file mode 100644 index 00000000000..7ed945d782c --- /dev/null +++ b/python/ql/test/experimental/query-tests/Classes/Naming/NamingConventionsClasses.qlref @@ -0,0 +1 @@ +experimental/Classes/NamingConventionsClasses.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.expected b/python/ql/test/experimental/query-tests/Functions/general/NamingConventionsFunctions.expected similarity index 100% rename from python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.expected rename to python/ql/test/experimental/query-tests/Functions/general/NamingConventionsFunctions.expected diff --git a/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.py b/python/ql/test/experimental/query-tests/Functions/general/NamingConventionsFunctions.py similarity index 100% rename from python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.py rename to python/ql/test/experimental/query-tests/Functions/general/NamingConventionsFunctions.py diff --git a/python/ql/test/experimental/query-tests/Functions/general/NamingConventionsFunctions.qlref b/python/ql/test/experimental/query-tests/Functions/general/NamingConventionsFunctions.qlref new file mode 100644 index 00000000000..0204694de0a --- /dev/null +++ b/python/ql/test/experimental/query-tests/Functions/general/NamingConventionsFunctions.qlref @@ -0,0 +1 @@ +experimental/Functions/NamingConventionsFunctions.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.qlref b/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.qlref deleted file mode 100644 index 01ad29859da..00000000000 --- a/python/ql/test/query-tests/Classes/Naming/NamingConventionsClasses.qlref +++ /dev/null @@ -1 +0,0 @@ -Classes/NamingConventionsClasses.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.qlref b/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.qlref deleted file mode 100644 index da4780ecbe9..00000000000 --- a/python/ql/test/query-tests/Functions/general/NamingConventionsFunctions.qlref +++ /dev/null @@ -1 +0,0 @@ -Functions/NamingConventionsFunctions.ql \ No newline at end of file From 26b030f8ccaf1b1922623a035e15cef239ec7314 Mon Sep 17 00:00:00 2001 From: dilanbhalla Date: Tue, 7 Jul 2020 10:52:26 -0700 Subject: [PATCH 0003/1429] fixed pr suggestions --- .../Classes/NamingConventionsClasses.qhelp | 2 +- .../Classes/NamingConventionsClasses.ql | 14 ++++++++++---- .../Functions/NamingConventionsFunctions.qhelp | 2 +- .../Functions/NamingConventionsFunctions.ql | 14 ++++++++++---- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp b/python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp index 7a6c0ca13dd..1a7f4bc45a4 100644 --- a/python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp +++ b/python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp @@ -22,7 +22,7 @@ Write the class name beginning with an uppercase letter. For example, clas
  • Guido van Rossum, Barry Warsaw, Nick Coghlan PEP 8 -- Style Guide for Python Code - + Python Class Names
  • diff --git a/python/ql/src/experimental/Classes/NamingConventionsClasses.ql b/python/ql/src/experimental/Classes/NamingConventionsClasses.ql index a1744f5d3e6..d4919c3ece8 100644 --- a/python/ql/src/experimental/Classes/NamingConventionsClasses.ql +++ b/python/ql/src/experimental/Classes/NamingConventionsClasses.ql @@ -3,15 +3,21 @@ * @description A class name that begins with a lowercase letter decreases readability. * @kind problem * @problem.severity recommendation - * @precision medium - * @id python/misnamed-type + * @id py/misnamed-class * @tags maintainability */ import python -from Class c +from Class c, string first_char where c.inSource() and - not c.getName().substring(0, 1).toUpperCase() = c.getName().substring(0, 1) + first_char = c.getName().prefix(1) and + not first_char = first_char.toUpperCase() and + not exists(Class c1, string first_char1 | + c1 != c and + c1.getLocation().getFile() = c.getLocation().getFile() and + first_char1 = c1.getName().prefix(1) and + not first_char1 = first_char1.toUpperCase() + ) select c, "Class names should start in uppercase." diff --git a/python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp b/python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp index 15014045542..46d948592ff 100644 --- a/python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp +++ b/python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp @@ -22,7 +22,7 @@ Write the function name beginning with an lowercase letter. For example, j
  • Guido van Rossum, Barry Warsaw, Nick Coghlan PEP 8 -- Style Guide for Python Code - + Python Function and Variable Names
  • diff --git a/python/ql/src/experimental/Functions/NamingConventionsFunctions.ql b/python/ql/src/experimental/Functions/NamingConventionsFunctions.ql index 3ed619bc084..80dc0e99cd8 100644 --- a/python/ql/src/experimental/Functions/NamingConventionsFunctions.ql +++ b/python/ql/src/experimental/Functions/NamingConventionsFunctions.ql @@ -3,15 +3,21 @@ * @description A function name that begins with an uppercase letter decreases readability. * @kind problem * @problem.severity recommendation - * @precision medium - * @id python/misnamed-function + * @id py/misnamed-function * @tags maintainability */ import python -from Function f +from Function f, string first_char where f.inSource() and - not f.getName().substring(0, 1).toLowerCase() = f.getName().substring(0, 1) + first_char = f.getName().prefix(1) and + not first_char = first_char.toLowerCase() and + not exists(Function f1, string first_char1 | + f1 != f and + f1.getLocation().getFile() = f.getLocation().getFile() and + first_char1 = f1.getName().prefix(1) and + not first_char1 = first_char1.toLowerCase() + ) select f, "Function names should start in lowercase." From 8119fd2ad1e2d009991f9a12969ccd55054163c4 Mon Sep 17 00:00:00 2001 From: haby0 Date: Thu, 18 Feb 2021 18:11:10 +0800 Subject: [PATCH 0004/1429] *)add JsonHijacking ql query --- .../Security/CWE/CWE-352/JsonHijacking.java | 119 ++++++++++++++++++ .../Security/CWE/CWE-352/JsonHijacking.qhelp | 35 ++++++ .../src/Security/CWE/CWE-352/JsonHijacking.ql | 32 +++++ .../Security/CWE/CWE-352/JsonHijackingLib.qll | 92 ++++++++++++++ .../Security/CWE/CWE-352/JsonStringLib.qll | 42 +++++++ .../security/CWE-352/JsonHijacking.expected | 48 +++++++ .../security/CWE-352/JsonHijacking.java | 119 ++++++++++++++++++ .../security/CWE-352/JsonHijacking.qlref | 1 + .../query-tests/security/CWE-352/options | 1 + .../com/alibaba/fastjson/JSON.java | 4 + .../stereotype/Controller.java | 14 +++ .../core/annotation/AliasFor.class | Bin 0 -> 385 bytes .../core/annotation/AliasFor.java | 10 ++ .../web/bind/annotation/GetMapping.class | Bin 0 -> 504 bytes .../web/bind/annotation/GetMapping.java | 19 +++ .../web/bind/annotation/RequestMapping.java | 13 ++ .../web/bind/annotation/ResponseBody.class | Bin 0 -> 184 bytes .../web/bind/annotation/ResponseBody.java | 4 + 18 files changed, 553 insertions(+) create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonHijacking.java create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonHijacking.qhelp create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonHijacking.ql create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonHijackingLib.qll create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonStringLib.qll create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.qlref create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/options create mode 100644 java/ql/test/stubs/spring-context-5.3.2/org/springframework/stereotype/Controller.java create mode 100644 java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.class create mode 100644 java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.java create mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.class create mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.java create mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestMapping.java create mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.class create mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.java diff --git a/java/ql/src/Security/CWE/CWE-352/JsonHijacking.java b/java/ql/src/Security/CWE/CWE-352/JsonHijacking.java new file mode 100644 index 00000000000..d08d436fa07 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonHijacking.java @@ -0,0 +1,119 @@ +import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Random; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class JsonHijacking { + + private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + + @GetMapping(value = "jsonp1") + @ResponseBody + public String bad1(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp2") + @ResponseBody + public String bad2(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; + + return resultStr; + } + + @GetMapping(value = "jsonp3") + @ResponseBody + public String bad3(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp4") + @ResponseBody + public String bad4(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @GetMapping(value = "jsonp5") + @ResponseBody + public void bad5(HttpServletRequest request, + HttpServletResponse response) throws Exception { + response.setContentType("application/json"); + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp6") + @ResponseBody + public void bad6(HttpServletRequest request, + HttpServletResponse response) throws Exception { + response.setContentType("application/json"); + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + ObjectMapper mapper = new ObjectMapper(); + String result = mapper.writeValueAsString(hashMap); + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp7") + @ResponseBody + public String good(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + String val = ""; + Random random = new Random(); + for (int i = 0; i < 10; i++) { + val += String.valueOf(random.nextInt(10)); + } + // good + jsonpCallback = jsonpCallback + "_" + val; + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + public static String getJsonStr(Object result) { + return JSONObject.toJSONString(result); + } +} \ No newline at end of file diff --git a/java/ql/src/Security/CWE/CWE-352/JsonHijacking.qhelp b/java/ql/src/Security/CWE/CWE-352/JsonHijacking.qhelp new file mode 100644 index 00000000000..38e1845f992 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonHijacking.qhelp @@ -0,0 +1,35 @@ + + + +

    The software uses external input as the function name to wrap JSON data and return it to the client as a request response. When there is a cross-domain problem, +there is a problem of sensitive information leakage.

    + +
    + + +

    The function name verification processing for external input can effectively prevent the leakage of sensitive information.

    + +
    + + +

    The following example shows the case of no verification processing and verification processing for the external input function name.

    + + + +
    + + +
  • +OWASPLondon20161124_JSON_Hijacking_Gareth_Heyes: +JSON hijacking. +
  • +
  • +Practical JSONP Injection: + + Completely controllable from the URL (GET variable) +. +
  • +
    +
    diff --git a/java/ql/src/Security/CWE/CWE-352/JsonHijacking.ql b/java/ql/src/Security/CWE/CWE-352/JsonHijacking.ql new file mode 100644 index 00000000000..a6a6d2475f0 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonHijacking.ql @@ -0,0 +1,32 @@ +/** + * @name JSON Hijacking + * @description User-controlled callback function names that are not verified are vulnerable + * to json hijacking attacks. + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/Json-hijacking + * @tags security + * external/cwe/cwe-352 + */ + +import java +import JsonHijackingLib +import semmle.code.java.dataflow.FlowSources +import DataFlow::PathGraph + +/** Taint-tracking configuration tracing flow from remote sources to output jsonp data. */ +class JsonHijackingConfig extends TaintTracking::Configuration { + JsonHijackingConfig() { this = "JsonHijackingConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof JsonHijackingSink } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, JsonHijackingConfig conf +where + conf.hasFlowPath(source, sink) and + exists(JsonHijackingFlowConfig jhfc | jhfc.hasFlowTo(sink.getNode())) +select sink.getNode(), source, sink, "Json Hijacking query might include code from $@.", + source.getNode(), "this user input" diff --git a/java/ql/src/Security/CWE/CWE-352/JsonHijackingLib.qll b/java/ql/src/Security/CWE/CWE-352/JsonHijackingLib.qll new file mode 100644 index 00000000000..ba91a6670bf --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonHijackingLib.qll @@ -0,0 +1,92 @@ +import java +import DataFlow +import JsonStringLib +import semmle.code.java.dataflow.DataFlow +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.frameworks.spring.SpringController + +/** A data flow sink for unvalidated user input that is used to jsonp. */ +abstract class JsonHijackingSink extends DataFlow::Node { } + +/** Use ```print```, ```println```, ```write``` to output result. */ +private class WriterPrintln extends JsonHijackingSink { + WriterPrintln() { + exists(MethodAccess ma | + ma.getMethod().getName().regexpMatch("print|println|write") and + ma.getMethod() + .getDeclaringType() + .getASourceSupertype*() + .hasQualifiedName("java.io", "PrintWriter") and + ma.getArgument(0) = this.asExpr() + ) + } +} + +/** Spring Request Method return result. */ +private class SpringReturn extends JsonHijackingSink { + SpringReturn() { + exists(ReturnStmt rs, Method m | m = rs.getEnclosingCallable() | + m instanceof SpringRequestMappingMethod and + rs.getResult() = this.asExpr() + ) + } +} + +/** A concatenate expression using `(` and `)` or `);`. */ +class JsonHijackingExpr extends AddExpr { + JsonHijackingExpr() { + getRightOperand().toString().regexpMatch("\"\\)\"|\"\\);\"") and + getLeftOperand() + .(AddExpr) + .getLeftOperand() + .(AddExpr) + .getRightOperand() + .toString() + .regexpMatch("\"\\(\"") + } + + /** Get the jsonp function name of this expression */ + Expr getFunctionName() { + result = getLeftOperand().(AddExpr).getLeftOperand().(AddExpr).getLeftOperand() + } + + /** Get the json data of this expression */ + Expr getJsonExpr() { result = getLeftOperand().(AddExpr).getRightOperand() } +} + +/** A data flow configuration tracing flow from remote sources to jsonp function name. */ +class RemoteFlowConfig extends DataFlow2::Configuration { + RemoteFlowConfig() { this = "RemoteFlowConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + exists(JsonHijackingExpr jhe | jhe.getFunctionName() = sink.asExpr()) + } +} + +/** A data flow configuration tracing flow from json data to splicing jsonp data. */ +class JsonDataFlowConfig extends DataFlow2::Configuration { + JsonDataFlowConfig() { this = "JsonDataFlowConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof JsonpStringSource } + + override predicate isSink(DataFlow::Node sink) { + exists(JsonHijackingExpr jhe | jhe.getJsonExpr() = sink.asExpr()) + } +} + +/** Taint-tracking configuration tracing flow from user-controllable function name jsonp data to output jsonp data. */ +class JsonHijackingFlowConfig extends TaintTracking::Configuration { + JsonHijackingFlowConfig() { this = "JsonHijackingFlowConfig" } + + override predicate isSource(DataFlow::Node src) { + exists(JsonHijackingExpr jhe, JsonDataFlowConfig jdfc, RemoteFlowConfig rfc | + jhe = src.asExpr() and + jdfc.hasFlowTo(DataFlow::exprNode(jhe.getJsonExpr())) and + rfc.hasFlowTo(DataFlow::exprNode(jhe.getFunctionName())) + ) + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof JsonHijackingSink } +} diff --git a/java/ql/src/Security/CWE/CWE-352/JsonStringLib.qll b/java/ql/src/Security/CWE/CWE-352/JsonStringLib.qll new file mode 100644 index 00000000000..0da8bc860d1 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonStringLib.qll @@ -0,0 +1,42 @@ +import java +import semmle.code.java.dataflow.DataFlow +import semmle.code.java.dataflow.FlowSources +import DataFlow::PathGraph + +/** Json string type data */ +abstract class JsonpStringSource extends DataFlow::Node { } + +/** Convert to String using Gson library. */ +private class GsonString extends JsonpStringSource { + GsonString() { + exists(MethodAccess ma, Method m | ma.getMethod() = m | + m.hasName("toJson") and + m.getDeclaringType().getASupertype*().hasQualifiedName("com.google.gson", "Gson") and + this.asExpr() = ma + ) + } +} + +/** Convert to String using Fastjson library. */ +private class FastjsonString extends JsonpStringSource { + FastjsonString() { + exists(MethodAccess ma, Method m | ma.getMethod() = m | + m.hasName("toJSONString") and + m.getDeclaringType().getASupertype*().hasQualifiedName("com.alibaba.fastjson", "JSON") and + this.asExpr() = ma + ) + } +} + +/** Convert to String using Jackson library. */ +private class JacksonString extends JsonpStringSource { + JacksonString() { + exists(MethodAccess ma, Method m | ma.getMethod() = m | + m.hasName("writeValueAsString") and + m.getDeclaringType() + .getASupertype*() + .hasQualifiedName("com.fasterxml.jackson.databind", "ObjectMapper") and + this.asExpr() = ma + ) + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.expected new file mode 100644 index 00000000000..8efc3be1673 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.expected @@ -0,0 +1,48 @@ +edges +| JsonHijacking.java:28:32:28:68 | getParameter(...) : String | JsonHijacking.java:33:16:33:24 | resultStr | +| JsonHijacking.java:32:21:32:54 | ... + ... : String | JsonHijacking.java:33:16:33:24 | resultStr | +| JsonHijacking.java:40:32:40:68 | getParameter(...) : String | JsonHijacking.java:44:16:44:24 | resultStr | +| JsonHijacking.java:42:21:42:80 | ... + ... : String | JsonHijacking.java:44:16:44:24 | resultStr | +| JsonHijacking.java:51:32:51:68 | getParameter(...) : String | JsonHijacking.java:54:16:54:24 | resultStr | +| JsonHijacking.java:53:21:53:55 | ... + ... : String | JsonHijacking.java:54:16:54:24 | resultStr | +| JsonHijacking.java:61:32:61:68 | getParameter(...) : String | JsonHijacking.java:64:16:64:24 | resultStr | +| JsonHijacking.java:63:21:63:54 | ... + ... : String | JsonHijacking.java:64:16:64:24 | resultStr | +| JsonHijacking.java:72:32:72:68 | getParameter(...) : String | JsonHijacking.java:80:20:80:28 | resultStr | +| JsonHijacking.java:79:21:79:54 | ... + ... : String | JsonHijacking.java:80:20:80:28 | resultStr | +| JsonHijacking.java:88:32:88:68 | getParameter(...) : String | JsonHijacking.java:95:20:95:28 | resultStr | +| JsonHijacking.java:94:21:94:54 | ... + ... : String | JsonHijacking.java:95:20:95:28 | resultStr | +| JsonHijacking.java:102:32:102:68 | getParameter(...) : String | JsonHijacking.java:113:16:113:24 | resultStr | +nodes +| JsonHijacking.java:28:32:28:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonHijacking.java:32:21:32:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonHijacking.java:33:16:33:24 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:33:16:33:24 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:40:32:40:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonHijacking.java:42:21:42:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonHijacking.java:44:16:44:24 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:44:16:44:24 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:51:32:51:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonHijacking.java:53:21:53:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonHijacking.java:54:16:54:24 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:54:16:54:24 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:61:32:61:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonHijacking.java:63:21:63:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonHijacking.java:64:16:64:24 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:64:16:64:24 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:72:32:72:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonHijacking.java:79:21:79:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonHijacking.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:88:32:88:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonHijacking.java:94:21:94:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonHijacking.java:95:20:95:28 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:95:20:95:28 | resultStr | semmle.label | resultStr | +| JsonHijacking.java:102:32:102:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonHijacking.java:113:16:113:24 | resultStr | semmle.label | resultStr | +#select +| JsonHijacking.java:33:16:33:24 | resultStr | JsonHijacking.java:28:32:28:68 | getParameter(...) : String | JsonHijacking.java:33:16:33:24 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:28:32:28:68 | getParameter(...) | this user input | +| JsonHijacking.java:44:16:44:24 | resultStr | JsonHijacking.java:40:32:40:68 | getParameter(...) : String | JsonHijacking.java:44:16:44:24 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:40:32:40:68 | getParameter(...) | this user input | +| JsonHijacking.java:54:16:54:24 | resultStr | JsonHijacking.java:51:32:51:68 | getParameter(...) : String | JsonHijacking.java:54:16:54:24 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:51:32:51:68 | getParameter(...) | this user input | +| JsonHijacking.java:64:16:64:24 | resultStr | JsonHijacking.java:61:32:61:68 | getParameter(...) : String | JsonHijacking.java:64:16:64:24 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:61:32:61:68 | getParameter(...) | this user input | +| JsonHijacking.java:80:20:80:28 | resultStr | JsonHijacking.java:72:32:72:68 | getParameter(...) : String | JsonHijacking.java:80:20:80:28 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:72:32:72:68 | getParameter(...) | this user input | +| JsonHijacking.java:95:20:95:28 | resultStr | JsonHijacking.java:88:32:88:68 | getParameter(...) : String | JsonHijacking.java:95:20:95:28 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:88:32:88:68 | getParameter(...) | this user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.java new file mode 100644 index 00000000000..9b473e0610c --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.java @@ -0,0 +1,119 @@ +import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Random; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class JsonHijacking { + + private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + + @GetMapping(value = "jsonp1") + @ResponseBody + public String bad1(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp2") + @ResponseBody + public String bad2(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; + + return resultStr; + } + + @GetMapping(value = "jsonp3") + @ResponseBody + public String bad3(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp4") + @ResponseBody + public String bad4(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @GetMapping(value = "jsonp5") + @ResponseBody + public void bad5(HttpServletRequest request, + HttpServletResponse response) throws Exception { + response.setContentType("application/json"); + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp6") + @ResponseBody + public void bad6(HttpServletRequest request, + HttpServletResponse response) throws Exception { + response.setContentType("application/json"); + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + ObjectMapper mapper = new ObjectMapper(); + String result = mapper.writeValueAsString(hashMap); + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp7") + @ResponseBody + public String good(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + String val = ""; + Random random = new Random(); + for (int i = 0; i < 10; i++) { + val += String.valueOf(random.nextInt(10)); + } + // good + jsonpCallback = jsonpCallback + "_" + val; + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + public static String getJsonStr(Object result) { + return JSONObject.toJSONString(result); + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.qlref b/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.qlref new file mode 100644 index 00000000000..e79471b3c1e --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.qlref @@ -0,0 +1 @@ +Security/CWE/CWE-352/JsonHijacking.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/options b/java/ql/test/experimental/query-tests/security/CWE-352/options new file mode 100644 index 00000000000..3676b8e38b6 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/options @@ -0,0 +1 @@ +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/apache-http-4.4.13/:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/fastjson-1.2.74/:${testdir}/../../../../stubs/gson-2.8.6/:${testdir}/../../../../stubs/jackson-databind-2.10/:${testdir}/../../../../stubs/springframework-5.2.3/:${testdir}/../../../../stubs/spring-context-5.3.2/:${testdir}/../../../../stubs/spring-web-5.3.2/:${testdir}/../../../../stubs/spring-core-5.3.2/ diff --git a/java/ql/test/stubs/fastjson-1.2.74/com/alibaba/fastjson/JSON.java b/java/ql/test/stubs/fastjson-1.2.74/com/alibaba/fastjson/JSON.java index b71e890e9b7..99e2873d375 100644 --- a/java/ql/test/stubs/fastjson-1.2.74/com/alibaba/fastjson/JSON.java +++ b/java/ql/test/stubs/fastjson-1.2.74/com/alibaba/fastjson/JSON.java @@ -26,6 +26,10 @@ import com.alibaba.fastjson.parser.*; import com.alibaba.fastjson.parser.deserializer.ParseProcess; public abstract class JSON { + public static String toJSONString(Object object) { + return null; + } + public static Object parse(String text) { return null; } diff --git a/java/ql/test/stubs/spring-context-5.3.2/org/springframework/stereotype/Controller.java b/java/ql/test/stubs/spring-context-5.3.2/org/springframework/stereotype/Controller.java new file mode 100644 index 00000000000..9b1751fa2ae --- /dev/null +++ b/java/ql/test/stubs/spring-context-5.3.2/org/springframework/stereotype/Controller.java @@ -0,0 +1,14 @@ +package org.springframework.stereotype; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Controller { + String value() default ""; +} diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.class b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.class new file mode 100644 index 0000000000000000000000000000000000000000..438065489278b92503abc2ad8d75716c5e56ac08 GIT binary patch literal 385 zcmb7=!Ab)$5QhJ0x31kDdrH&Kx0=)qG#3PM4!PcXZrOKO@(l3m};gAd?CiL-}x zJqaEL=Fj~6^G&|KKRyB6W0qr@<21(^Vbrp1G~wdrcD3b}m1S3}bqdDS4}|lDb3So0 z-aYCKH#QMKxO{0`GCTd`S`$rab#IG=`O1e{#kVeF6L_cJeRx%s4_fgdPA#nAxb#7` zj5*1|vPl9`tbG$Iy);(DbZ?q>Y=pc21QTZcMbG6{R|0?4KmBGoU|o}(H;@|2R}C^k ghLPwaQNxHF$I?t>JeJBL3UL&FIx;a%x-6Xh0OC_*bpQYW literal 0 HcmV?d00001 diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.java b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.java new file mode 100644 index 00000000000..3a823fade5b --- /dev/null +++ b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.java @@ -0,0 +1,10 @@ +package org.springframework.core.annotation; + +public @interface AliasFor { + @AliasFor("attribute") + String value() default ""; + + @AliasFor("value") + String attribute() default ""; + +} diff --git a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.class b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.class new file mode 100644 index 0000000000000000000000000000000000000000..5392ca0ebc1593d6bfa2f7d765ee4ca169fb260f GIT binary patch literal 504 zcma)&y-ve06orr5G%4k$Ewlp@8)_FUu~3N#3Bgi?)JiO!oYW02i5(KVeK!UkfQLfd z3?&dTFj%_xlg>TI=i~G39l#Za0geNl1Q;-QTBMR;Fd9$SVk3AWbj;^AS316C=-+5< ztgy=HTe%W0u?%2nZA9WoH5`o>f62T|*k=Ym6S+tWhIV9h;Zj+SS#FjtD#y;;xIB_~ zDxp)|dubm;mXYs88HC|<=CoC*d{Tu96Imr8>11m1m={?Yb44Ckk!ycGS!yuAF9#FEVXJbh%nj0^%G u-TFC+dFlH8Nm;4MC5#O62q7eGj&Kvy7#SEDn1GlW=t>44%>pEu7+3+XjWw+R literal 0 HcmV?d00001 diff --git a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.java b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.java new file mode 100644 index 00000000000..b2134009968 --- /dev/null +++ b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.java @@ -0,0 +1,4 @@ +package org.springframework.web.bind.annotation; + +public @interface ResponseBody { +} From 872a000a33e1b0d6fb2efeb926c7ed9e18725bd5 Mon Sep 17 00:00:00 2001 From: haby0 Date: Wed, 24 Feb 2021 20:36:12 +0800 Subject: [PATCH 0005/1429] *)update to JSONP injection --- .../Security/CWE/CWE-352/JsonpInjection.java | 170 +++++++++++++++++ .../Security/CWE/CWE-352/JsonpInjection.qhelp | 35 ++++ .../Security/CWE/CWE-352/JsonpInjection.ql | 55 ++++++ .../CWE/CWE-352/JsonpInjectionLib.qll | 92 ++++++++++ .../security/CWE-352/JsonpInjection.expected | 60 ++++++ .../security/CWE-352/JsonpInjection.java | 171 ++++++++++++++++++ .../security/CWE-352/JsonpInjection.qlref | 1 + 7 files changed, 584 insertions(+) create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjection.java create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjection.qhelp create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.qlref diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.java b/java/ql/src/Security/CWE/CWE-352/JsonpInjection.java new file mode 100644 index 00000000000..8b4e7cc005e --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjection.java @@ -0,0 +1,170 @@ +import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Random; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class JsonpInjection { +private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + + @GetMapping(value = "jsonp1") + @ResponseBody + public String bad1(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp2") + @ResponseBody + public String bad2(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; + + return resultStr; + } + + @GetMapping(value = "jsonp3") + @ResponseBody + public String bad3(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp4") + @ResponseBody + public String bad4(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @GetMapping(value = "jsonp5") + @ResponseBody + public void bad5(HttpServletRequest request, + HttpServletResponse response) throws Exception { + response.setContentType("application/json"); + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp6") + @ResponseBody + public void bad6(HttpServletRequest request, + HttpServletResponse response) throws Exception { + response.setContentType("application/json"); + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + ObjectMapper mapper = new ObjectMapper(); + String result = mapper.writeValueAsString(hashMap); + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp7") + @ResponseBody + public String good(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + String val = ""; + Random random = new Random(); + for (int i = 0; i < 10; i++) { + val += String.valueOf(random.nextInt(10)); + } + // good + jsonpCallback = jsonpCallback + "_" + val; + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp8") + @ResponseBody + public String good1(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + String token = request.getParameter("token"); + + // good + if (verifToken(token)){ + System.out.println(token); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + return "error"; + } + + @GetMapping(value = "jsonp9") + @ResponseBody + public String good2(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + String referer = request.getHeader("Referer"); + + boolean result = verifReferer(referer); + // good + if (result){ + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + return "error"; + } + + public static String getJsonStr(Object result) { + return JSONObject.toJSONString(result); + } + + public static boolean verifToken(String token){ + if (token != "xxxx"){ + return false; + } + return true; + } + + public static boolean verifReferer(String referer){ + if (!referer.startsWith("http://test.com/")){ + return false; + } + return true; + } +} \ No newline at end of file diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.qhelp b/java/ql/src/Security/CWE/CWE-352/JsonpInjection.qhelp new file mode 100644 index 00000000000..b063b409d3a --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjection.qhelp @@ -0,0 +1,35 @@ + + + +

    The software uses external input as the function name to wrap JSON data and return it to the client as a request response. When there is a cross-domain problem, +there is a problem of sensitive information leakage.

    + +
    + + +

    Adding `Referer` or random `token` verification processing can effectively prevent the leakage of sensitive information.

    + +
    + + +

    The following example shows the case of no verification processing and verification processing for the external input function name.

    + + + +
    + + +
  • +OWASPLondon20161124_JSON_Hijacking_Gareth_Heyes: +JSON hijacking. +
  • +
  • +Practical JSONP Injection: + + Completely controllable from the URL (GET variable) +. +
  • +
    +
    diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql b/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql new file mode 100644 index 00000000000..e7ca2d41e34 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql @@ -0,0 +1,55 @@ +/** + * @name JSON Hijacking + * @description User-controlled callback function names that are not verified are vulnerable + * to json hijacking attacks. + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/Json-hijacking + * @tags security + * external/cwe/cwe-352 + */ + +import java +import JsonpInjectionLib +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.deadcode.WebEntryPoints +import DataFlow::PathGraph + +class VerifAuth extends DataFlow::BarrierGuard { + VerifAuth() { + exists(MethodAccess ma, Node prod, Node succ | + this = ma and + ma.getMethod().getName().regexpMatch("(?i).*(token|auth|referer).*") and + prod instanceof RemoteFlowSource and + succ.asExpr() = ma.getAnArgument() and + ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer).*") and + localFlowStep*(prod, succ) + ) + } + + override predicate checks(Expr e, boolean branch) { + exists(ReturnStmt rs | + e = rs.getResult() and + branch = true + ) + } +} + +/** Taint-tracking configuration tracing flow from remote sources to output jsonp data. */ +class JsonpInjectionConfig extends TaintTracking::Configuration { + JsonpInjectionConfig() { this = "JsonpInjectionConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof JsonpInjectionSink } + + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { guard instanceof VerifAuth } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, JsonpInjectionConfig conf +where + conf.hasFlowPath(source, sink) and + exists(JsonpInjectionFlowConfig jhfc | jhfc.hasFlowTo(sink.getNode())) +select sink.getNode(), source, sink, "Json Hijacking query might include code from $@.", + source.getNode(), "this user input" \ No newline at end of file diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll new file mode 100644 index 00000000000..4294a5e8c8f --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll @@ -0,0 +1,92 @@ +import java +import DataFlow +import JsonStringLib +import semmle.code.java.dataflow.DataFlow +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.frameworks.spring.SpringController + +/** A data flow sink for unvalidated user input that is used to jsonp. */ +abstract class JsonpInjectionSink extends DataFlow::Node { } + +/** Use ```print```, ```println```, ```write``` to output result. */ +private class WriterPrintln extends JsonpInjectionSink { + WriterPrintln() { + exists(MethodAccess ma | + ma.getMethod().getName().regexpMatch("print|println|write") and + ma.getMethod() + .getDeclaringType() + .getASourceSupertype*() + .hasQualifiedName("java.io", "PrintWriter") and + ma.getArgument(0) = this.asExpr() + ) + } +} + +/** Spring Request Method return result. */ +private class SpringReturn extends JsonpInjectionSink { + SpringReturn() { + exists(ReturnStmt rs, Method m | m = rs.getEnclosingCallable() | + m instanceof SpringRequestMappingMethod and + rs.getResult() = this.asExpr() + ) + } +} + +/** A concatenate expression using `(` and `)` or `);`. */ +class JsonpInjectionExpr extends AddExpr { + JsonpInjectionExpr() { + getRightOperand().toString().regexpMatch("\"\\)\"|\"\\);\"") and + getLeftOperand() + .(AddExpr) + .getLeftOperand() + .(AddExpr) + .getRightOperand() + .toString() + .regexpMatch("\"\\(\"") + } + + /** Get the jsonp function name of this expression */ + Expr getFunctionName() { + result = getLeftOperand().(AddExpr).getLeftOperand().(AddExpr).getLeftOperand() + } + + /** Get the json data of this expression */ + Expr getJsonExpr() { result = getLeftOperand().(AddExpr).getRightOperand() } +} + +/** A data flow configuration tracing flow from remote sources to jsonp function name. */ +class RemoteFlowConfig extends DataFlow2::Configuration { + RemoteFlowConfig() { this = "RemoteFlowConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + exists(JsonpInjectionExpr jhe | jhe.getFunctionName() = sink.asExpr()) + } +} + +/** A data flow configuration tracing flow from json data to splicing jsonp data. */ +class JsonDataFlowConfig extends DataFlow2::Configuration { + JsonDataFlowConfig() { this = "JsonDataFlowConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof JsonpStringSource } + + override predicate isSink(DataFlow::Node sink) { + exists(JsonpInjectionExpr jhe | jhe.getJsonExpr() = sink.asExpr()) + } +} + +/** Taint-tracking configuration tracing flow from user-controllable function name jsonp data to output jsonp data. */ +class JsonpInjectionFlowConfig extends DataFlow::Configuration { + JsonpInjectionFlowConfig() { this = "JsonpInjectionFlowConfig" } + + override predicate isSource(DataFlow::Node src) { + exists(JsonpInjectionExpr jhe, JsonDataFlowConfig jdfc, RemoteFlowConfig rfc | + jhe = src.asExpr() and + jdfc.hasFlowTo(DataFlow::exprNode(jhe.getJsonExpr())) and + rfc.hasFlowTo(DataFlow::exprNode(jhe.getFunctionName())) + ) + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof JsonpInjectionSink } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected new file mode 100644 index 00000000000..019af8f5c05 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected @@ -0,0 +1,60 @@ +edges +| JsonpInjection.java:28:32:28:68 | getParameter(...) : String | JsonpInjection.java:33:16:33:24 | resultStr | +| JsonpInjection.java:32:21:32:54 | ... + ... : String | JsonpInjection.java:33:16:33:24 | resultStr | +| JsonpInjection.java:40:32:40:68 | getParameter(...) : String | JsonpInjection.java:44:16:44:24 | resultStr | +| JsonpInjection.java:42:21:42:80 | ... + ... : String | JsonpInjection.java:44:16:44:24 | resultStr | +| JsonpInjection.java:51:32:51:68 | getParameter(...) : String | JsonpInjection.java:54:16:54:24 | resultStr | +| JsonpInjection.java:53:21:53:55 | ... + ... : String | JsonpInjection.java:54:16:54:24 | resultStr | +| JsonpInjection.java:61:32:61:68 | getParameter(...) : String | JsonpInjection.java:64:16:64:24 | resultStr | +| JsonpInjection.java:63:21:63:54 | ... + ... : String | JsonpInjection.java:64:16:64:24 | resultStr | +| JsonpInjection.java:72:32:72:68 | getParameter(...) : String | JsonpInjection.java:80:20:80:28 | resultStr | +| JsonpInjection.java:79:21:79:54 | ... + ... : String | JsonpInjection.java:80:20:80:28 | resultStr | +| JsonpInjection.java:88:32:88:68 | getParameter(...) : String | JsonpInjection.java:95:20:95:28 | resultStr | +| JsonpInjection.java:94:21:94:54 | ... + ... : String | JsonpInjection.java:95:20:95:28 | resultStr | +| JsonpInjection.java:102:32:102:68 | getParameter(...) : String | JsonpInjection.java:113:16:113:24 | resultStr | +| JsonpInjection.java:128:25:128:59 | ... + ... : String | JsonpInjection.java:129:20:129:28 | resultStr | +| JsonpInjection.java:147:25:147:59 | ... + ... : String | JsonpInjection.java:148:20:148:28 | resultStr | +nodes +| JsonpInjection.java:28:32:28:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjection.java:32:21:32:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:33:16:33:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:33:16:33:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:40:32:40:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjection.java:42:21:42:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:44:16:44:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:44:16:44:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:51:32:51:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjection.java:53:21:53:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:54:16:54:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:54:16:54:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:61:32:61:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjection.java:63:21:63:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:64:16:64:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:64:16:64:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:72:32:72:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjection.java:79:21:79:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:88:32:88:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjection.java:94:21:94:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:95:20:95:28 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:95:20:95:28 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:102:32:102:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjection.java:113:16:113:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:128:25:128:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:129:20:129:28 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:147:25:147:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:148:20:148:28 | resultStr | semmle.label | resultStr | +#select +| JsonpInjection.java:33:16:33:24 | resultStr | JsonpInjection.java:28:32:28:68 | getParameter(...) : String | JsonpInjection.java:33:16:33:24 | resultStr | Json Hijacking query +might include code from $@. | JsonpInjection.java:28:32:28:68 | getParameter(...) | this user input | +| JsonpInjection.java:44:16:44:24 | resultStr | JsonpInjection.java:40:32:40:68 | getParameter(...) : String | JsonpInjection.java:44:16:44:24 | resultStr | Json Hijacking query +might include code from $@. | JsonpInjection.java:40:32:40:68 | getParameter(...) | this user input | +| JsonpInjection.java:54:16:54:24 | resultStr | JsonpInjection.java:51:32:51:68 | getParameter(...) : String | JsonpInjection.java:54:16:54:24 | resultStr | Json Hijacking query +might include code from $@. | JsonpInjection.java:51:32:51:68 | getParameter(...) | this user input | +| JsonpInjection.java:64:16:64:24 | resultStr | JsonpInjection.java:61:32:61:68 | getParameter(...) : String | JsonpInjection.java:64:16:64:24 | resultStr | Json Hijacking query +might include code from $@. | JsonpInjection.java:61:32:61:68 | getParameter(...) | this user input | +| JsonpInjection.java:80:20:80:28 | resultStr | JsonpInjection.java:72:32:72:68 | getParameter(...) : String | JsonpInjection.java:80:20:80:28 | resultStr | Json Hijacking query +might include code from $@. | JsonpInjection.java:72:32:72:68 | getParameter(...) | this user input | +| JsonpInjection.java:95:20:95:28 | resultStr | JsonpInjection.java:88:32:88:68 | getParameter(...) : String | JsonpInjection.java:95:20:95:28 | resultStr | Json Hijacking query +might include code from $@. | JsonpInjection.java:88:32:88:68 | getParameter(...) | this user input | \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java new file mode 100644 index 00000000000..df3aa2c02fe --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java @@ -0,0 +1,171 @@ +import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Random; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class JsonpInjection { + + private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + + @GetMapping(value = "jsonp1") + @ResponseBody + public String bad1(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp2") + @ResponseBody + public String bad2(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; + + return resultStr; + } + + @GetMapping(value = "jsonp3") + @ResponseBody + public String bad3(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp4") + @ResponseBody + public String bad4(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @GetMapping(value = "jsonp5") + @ResponseBody + public void bad5(HttpServletRequest request, + HttpServletResponse response) throws Exception { + response.setContentType("application/json"); + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp6") + @ResponseBody + public void bad6(HttpServletRequest request, + HttpServletResponse response) throws Exception { + response.setContentType("application/json"); + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + ObjectMapper mapper = new ObjectMapper(); + String result = mapper.writeValueAsString(hashMap); + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp7") + @ResponseBody + public String good(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + String val = ""; + Random random = new Random(); + for (int i = 0; i < 10; i++) { + val += String.valueOf(random.nextInt(10)); + } + // good + jsonpCallback = jsonpCallback + "_" + val; + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp8") + @ResponseBody + public String good1(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + String token = request.getParameter("token"); + + // good + if (verifToken(token)){ + System.out.println(token); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + return "error"; + } + + @GetMapping(value = "jsonp9") + @ResponseBody + public String good2(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + String referer = request.getHeader("Referer"); + + boolean result = verifReferer(referer); + // good + if (result){ + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + return "error"; + } + + public static String getJsonStr(Object result) { + return JSONObject.toJSONString(result); + } + + public static boolean verifToken(String token){ + if (token != "xxxx"){ + return false; + } + return true; + } + + public static boolean verifReferer(String referer){ + if (!referer.startsWith("http://test.com/")){ + return false; + } + return true; + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.qlref new file mode 100644 index 00000000000..6ad4b8acda7 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.qlref @@ -0,0 +1 @@ +Security/CWE/CWE-352/JsonpInjection.ql From 6fe8bafc7d35f77d5db38d0802b192b6abb45dd1 Mon Sep 17 00:00:00 2001 From: haby0 Date: Wed, 24 Feb 2021 20:59:51 +0800 Subject: [PATCH 0006/1429] *)update --- .../Security/CWE/CWE-352/JsonHijacking.java | 119 ------------------ .../Security/CWE/CWE-352/JsonHijacking.qhelp | 35 ------ .../src/Security/CWE/CWE-352/JsonHijacking.ql | 32 ----- .../Security/CWE/CWE-352/JsonHijackingLib.qll | 92 -------------- .../security/CWE-352/JsonHijacking.expected | 48 ------- .../security/CWE-352/JsonHijacking.java | 119 ------------------ .../security/CWE-352/JsonHijacking.qlref | 1 - 7 files changed, 446 deletions(-) delete mode 100644 java/ql/src/Security/CWE/CWE-352/JsonHijacking.java delete mode 100644 java/ql/src/Security/CWE/CWE-352/JsonHijacking.qhelp delete mode 100644 java/ql/src/Security/CWE/CWE-352/JsonHijacking.ql delete mode 100644 java/ql/src/Security/CWE/CWE-352/JsonHijackingLib.qll delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.expected delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.java delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.qlref diff --git a/java/ql/src/Security/CWE/CWE-352/JsonHijacking.java b/java/ql/src/Security/CWE/CWE-352/JsonHijacking.java deleted file mode 100644 index d08d436fa07..00000000000 --- a/java/ql/src/Security/CWE/CWE-352/JsonHijacking.java +++ /dev/null @@ -1,119 +0,0 @@ -import com.alibaba.fastjson.JSONObject; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.gson.Gson; -import java.io.PrintWriter; -import java.util.HashMap; -import java.util.Random; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -@Controller -public class JsonHijacking { - - private static HashMap hashMap = new HashMap(); - - static { - hashMap.put("username","admin"); - hashMap.put("password","123456"); - } - - - @GetMapping(value = "jsonp1") - @ResponseBody - public String bad1(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - - Gson gson = new Gson(); - String result = gson.toJson(hashMap); - resultStr = jsonpCallback + "(" + result + ")"; - return resultStr; - } - - @GetMapping(value = "jsonp2") - @ResponseBody - public String bad2(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - - resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; - - return resultStr; - } - - @GetMapping(value = "jsonp3") - @ResponseBody - public String bad3(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - String jsonStr = getJsonStr(hashMap); - resultStr = jsonpCallback + "(" + jsonStr + ")"; - return resultStr; - } - - @GetMapping(value = "jsonp4") - @ResponseBody - public String bad4(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - String restr = JSONObject.toJSONString(hashMap); - resultStr = jsonpCallback + "(" + restr + ");"; - return resultStr; - } - - @GetMapping(value = "jsonp5") - @ResponseBody - public void bad5(HttpServletRequest request, - HttpServletResponse response) throws Exception { - response.setContentType("application/json"); - String jsonpCallback = request.getParameter("jsonpCallback"); - PrintWriter pw = null; - Gson gson = new Gson(); - String result = gson.toJson(hashMap); - - String resultStr = null; - pw = response.getWriter(); - resultStr = jsonpCallback + "(" + result + ")"; - pw.println(resultStr); - } - - @GetMapping(value = "jsonp6") - @ResponseBody - public void bad6(HttpServletRequest request, - HttpServletResponse response) throws Exception { - response.setContentType("application/json"); - String jsonpCallback = request.getParameter("jsonpCallback"); - PrintWriter pw = null; - ObjectMapper mapper = new ObjectMapper(); - String result = mapper.writeValueAsString(hashMap); - String resultStr = null; - pw = response.getWriter(); - resultStr = jsonpCallback + "(" + result + ")"; - pw.println(resultStr); - } - - @GetMapping(value = "jsonp7") - @ResponseBody - public String good(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - - String val = ""; - Random random = new Random(); - for (int i = 0; i < 10; i++) { - val += String.valueOf(random.nextInt(10)); - } - // good - jsonpCallback = jsonpCallback + "_" + val; - String jsonStr = getJsonStr(hashMap); - resultStr = jsonpCallback + "(" + jsonStr + ")"; - return resultStr; - } - - public static String getJsonStr(Object result) { - return JSONObject.toJSONString(result); - } -} \ No newline at end of file diff --git a/java/ql/src/Security/CWE/CWE-352/JsonHijacking.qhelp b/java/ql/src/Security/CWE/CWE-352/JsonHijacking.qhelp deleted file mode 100644 index 38e1845f992..00000000000 --- a/java/ql/src/Security/CWE/CWE-352/JsonHijacking.qhelp +++ /dev/null @@ -1,35 +0,0 @@ - - - -

    The software uses external input as the function name to wrap JSON data and return it to the client as a request response. When there is a cross-domain problem, -there is a problem of sensitive information leakage.

    - -
    - - -

    The function name verification processing for external input can effectively prevent the leakage of sensitive information.

    - -
    - - -

    The following example shows the case of no verification processing and verification processing for the external input function name.

    - - - -
    - - -
  • -OWASPLondon20161124_JSON_Hijacking_Gareth_Heyes: -JSON hijacking. -
  • -
  • -Practical JSONP Injection: - - Completely controllable from the URL (GET variable) -. -
  • -
    -
    diff --git a/java/ql/src/Security/CWE/CWE-352/JsonHijacking.ql b/java/ql/src/Security/CWE/CWE-352/JsonHijacking.ql deleted file mode 100644 index a6a6d2475f0..00000000000 --- a/java/ql/src/Security/CWE/CWE-352/JsonHijacking.ql +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @name JSON Hijacking - * @description User-controlled callback function names that are not verified are vulnerable - * to json hijacking attacks. - * @kind path-problem - * @problem.severity error - * @precision high - * @id java/Json-hijacking - * @tags security - * external/cwe/cwe-352 - */ - -import java -import JsonHijackingLib -import semmle.code.java.dataflow.FlowSources -import DataFlow::PathGraph - -/** Taint-tracking configuration tracing flow from remote sources to output jsonp data. */ -class JsonHijackingConfig extends TaintTracking::Configuration { - JsonHijackingConfig() { this = "JsonHijackingConfig" } - - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - - override predicate isSink(DataFlow::Node sink) { sink instanceof JsonHijackingSink } -} - -from DataFlow::PathNode source, DataFlow::PathNode sink, JsonHijackingConfig conf -where - conf.hasFlowPath(source, sink) and - exists(JsonHijackingFlowConfig jhfc | jhfc.hasFlowTo(sink.getNode())) -select sink.getNode(), source, sink, "Json Hijacking query might include code from $@.", - source.getNode(), "this user input" diff --git a/java/ql/src/Security/CWE/CWE-352/JsonHijackingLib.qll b/java/ql/src/Security/CWE/CWE-352/JsonHijackingLib.qll deleted file mode 100644 index ba91a6670bf..00000000000 --- a/java/ql/src/Security/CWE/CWE-352/JsonHijackingLib.qll +++ /dev/null @@ -1,92 +0,0 @@ -import java -import DataFlow -import JsonStringLib -import semmle.code.java.dataflow.DataFlow -import semmle.code.java.dataflow.FlowSources -import semmle.code.java.frameworks.spring.SpringController - -/** A data flow sink for unvalidated user input that is used to jsonp. */ -abstract class JsonHijackingSink extends DataFlow::Node { } - -/** Use ```print```, ```println```, ```write``` to output result. */ -private class WriterPrintln extends JsonHijackingSink { - WriterPrintln() { - exists(MethodAccess ma | - ma.getMethod().getName().regexpMatch("print|println|write") and - ma.getMethod() - .getDeclaringType() - .getASourceSupertype*() - .hasQualifiedName("java.io", "PrintWriter") and - ma.getArgument(0) = this.asExpr() - ) - } -} - -/** Spring Request Method return result. */ -private class SpringReturn extends JsonHijackingSink { - SpringReturn() { - exists(ReturnStmt rs, Method m | m = rs.getEnclosingCallable() | - m instanceof SpringRequestMappingMethod and - rs.getResult() = this.asExpr() - ) - } -} - -/** A concatenate expression using `(` and `)` or `);`. */ -class JsonHijackingExpr extends AddExpr { - JsonHijackingExpr() { - getRightOperand().toString().regexpMatch("\"\\)\"|\"\\);\"") and - getLeftOperand() - .(AddExpr) - .getLeftOperand() - .(AddExpr) - .getRightOperand() - .toString() - .regexpMatch("\"\\(\"") - } - - /** Get the jsonp function name of this expression */ - Expr getFunctionName() { - result = getLeftOperand().(AddExpr).getLeftOperand().(AddExpr).getLeftOperand() - } - - /** Get the json data of this expression */ - Expr getJsonExpr() { result = getLeftOperand().(AddExpr).getRightOperand() } -} - -/** A data flow configuration tracing flow from remote sources to jsonp function name. */ -class RemoteFlowConfig extends DataFlow2::Configuration { - RemoteFlowConfig() { this = "RemoteFlowConfig" } - - override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } - - override predicate isSink(DataFlow::Node sink) { - exists(JsonHijackingExpr jhe | jhe.getFunctionName() = sink.asExpr()) - } -} - -/** A data flow configuration tracing flow from json data to splicing jsonp data. */ -class JsonDataFlowConfig extends DataFlow2::Configuration { - JsonDataFlowConfig() { this = "JsonDataFlowConfig" } - - override predicate isSource(DataFlow::Node src) { src instanceof JsonpStringSource } - - override predicate isSink(DataFlow::Node sink) { - exists(JsonHijackingExpr jhe | jhe.getJsonExpr() = sink.asExpr()) - } -} - -/** Taint-tracking configuration tracing flow from user-controllable function name jsonp data to output jsonp data. */ -class JsonHijackingFlowConfig extends TaintTracking::Configuration { - JsonHijackingFlowConfig() { this = "JsonHijackingFlowConfig" } - - override predicate isSource(DataFlow::Node src) { - exists(JsonHijackingExpr jhe, JsonDataFlowConfig jdfc, RemoteFlowConfig rfc | - jhe = src.asExpr() and - jdfc.hasFlowTo(DataFlow::exprNode(jhe.getJsonExpr())) and - rfc.hasFlowTo(DataFlow::exprNode(jhe.getFunctionName())) - ) - } - - override predicate isSink(DataFlow::Node sink) { sink instanceof JsonHijackingSink } -} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.expected deleted file mode 100644 index 8efc3be1673..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.expected +++ /dev/null @@ -1,48 +0,0 @@ -edges -| JsonHijacking.java:28:32:28:68 | getParameter(...) : String | JsonHijacking.java:33:16:33:24 | resultStr | -| JsonHijacking.java:32:21:32:54 | ... + ... : String | JsonHijacking.java:33:16:33:24 | resultStr | -| JsonHijacking.java:40:32:40:68 | getParameter(...) : String | JsonHijacking.java:44:16:44:24 | resultStr | -| JsonHijacking.java:42:21:42:80 | ... + ... : String | JsonHijacking.java:44:16:44:24 | resultStr | -| JsonHijacking.java:51:32:51:68 | getParameter(...) : String | JsonHijacking.java:54:16:54:24 | resultStr | -| JsonHijacking.java:53:21:53:55 | ... + ... : String | JsonHijacking.java:54:16:54:24 | resultStr | -| JsonHijacking.java:61:32:61:68 | getParameter(...) : String | JsonHijacking.java:64:16:64:24 | resultStr | -| JsonHijacking.java:63:21:63:54 | ... + ... : String | JsonHijacking.java:64:16:64:24 | resultStr | -| JsonHijacking.java:72:32:72:68 | getParameter(...) : String | JsonHijacking.java:80:20:80:28 | resultStr | -| JsonHijacking.java:79:21:79:54 | ... + ... : String | JsonHijacking.java:80:20:80:28 | resultStr | -| JsonHijacking.java:88:32:88:68 | getParameter(...) : String | JsonHijacking.java:95:20:95:28 | resultStr | -| JsonHijacking.java:94:21:94:54 | ... + ... : String | JsonHijacking.java:95:20:95:28 | resultStr | -| JsonHijacking.java:102:32:102:68 | getParameter(...) : String | JsonHijacking.java:113:16:113:24 | resultStr | -nodes -| JsonHijacking.java:28:32:28:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonHijacking.java:32:21:32:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonHijacking.java:33:16:33:24 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:33:16:33:24 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:40:32:40:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonHijacking.java:42:21:42:80 | ... + ... : String | semmle.label | ... + ... : String | -| JsonHijacking.java:44:16:44:24 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:44:16:44:24 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:51:32:51:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonHijacking.java:53:21:53:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonHijacking.java:54:16:54:24 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:54:16:54:24 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:61:32:61:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonHijacking.java:63:21:63:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonHijacking.java:64:16:64:24 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:64:16:64:24 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:72:32:72:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonHijacking.java:79:21:79:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonHijacking.java:80:20:80:28 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:80:20:80:28 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:88:32:88:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonHijacking.java:94:21:94:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonHijacking.java:95:20:95:28 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:95:20:95:28 | resultStr | semmle.label | resultStr | -| JsonHijacking.java:102:32:102:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonHijacking.java:113:16:113:24 | resultStr | semmle.label | resultStr | -#select -| JsonHijacking.java:33:16:33:24 | resultStr | JsonHijacking.java:28:32:28:68 | getParameter(...) : String | JsonHijacking.java:33:16:33:24 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:28:32:28:68 | getParameter(...) | this user input | -| JsonHijacking.java:44:16:44:24 | resultStr | JsonHijacking.java:40:32:40:68 | getParameter(...) : String | JsonHijacking.java:44:16:44:24 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:40:32:40:68 | getParameter(...) | this user input | -| JsonHijacking.java:54:16:54:24 | resultStr | JsonHijacking.java:51:32:51:68 | getParameter(...) : String | JsonHijacking.java:54:16:54:24 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:51:32:51:68 | getParameter(...) | this user input | -| JsonHijacking.java:64:16:64:24 | resultStr | JsonHijacking.java:61:32:61:68 | getParameter(...) : String | JsonHijacking.java:64:16:64:24 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:61:32:61:68 | getParameter(...) | this user input | -| JsonHijacking.java:80:20:80:28 | resultStr | JsonHijacking.java:72:32:72:68 | getParameter(...) : String | JsonHijacking.java:80:20:80:28 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:72:32:72:68 | getParameter(...) | this user input | -| JsonHijacking.java:95:20:95:28 | resultStr | JsonHijacking.java:88:32:88:68 | getParameter(...) : String | JsonHijacking.java:95:20:95:28 | resultStr | Json Hijacking query might include code from $@. | JsonHijacking.java:88:32:88:68 | getParameter(...) | this user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.java deleted file mode 100644 index 9b473e0610c..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.java +++ /dev/null @@ -1,119 +0,0 @@ -import com.alibaba.fastjson.JSONObject; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.gson.Gson; -import java.io.PrintWriter; -import java.util.HashMap; -import java.util.Random; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -@Controller -public class JsonHijacking { - - private static HashMap hashMap = new HashMap(); - - static { - hashMap.put("username","admin"); - hashMap.put("password","123456"); - } - - - @GetMapping(value = "jsonp1") - @ResponseBody - public String bad1(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - - Gson gson = new Gson(); - String result = gson.toJson(hashMap); - resultStr = jsonpCallback + "(" + result + ")"; - return resultStr; - } - - @GetMapping(value = "jsonp2") - @ResponseBody - public String bad2(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - - resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; - - return resultStr; - } - - @GetMapping(value = "jsonp3") - @ResponseBody - public String bad3(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - String jsonStr = getJsonStr(hashMap); - resultStr = jsonpCallback + "(" + jsonStr + ")"; - return resultStr; - } - - @GetMapping(value = "jsonp4") - @ResponseBody - public String bad4(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - String restr = JSONObject.toJSONString(hashMap); - resultStr = jsonpCallback + "(" + restr + ");"; - return resultStr; - } - - @GetMapping(value = "jsonp5") - @ResponseBody - public void bad5(HttpServletRequest request, - HttpServletResponse response) throws Exception { - response.setContentType("application/json"); - String jsonpCallback = request.getParameter("jsonpCallback"); - PrintWriter pw = null; - Gson gson = new Gson(); - String result = gson.toJson(hashMap); - - String resultStr = null; - pw = response.getWriter(); - resultStr = jsonpCallback + "(" + result + ")"; - pw.println(resultStr); - } - - @GetMapping(value = "jsonp6") - @ResponseBody - public void bad6(HttpServletRequest request, - HttpServletResponse response) throws Exception { - response.setContentType("application/json"); - String jsonpCallback = request.getParameter("jsonpCallback"); - PrintWriter pw = null; - ObjectMapper mapper = new ObjectMapper(); - String result = mapper.writeValueAsString(hashMap); - String resultStr = null; - pw = response.getWriter(); - resultStr = jsonpCallback + "(" + result + ")"; - pw.println(resultStr); - } - - @GetMapping(value = "jsonp7") - @ResponseBody - public String good(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - - String val = ""; - Random random = new Random(); - for (int i = 0; i < 10; i++) { - val += String.valueOf(random.nextInt(10)); - } - // good - jsonpCallback = jsonpCallback + "_" + val; - String jsonStr = getJsonStr(hashMap); - resultStr = jsonpCallback + "(" + jsonStr + ")"; - return resultStr; - } - - public static String getJsonStr(Object result) { - return JSONObject.toJSONString(result); - } -} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.qlref b/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.qlref deleted file mode 100644 index e79471b3c1e..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonHijacking.qlref +++ /dev/null @@ -1 +0,0 @@ -Security/CWE/CWE-352/JsonHijacking.ql From f795d5e0d3b4f1a7fc5446755020786c3faecae1 Mon Sep 17 00:00:00 2001 From: haby0 Date: Sat, 27 Feb 2021 16:25:17 +0800 Subject: [PATCH 0007/1429] update JSONP Injection ql --- .../Security/CWE/CWE-352/JsonpInjection.ql | 45 +++++--- .../CWE/CWE-352/JsonpInjectionFilterLib.qll | 77 +++++++++++++ .../CWE/CWE-352/JsonpInjectionLib.qll | 65 ++++++++++- .../CWE/CWE-352/JsonpInjectionServlet.java | 60 ++++++++++ .../CWE/CWE-352/JsonpInjectionServlet1.java | 64 +++++++++++ .../CWE/CWE-352/JsonpInjectionServlet2.java | 50 +++++++++ .../semmle/code/java/frameworks/Servlets.qll | 27 +++++ .../security/CWE-352/JsonpInjection.expected | 106 +++++++++--------- .../security/CWE-352/JsonpInjection.java | 13 ++- .../core/annotation/AliasFor.class | Bin 385 -> 0 bytes .../web/bind/annotation/GetMapping.class | Bin 504 -> 0 bytes .../web/bind/annotation/RequestMapping.java | 2 + .../web/bind/annotation/RequestMethod.java | 15 +++ .../web/bind/annotation/ResponseBody.class | Bin 184 -> 0 bytes 14 files changed, 448 insertions(+), 76 deletions(-) create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjectionFilterLib.qll create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet.java create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet1.java create mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet2.java delete mode 100644 java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.class delete mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.class create mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestMethod.java delete mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.class diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql b/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql index e7ca2d41e34..53ee6182511 100644 --- a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql @@ -1,55 +1,68 @@ /** - * @name JSON Hijacking + * @name JSONP Injection * @description User-controlled callback function names that are not verified are vulnerable - * to json hijacking attacks. + * to jsonp injection attacks. * @kind path-problem * @problem.severity error * @precision high - * @id java/Json-hijacking + * @id java/JSONP-Injection * @tags security * external/cwe/cwe-352 */ import java import JsonpInjectionLib +import JsonpInjectionFilterLib import semmle.code.java.dataflow.FlowSources import semmle.code.java.deadcode.WebEntryPoints import DataFlow::PathGraph -class VerifAuth extends DataFlow::BarrierGuard { - VerifAuth() { + +/** If there is a method to verify `token`, `auth`, `referer`, and `origin`, it will not pass. */ +class ServletVerifAuth extends DataFlow::BarrierGuard { + ServletVerifAuth() { exists(MethodAccess ma, Node prod, Node succ | - this = ma and - ma.getMethod().getName().regexpMatch("(?i).*(token|auth|referer).*") and + ma.getMethod().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and prod instanceof RemoteFlowSource and succ.asExpr() = ma.getAnArgument() and - ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer).*") and - localFlowStep*(prod, succ) + ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and + localFlowStep*(prod, succ) and + this = ma ) } override predicate checks(Expr e, boolean branch) { - exists(ReturnStmt rs | - e = rs.getResult() and + exists(Node node | + node instanceof JsonpInjectionSink and + e = node.asExpr() and branch = true ) } } -/** Taint-tracking configuration tracing flow from remote sources to output jsonp data. */ +/** Taint-tracking configuration tracing flow from get method request sources to output jsonp data. */ class JsonpInjectionConfig extends TaintTracking::Configuration { JsonpInjectionConfig() { this = "JsonpInjectionConfig" } - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + override predicate isSource(DataFlow::Node source) { source instanceof GetHttpRequestSource } override predicate isSink(DataFlow::Node sink) { sink instanceof JsonpInjectionSink } - override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { guard instanceof VerifAuth } + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof ServletVerifAuth + } + + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { + exists(MethodAccess ma | + isRequestGetParamMethod(ma) and pred.asExpr() = ma.getQualifier() and succ.asExpr() = ma + ) + } } from DataFlow::PathNode source, DataFlow::PathNode sink, JsonpInjectionConfig conf where + not checks() = false and conf.hasFlowPath(source, sink) and exists(JsonpInjectionFlowConfig jhfc | jhfc.hasFlowTo(sink.getNode())) -select sink.getNode(), source, sink, "Json Hijacking query might include code from $@.", - source.getNode(), "this user input" \ No newline at end of file +select sink.getNode(), source, sink, "Jsonp Injection query might include code from $@.", + source.getNode(), "this user input" diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionFilterLib.qll b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionFilterLib.qll new file mode 100644 index 00000000000..b349bed2641 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionFilterLib.qll @@ -0,0 +1,77 @@ +/** + * @name JSONP Injection + * @description User-controlled callback function names that are not verified are vulnerable + * to json hijacking attacks. + * @kind path-problem + */ + +import java +import DataFlow +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking2 +import DataFlow::PathGraph + +class FilterVerifAuth extends DataFlow::BarrierGuard { + FilterVerifAuth() { + exists(MethodAccess ma, Node prod, Node succ | + ma.getMethod().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and + prod instanceof RemoteFlowSource and + succ.asExpr() = ma.getAnArgument() and + ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and + localFlowStep*(prod, succ) and + this = ma + ) + } + + override predicate checks(Expr e, boolean branch) { + exists(Node node | + node instanceof DoFilterMethodSink and + e = node.asExpr() and + branch = true + ) + } +} + +/** A data flow source for `Filter.doFilter` method paramters. */ +private class DoFilterMethodSource extends DataFlow::Node { + DoFilterMethodSource() { + exists(Method m | + isDoFilterMethod(m) and + m.getAParameter().getAnAccess() = this.asExpr() + ) + } +} + +/** A data flow sink for `FilterChain.doFilter` method qualifying expression. */ +private class DoFilterMethodSink extends DataFlow::Node { + DoFilterMethodSink() { + exists(MethodAccess ma, Method m | ma.getMethod() = m | + m.hasName("doFilter") and + m.getDeclaringType*().hasQualifiedName("javax.servlet", "FilterChain") and + ma.getQualifier() = this.asExpr() + ) + } +} + +/** Taint-tracking configuration tracing flow from `doFilter` method paramter source to output + * `FilterChain.doFilter` method qualifying expression. + * */ +class DoFilterMethodConfig extends TaintTracking::Configuration { + DoFilterMethodConfig() { this = "DoFilterMethodConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof DoFilterMethodSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof DoFilterMethodSink } + + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof FilterVerifAuth + } +} + +/** Implement class modeling verification for `Filter.doFilter`, return false if it fails. */ +boolean checks() { + exists(DataFlow::PathNode source, DataFlow::PathNode sink, DoFilterMethodConfig conf | + conf.hasFlowPath(source, sink) and + result = false + ) +} diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll index 4294a5e8c8f..3f730425823 100644 --- a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll @@ -5,6 +5,69 @@ import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.FlowSources import semmle.code.java.frameworks.spring.SpringController +/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ +private predicate isGetServletMethod(Method m) { + isServletRequestMethod(m) and m.getName() = "doGet" +} + +/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ +private predicate isGetSpringControllerMethod(Method m) { + exists(Annotation a | + a = m.getAnAnnotation() and + a.getType().hasQualifiedName("org.springframework.web.bind.annotation", "GetMapping") + ) + or + exists(Annotation a | + a = m.getAnAnnotation() and + a.getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestMapping") and + a.getValue("method").toString().regexpMatch("RequestMethod.GET|\\{...\\}") + ) +} + +/** Method parameters use the annotation `@RequestParam` or the parameter type is `ServletRequest`, `String`, `Object` */ +predicate checkSpringMethodParameterType(Method m, int i) { + m.getParameter(i).getType() instanceof ServletRequest + or + exists(Parameter p | + p = m.getParameter(i) and + p.hasAnnotation() and + p.getAnAnnotation() + .getType() + .hasQualifiedName("org.springframework.web.bind.annotation", "RequestParam") and + p.getType().getName().regexpMatch("String|Object") + ) + or + exists(Parameter p | + p = m.getParameter(i) and + not p.hasAnnotation() and + p.getType().getName().regexpMatch("String|Object") + ) +} + +/** A data flow source for get method request parameters. */ +abstract class GetHttpRequestSource extends DataFlow::Node { } + +/** A data flow source for servlet get method request parameters. */ +private class ServletGetHttpRequestSource extends GetHttpRequestSource { + ServletGetHttpRequestSource() { + exists(Method m | + isGetServletMethod(m) and + m.getParameter(0).getAnAccess() = this.asExpr() + ) + } +} + +/** A data flow source for spring controller get method request parameters. */ +private class SpringGetHttpRequestSource extends GetHttpRequestSource { + SpringGetHttpRequestSource() { + exists(SpringControllerMethod scm, int i | + isGetSpringControllerMethod(scm) and + checkSpringMethodParameterType(scm, i) and + scm.getParameter(i).getAnAccess() = this.asExpr() + ) + } +} + /** A data flow sink for unvalidated user input that is used to jsonp. */ abstract class JsonpInjectionSink extends DataFlow::Node { } @@ -26,7 +89,7 @@ private class WriterPrintln extends JsonpInjectionSink { private class SpringReturn extends JsonpInjectionSink { SpringReturn() { exists(ReturnStmt rs, Method m | m = rs.getEnclosingCallable() | - m instanceof SpringRequestMappingMethod and + isGetSpringControllerMethod(m) and rs.getResult() = this.asExpr() ) } diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet.java b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet.java new file mode 100644 index 00000000000..916cd9bf676 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet.java @@ -0,0 +1,60 @@ +import com.google.gson.Gson; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class JsonpInjectionServlet extends HttpServlet { + + private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + private static final long serialVersionUID = 1L; + + private String key = "test"; + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String jsonpCallback = req.getParameter("jsonpCallback"); + + PrintWriter pw = null; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + + String resultStr = null; + pw = resp.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + pw.flush(); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String jsonpCallback = req.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + + String resultStr = null; + pw = resp.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + pw.flush(); + } + + @Override + public void init(ServletConfig config) throws ServletException { + this.key = config.getInitParameter("key"); + System.out.println("åˆå§‹åŒ–" + this.key); + super.init(config); + } + +} diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet1.java b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet1.java new file mode 100644 index 00000000000..14ef76275b1 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet1.java @@ -0,0 +1,64 @@ +import com.google.gson.Gson; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class JsonpInjectionServlet1 extends HttpServlet { + + private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + private static final long serialVersionUID = 1L; + + private String key = "test"; + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doPost(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("application/json"); + String jsonpCallback = req.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String jsonResult = gson.toJson(hashMap); + + String referer = req.getHeader("Referer"); + + boolean result = verifReferer(referer); + + // good + if (result){ + String resultStr = null; + pw = resp.getWriter(); + resultStr = jsonpCallback + "(" + jsonResult + ")"; + pw.println(resultStr); + pw.flush(); + } + } + + public static boolean verifReferer(String referer){ + if (!referer.startsWith("http://test.com/")){ + return false; + } + return true; + } + + @Override + public void init(ServletConfig config) throws ServletException { + this.key = config.getInitParameter("key"); + System.out.println("åˆå§‹åŒ–" + this.key); + super.init(config); + } + +} diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet2.java b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet2.java new file mode 100644 index 00000000000..bbfbc2dc436 --- /dev/null +++ b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet2.java @@ -0,0 +1,50 @@ +import com.google.gson.Gson; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class JsonpInjectionServlet2 extends HttpServlet { + + private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + private static final long serialVersionUID = 1L; + + private String key = "test"; + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doPost(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("application/json"); + String jsonpCallback = req.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + + String resultStr = null; + pw = resp.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + pw.flush(); + } + + @Override + public void init(ServletConfig config) throws ServletException { + this.key = config.getInitParameter("key"); + System.out.println("åˆå§‹åŒ–" + this.key); + super.init(config); + } + +} diff --git a/java/ql/src/semmle/code/java/frameworks/Servlets.qll b/java/ql/src/semmle/code/java/frameworks/Servlets.qll index 3fad8c4e18b..b2054dc30cb 100644 --- a/java/ql/src/semmle/code/java/frameworks/Servlets.qll +++ b/java/ql/src/semmle/code/java/frameworks/Servlets.qll @@ -337,3 +337,30 @@ predicate isRequestGetParamMethod(MethodAccess ma) { ma.getMethod() instanceof ServletRequestGetParameterMapMethod or ma.getMethod() instanceof HttpServletRequestGetQueryStringMethod } + + +/** + * A class that has `javax.servlet.Filter` as an ancestor. + */ +class FilterClass extends Class { + FilterClass() { getAnAncestor().hasQualifiedName("javax.servlet", "Filter") } +} + + +/** + * The interface `javax.servlet.FilterChain` + */ +class FilterChain extends RefType { + FilterChain() { + hasQualifiedName("javax.servlet", "FilterChain") + } +} + +/** Holds if `m` is a request handler method (for example `doGet` or `doPost`). */ +predicate isDoFilterMethod(Method m) { + m.getDeclaringType() instanceof FilterClass and + m.getNumberOfParameters() = 3 and + m.getParameter(0).getType() instanceof ServletRequest and + m.getParameter(1).getType() instanceof ServletResponse and + m.getParameter(2).getType() instanceof FilterChain +} \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected index 019af8f5c05..7e3069cf1d9 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected @@ -1,60 +1,60 @@ edges -| JsonpInjection.java:28:32:28:68 | getParameter(...) : String | JsonpInjection.java:33:16:33:24 | resultStr | -| JsonpInjection.java:32:21:32:54 | ... + ... : String | JsonpInjection.java:33:16:33:24 | resultStr | -| JsonpInjection.java:40:32:40:68 | getParameter(...) : String | JsonpInjection.java:44:16:44:24 | resultStr | -| JsonpInjection.java:42:21:42:80 | ... + ... : String | JsonpInjection.java:44:16:44:24 | resultStr | -| JsonpInjection.java:51:32:51:68 | getParameter(...) : String | JsonpInjection.java:54:16:54:24 | resultStr | -| JsonpInjection.java:53:21:53:55 | ... + ... : String | JsonpInjection.java:54:16:54:24 | resultStr | -| JsonpInjection.java:61:32:61:68 | getParameter(...) : String | JsonpInjection.java:64:16:64:24 | resultStr | -| JsonpInjection.java:63:21:63:54 | ... + ... : String | JsonpInjection.java:64:16:64:24 | resultStr | -| JsonpInjection.java:72:32:72:68 | getParameter(...) : String | JsonpInjection.java:80:20:80:28 | resultStr | +| JsonpInjection.java:29:32:29:38 | request : HttpServletRequest | JsonpInjection.java:34:16:34:24 | resultStr | +| JsonpInjection.java:33:21:33:54 | ... + ... : String | JsonpInjection.java:34:16:34:24 | resultStr | +| JsonpInjection.java:41:32:41:38 | request : HttpServletRequest | JsonpInjection.java:45:16:45:24 | resultStr | +| JsonpInjection.java:43:21:43:80 | ... + ... : String | JsonpInjection.java:45:16:45:24 | resultStr | +| JsonpInjection.java:52:32:52:38 | request : HttpServletRequest | JsonpInjection.java:55:16:55:24 | resultStr | +| JsonpInjection.java:54:21:54:55 | ... + ... : String | JsonpInjection.java:55:16:55:24 | resultStr | +| JsonpInjection.java:62:32:62:38 | request : HttpServletRequest | JsonpInjection.java:65:16:65:24 | resultStr | +| JsonpInjection.java:64:21:64:54 | ... + ... : String | JsonpInjection.java:65:16:65:24 | resultStr | +| JsonpInjection.java:72:32:72:38 | request : HttpServletRequest | JsonpInjection.java:80:20:80:28 | resultStr | | JsonpInjection.java:79:21:79:54 | ... + ... : String | JsonpInjection.java:80:20:80:28 | resultStr | -| JsonpInjection.java:88:32:88:68 | getParameter(...) : String | JsonpInjection.java:95:20:95:28 | resultStr | -| JsonpInjection.java:94:21:94:54 | ... + ... : String | JsonpInjection.java:95:20:95:28 | resultStr | -| JsonpInjection.java:102:32:102:68 | getParameter(...) : String | JsonpInjection.java:113:16:113:24 | resultStr | -| JsonpInjection.java:128:25:128:59 | ... + ... : String | JsonpInjection.java:129:20:129:28 | resultStr | -| JsonpInjection.java:147:25:147:59 | ... + ... : String | JsonpInjection.java:148:20:148:28 | resultStr | +| JsonpInjection.java:87:32:87:38 | request : HttpServletRequest | JsonpInjection.java:94:20:94:28 | resultStr | +| JsonpInjection.java:93:21:93:54 | ... + ... : String | JsonpInjection.java:94:20:94:28 | resultStr | +| JsonpInjection.java:101:32:101:38 | request : HttpServletRequest | JsonpInjection.java:112:16:112:24 | resultStr | +| JsonpInjection.java:127:25:127:59 | ... + ... : String | JsonpInjection.java:128:20:128:28 | resultStr | +| JsonpInjection.java:148:25:148:59 | ... + ... : String | JsonpInjection.java:149:20:149:28 | resultStr | nodes -| JsonpInjection.java:28:32:28:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjection.java:32:21:32:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:33:16:33:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:33:16:33:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:40:32:40:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjection.java:42:21:42:80 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:44:16:44:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:44:16:44:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:51:32:51:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjection.java:53:21:53:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:54:16:54:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:54:16:54:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:61:32:61:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjection.java:63:21:63:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:64:16:64:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:64:16:64:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:72:32:72:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjection.java:29:32:29:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | +| JsonpInjection.java:33:21:33:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:34:16:34:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:34:16:34:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:41:32:41:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | +| JsonpInjection.java:43:21:43:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:45:16:45:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:45:16:45:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:52:32:52:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | +| JsonpInjection.java:54:21:54:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:55:16:55:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:55:16:55:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:62:32:62:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | +| JsonpInjection.java:64:21:64:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:65:16:65:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:65:16:65:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:72:32:72:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | | JsonpInjection.java:79:21:79:54 | ... + ... : String | semmle.label | ... + ... : String | | JsonpInjection.java:80:20:80:28 | resultStr | semmle.label | resultStr | | JsonpInjection.java:80:20:80:28 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:88:32:88:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjection.java:94:21:94:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:95:20:95:28 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:95:20:95:28 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:102:32:102:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjection.java:113:16:113:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:128:25:128:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:129:20:129:28 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:147:25:147:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:148:20:148:28 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:87:32:87:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | +| JsonpInjection.java:93:21:93:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:94:20:94:28 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:94:20:94:28 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:101:32:101:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | +| JsonpInjection.java:112:16:112:24 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:127:25:127:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:128:20:128:28 | resultStr | semmle.label | resultStr | +| JsonpInjection.java:148:25:148:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjection.java:149:20:149:28 | resultStr | semmle.label | resultStr | #select -| JsonpInjection.java:33:16:33:24 | resultStr | JsonpInjection.java:28:32:28:68 | getParameter(...) : String | JsonpInjection.java:33:16:33:24 | resultStr | Json Hijacking query -might include code from $@. | JsonpInjection.java:28:32:28:68 | getParameter(...) | this user input | -| JsonpInjection.java:44:16:44:24 | resultStr | JsonpInjection.java:40:32:40:68 | getParameter(...) : String | JsonpInjection.java:44:16:44:24 | resultStr | Json Hijacking query -might include code from $@. | JsonpInjection.java:40:32:40:68 | getParameter(...) | this user input | -| JsonpInjection.java:54:16:54:24 | resultStr | JsonpInjection.java:51:32:51:68 | getParameter(...) : String | JsonpInjection.java:54:16:54:24 | resultStr | Json Hijacking query -might include code from $@. | JsonpInjection.java:51:32:51:68 | getParameter(...) | this user input | -| JsonpInjection.java:64:16:64:24 | resultStr | JsonpInjection.java:61:32:61:68 | getParameter(...) : String | JsonpInjection.java:64:16:64:24 | resultStr | Json Hijacking query -might include code from $@. | JsonpInjection.java:61:32:61:68 | getParameter(...) | this user input | -| JsonpInjection.java:80:20:80:28 | resultStr | JsonpInjection.java:72:32:72:68 | getParameter(...) : String | JsonpInjection.java:80:20:80:28 | resultStr | Json Hijacking query -might include code from $@. | JsonpInjection.java:72:32:72:68 | getParameter(...) | this user input | -| JsonpInjection.java:95:20:95:28 | resultStr | JsonpInjection.java:88:32:88:68 | getParameter(...) : String | JsonpInjection.java:95:20:95:28 | resultStr | Json Hijacking query -might include code from $@. | JsonpInjection.java:88:32:88:68 | getParameter(...) | this user input | \ No newline at end of file +| JsonpInjection.java:34:16:34:24 | resultStr | JsonpInjection.java:29:32:29:38 | request : HttpServletRequest | JsonpInjection.java:34:16:34:24 | +resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:29:32:29:38 | request | this user input | +| JsonpInjection.java:45:16:45:24 | resultStr | JsonpInjection.java:41:32:41:38 | request : HttpServletRequest | JsonpInjection.java:45:16:45:24 | +resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:41:32:41:38 | request | this user input | +| JsonpInjection.java:55:16:55:24 | resultStr | JsonpInjection.java:52:32:52:38 | request : HttpServletRequest | JsonpInjection.java:55:16:55:24 | +resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:52:32:52:38 | request | this user input | +| JsonpInjection.java:65:16:65:24 | resultStr | JsonpInjection.java:62:32:62:38 | request : HttpServletRequest | JsonpInjection.java:65:16:65:24 | +resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:62:32:62:38 | request | this user input | +| JsonpInjection.java:80:20:80:28 | resultStr | JsonpInjection.java:72:32:72:38 | request : HttpServletRequest | JsonpInjection.java:80:20:80:28 | +resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:72:32:72:38 | request | this user input | +| JsonpInjection.java:94:20:94:28 | resultStr | JsonpInjection.java:87:32:87:38 | request : HttpServletRequest | JsonpInjection.java:94:20:94:28 | +resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:87:32:87:38 | request | this user input | \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java index df3aa2c02fe..9f079513a8b 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java @@ -8,11 +8,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class JsonpInjection { - private static HashMap hashMap = new HashMap(); static { @@ -21,7 +22,7 @@ public class JsonpInjection { } - @GetMapping(value = "jsonp1") + @GetMapping(value = "jsonp1", produces="text/javascript") @ResponseBody public String bad1(HttpServletRequest request) { String resultStr = null; @@ -68,7 +69,6 @@ public class JsonpInjection { @ResponseBody public void bad5(HttpServletRequest request, HttpServletResponse response) throws Exception { - response.setContentType("application/json"); String jsonpCallback = request.getParameter("jsonpCallback"); PrintWriter pw = null; Gson gson = new Gson(); @@ -84,7 +84,6 @@ public class JsonpInjection { @ResponseBody public void bad6(HttpServletRequest request, HttpServletResponse response) throws Exception { - response.setContentType("application/json"); String jsonpCallback = request.getParameter("jsonpCallback"); PrintWriter pw = null; ObjectMapper mapper = new ObjectMapper(); @@ -141,8 +140,10 @@ public class JsonpInjection { String referer = request.getHeader("Referer"); boolean result = verifReferer(referer); + + boolean test = result; // good - if (result){ + if (test){ String jsonStr = getJsonStr(hashMap); resultStr = jsonpCallback + "(" + jsonStr + ")"; return resultStr; @@ -168,4 +169,4 @@ public class JsonpInjection { } return true; } -} +} \ No newline at end of file diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.class b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.class deleted file mode 100644 index 438065489278b92503abc2ad8d75716c5e56ac08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 385 zcmb7=!Ab)$5QhJ0x31kDdrH&Kx0=)qG#3PM4!PcXZrOKO@(l3m};gAd?CiL-}x zJqaEL=Fj~6^G&|KKRyB6W0qr@<21(^Vbrp1G~wdrcD3b}m1S3}bqdDS4}|lDb3So0 z-aYCKH#QMKxO{0`GCTd`S`$rab#IG=`O1e{#kVeF6L_cJeRx%s4_fgdPA#nAxb#7` zj5*1|vPl9`tbG$Iy);(DbZ?q>Y=pc21QTZcMbG6{R|0?4KmBGoU|o}(H;@|2R}C^k ghLPwaQNxHF$I?t>JeJBL3UL&FIx;a%x-6Xh0OC_*bpQYW diff --git a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.class b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.class deleted file mode 100644 index 5392ca0ebc1593d6bfa2f7d765ee4ca169fb260f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 504 zcma)&y-ve06orr5G%4k$Ewlp@8)_FUu~3N#3Bgi?)JiO!oYW02i5(KVeK!UkfQLfd z3?&dTFj%_xlg>TI=i~G39l#Za0geNl1Q;-QTBMR;Fd9$SVk3AWbj;^AS316C=-+5< ztgy=HTe%W0u?%2nZA9WoH5`o>f62T|*k=Ym6S+tWhIV9h;Zj+SS#FjtD#y;;xIB_~ zDxp)|dubm;mXYs88HC|<=CoC*d{Tu96Imr8>11m1m={?Yb44Ckk!ycGS!yuAF9#FEVXJbh%nj0^%G u-TFC+dFlH8Nm;4MC5#O62q7eGj&Kvy7#SEDn1GlW=t>44%>pEu7+3+XjWw+R From 95d1994196dc52ad516b7c6df06e673be8906d9b Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Mon, 1 Mar 2021 22:06:52 +0000 Subject: [PATCH 0008/1429] Query to check sensitive cookies without the HttpOnly flag set --- .../CWE-1004/SensitiveCookieNotHttpOnly.java | 44 +++ .../CWE-1004/SensitiveCookieNotHttpOnly.qhelp | 27 ++ .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 194 ++++++++++ .../SensitiveCookieNotHttpOnly.expected | 13 + .../CWE-1004/SensitiveCookieNotHttpOnly.java | 57 +++ .../CWE-1004/SensitiveCookieNotHttpOnly.qlref | 1 + .../query-tests/security/CWE-1004/options | 1 + .../javax/ws/rs/core/Cookie.java | 187 +++++++++ .../javax/ws/rs/core/NewCookie.java | 359 ++++++++++++++++++ .../javax/servlet/http/Cookie.java | 33 ++ .../servlet/http/HttpServletResponse.java | 6 + 11 files changed, 922 insertions(+) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.java create mode 100644 java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql create mode 100644 java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.qlref create mode 100644 java/ql/test/experimental/query-tests/security/CWE-1004/options create mode 100644 java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/Cookie.java create mode 100644 java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.java b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.java new file mode 100644 index 00000000000..48d80707ff8 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.java @@ -0,0 +1,44 @@ +class SensitiveCookieNotHttpOnly { + // GOOD - Create a sensitive cookie with the `HttpOnly` flag set. + public void addCookie(String jwt_token, HttpServletRequest request, HttpServletResponse response) { + Cookie jwtCookie =new Cookie("jwt_token", jwt_token); + jwtCookie.setPath("/"); + jwtCookie.setMaxAge(3600*24*7); + jwtCookie.setHttpOnly(true); + response.addCookie(jwtCookie); + } + + // BAD - Create a sensitive cookie without the `HttpOnly` flag set. + public void addCookie2(String jwt_token, String userId, HttpServletRequest request, HttpServletResponse response) { + Cookie jwtCookie =new Cookie("jwt_token", jwt_token); + jwtCookie.setPath("/"); + jwtCookie.setMaxAge(3600*24*7); + response.addCookie(jwtCookie); + } + + // GOOD - Set a sensitive cookie header with the `HttpOnly` flag set. + public void addCookie3(String authId, HttpServletRequest request, HttpServletResponse response) { + response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure"); + } + + // BAD - Set a sensitive cookie header without the `HttpOnly` flag set. + public void addCookie4(String authId, HttpServletRequest request, HttpServletResponse response) { + response.addHeader("Set-Cookie", "token=" +authId + ";Secure"); + } + + // GOOD - Set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` with the `HttpOnly` flag set through string concatenation. + public void addCookie5(String accessKey, HttpServletRequest request, HttpServletResponse response) { + response.setHeader("Set-Cookie", new NewCookie("session-access-key", accessKey, "/", null, null, 0, true) + ";HttpOnly"); + } + + // BAD - Set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` without the `HttpOnly` flag set. + public void addCookie6(String accessKey, HttpServletRequest request, HttpServletResponse response) { + response.setHeader("Set-Cookie", new NewCookie("session-access-key", accessKey, "/", null, null, 0, true).toString()); + } + + // GOOD - Set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` with the `HttpOnly` flag set through the constructor. + public void addCookie7(String accessKey, HttpServletRequest request, HttpServletResponse response) { + NewCookie accessKeyCookie = new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true); + response.setHeader("Set-Cookie", accessKeyCookie.toString()); + } +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.qhelp b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.qhelp new file mode 100644 index 00000000000..880ed767be9 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.qhelp @@ -0,0 +1,27 @@ + + + + +

    Cross-Site Scripting (XSS) is categorized as one of the OWASP Top 10 Security Vulnerabilities. The HttpOnly flag directs compatible browsers to prevent client-side script from accessing cookies. Including the HttpOnly flag in the Set-Cookie HTTP response header for a sensitive cookie helps mitigate the risk associated with XSS where an attacker's script code attempts to read the contents of a cookie and exfiltrate information obtained.

    +
    + + +

    Use the HttpOnly flag when generating a cookie containing sensitive information to help mitigate the risk of client side script accessing the protected cookie.

    +
    + + +

    The following example shows two ways of generating sensitive cookies. In the 'BAD' cases, the HttpOnly flag is not set. In the 'GOOD' cases, the HttpOnly flag is set.

    + +
    + + +
  • + PortSwigger: + Cookie without HttpOnly flag set +
  • +
  • + OWASP: + HttpOnly +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql new file mode 100644 index 00000000000..bf4e60134c9 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -0,0 +1,194 @@ +/** + * @name Sensitive cookies without the HttpOnly response header set + * @description Sensitive cookies without 'HttpOnly' leaves session cookies vulnerable to an XSS attack. + * @kind path-problem + * @id java/sensitive-cookie-not-httponly + * @tags security + * external/cwe/cwe-1004 + */ + +import java +import semmle.code.java.frameworks.Servlets +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking +import DataFlow::PathGraph + +/** Gets a regular expression for matching common names of sensitive cookies. */ +string getSensitiveCookieNameRegex() { result = "(?i).*(auth|session|token|key|credential).*" } + +/** Holds if a string is concatenated with the name of a sensitive cookie. */ +predicate isSensitiveCookieNameExpr(Expr expr) { + expr.(StringLiteral) + .getRepresentedString() + .toLowerCase() + .regexpMatch(getSensitiveCookieNameRegex()) or + isSensitiveCookieNameExpr(expr.(AddExpr).getAnOperand()) +} + +/** Holds if a string is concatenated with the `HttpOnly` flag. */ +predicate hasHttpOnlyExpr(Expr expr) { + expr.(StringLiteral).getRepresentedString().toLowerCase().matches("%httponly%") or + hasHttpOnlyExpr(expr.(AddExpr).getAnOperand()) +} + +/** The method call `Set-Cookie` of `addHeader` or `setHeader`. */ +class SetCookieMethodAccess extends MethodAccess { + SetCookieMethodAccess() { + ( + this.getMethod() instanceof ResponseAddHeaderMethod or + this.getMethod() instanceof ResponseSetHeaderMethod + ) and + this.getArgument(0).(StringLiteral).getRepresentedString().toLowerCase() = "set-cookie" + } +} + +/** Sensitive cookie name used in a `Cookie` constructor or a `Set-Cookie` call. */ +class SensitiveCookieNameExpr extends Expr { + SensitiveCookieNameExpr() { + isSensitiveCookieNameExpr(this) and + ( + exists( + ClassInstanceExpr cie // new Cookie("jwt_token", token) + | + ( + cie.getConstructor().getDeclaringType().hasQualifiedName("javax.servlet.http", "Cookie") or + cie.getConstructor() + .getDeclaringType() + .getASupertype*() + .hasQualifiedName("javax.ws.rs.core", "Cookie") or + cie.getConstructor() + .getDeclaringType() + .getASupertype*() + .hasQualifiedName("jakarta.ws.rs.core", "Cookie") + ) and + DataFlow::localExprFlow(this, cie.getArgument(0)) + ) + or + exists( + SetCookieMethodAccess ma // response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure") + | + DataFlow::localExprFlow(this, ma.getArgument(1)) + ) + ) + } +} + +/** Sink of adding a cookie to the HTTP response. */ +class CookieResponseSink extends DataFlow::ExprNode { + CookieResponseSink() { + exists(MethodAccess ma | + ( + ma.getMethod() instanceof ResponseAddCookieMethod or + ma instanceof SetCookieMethodAccess + ) and + ma.getAnArgument() = this.getExpr() + ) + } +} + +/** Holds if the `node` is a method call of `setHttpOnly(true)` on a cookie. */ +predicate setHttpOnlyMethodAccess(DataFlow::Node node) { + exists( + MethodAccess addCookie, Variable cookie, MethodAccess m // jwtCookie.setHttpOnly(true) + | + addCookie.getMethod() instanceof ResponseAddCookieMethod and + addCookie.getArgument(0) = cookie.getAnAccess() and + m.getMethod().getName() = "setHttpOnly" and + m.getArgument(0).(BooleanLiteral).getBooleanValue() = true and + m.getQualifier() = cookie.getAnAccess() and + node.asExpr() = cookie.getAnAccess() + ) +} + +/** Holds if the `node` is a method call of `Set-Cookie` header with the `HttpOnly` flag whose cookie name is sensitive. */ +predicate setHttpOnlyInSetCookie(DataFlow::Node node) { + exists(SetCookieMethodAccess sa | + hasHttpOnlyExpr(node.asExpr()) and + DataFlow::localExprFlow(node.asExpr(), sa.getArgument(1)) + ) +} + +/** Holds if the `node` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */ +predicate setHttpOnlyInNewCookie(DataFlow::Node node) { + exists(ClassInstanceExpr cie | + cie.getConstructor().getDeclaringType().hasName("NewCookie") and + DataFlow::localExprFlow(node.asExpr(), cie.getArgument(0)) and + ( + cie.getNumArgument() = 6 and cie.getArgument(5).(BooleanLiteral).getBooleanValue() = true // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) + or + cie.getNumArgument() = 8 and + cie.getArgument(6).getType() instanceof BooleanType and + cie.getArgument(7).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) + or + cie.getNumArgument() = 10 and cie.getArgument(9).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) + ) + ) +} + +/** + * Holds if the node is a test method indicated by: + * a) in a test directory such as `src/test/java` + * b) in a test package whose name has the word `test` + * c) in a test class whose name has the word `test` + * d) in a test class implementing a test framework such as JUnit or TestNG + */ +predicate isTestMethod(DataFlow::Node node) { + exists(MethodAccess ma, Method m | + node.asExpr() = ma.getAnArgument() and + m = ma.getEnclosingCallable() and + ( + m.getDeclaringType().getName().toLowerCase().matches("%test%") or // Simple check to exclude test classes to reduce FPs + m.getDeclaringType().getPackage().getName().toLowerCase().matches("%test%") or // Simple check to exclude classes in test packages to reduce FPs + exists(m.getLocation().getFile().getAbsolutePath().indexOf("/src/test/java")) or // Match test directory structure of build tools like maven + m instanceof TestMethod // Test method of a test case implementing a test framework such as JUnit or TestNG + ) + ) +} + +/** A taint configuration tracking flow from a sensitive cookie without HttpOnly flag set to its HTTP response. */ +class MissingHttpOnlyConfiguration extends TaintTracking::Configuration { + MissingHttpOnlyConfiguration() { this = "MissingHttpOnlyConfiguration" } + + override predicate isSource(DataFlow::Node source) { + source.asExpr() instanceof SensitiveCookieNameExpr + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof CookieResponseSink } + + override predicate isSanitizer(DataFlow::Node node) { + // cookie.setHttpOnly(true) + setHttpOnlyMethodAccess(node) + or + // response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure") + setHttpOnlyInSetCookie(node) + or + // new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true) + setHttpOnlyInNewCookie(node) + or + // Test class or method + isTestMethod(node) + } + + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { + exists( + ClassInstanceExpr cie // `NewCookie` constructor + | + cie.getAnArgument() = pred.asExpr() and + cie = succ.asExpr() and + cie.getConstructor().getDeclaringType().hasName("NewCookie") + ) + or + exists( + MethodAccess ma // `toString` call on a cookie object + | + ma.getQualifier() = pred.asExpr() and + ma.getMethod().hasName("toString") and + ma = succ.asExpr() + ) + } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, MissingHttpOnlyConfiguration c +where c.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "$@ doesn't have the HttpOnly flag set.", source.getNode(), + "This sensitive cookie" diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected new file mode 100644 index 00000000000..84d6be3863a --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected @@ -0,0 +1,13 @@ +edges +| SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | +| SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | +nodes +| SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" : String | semmle.label | "jwt_token" : String | +| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | semmle.label | jwtCookie | +| SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | semmle.label | ... + ... | +| SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | semmle.label | toString(...) | +| SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" : String | semmle.label | "session-access-key" : String | +#select +| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" | This sensitive cookie | diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java new file mode 100644 index 00000000000..5e4f349f7c8 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java @@ -0,0 +1,57 @@ +import java.io.IOException; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletException; + +import javax.ws.rs.core.NewCookie; + +class SensitiveCookieNotHttpOnly { + // GOOD - Tests adding a sensitive cookie with the `HttpOnly` flag set. + public void addCookie(String jwt_token, HttpServletRequest request, HttpServletResponse response) { + Cookie jwtCookie =new Cookie("jwt_token", jwt_token); + jwtCookie.setPath("/"); + jwtCookie.setMaxAge(3600*24*7); + jwtCookie.setHttpOnly(true); + response.addCookie(jwtCookie); + } + + // BAD - Tests adding a sensitive cookie without the `HttpOnly` flag set. + public void addCookie2(String jwt_token, String userId, HttpServletRequest request, HttpServletResponse response) { + Cookie jwtCookie =new Cookie("jwt_token", jwt_token); + Cookie userIdCookie =new Cookie("user_id", userId.toString()); + jwtCookie.setPath("/"); + userIdCookie.setPath("/"); + jwtCookie.setMaxAge(3600*24*7); + userIdCookie.setMaxAge(3600*24*7); + response.addCookie(jwtCookie); + response.addCookie(userIdCookie); + } + + // GOOD - Tests set a sensitive cookie header with the `HttpOnly` flag set. + public void addCookie3(String authId, HttpServletRequest request, HttpServletResponse response) { + response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure"); + } + + // BAD - Tests set a sensitive cookie header without the `HttpOnly` flag set. + public void addCookie4(String authId, HttpServletRequest request, HttpServletResponse response) { + response.addHeader("Set-Cookie", "token=" +authId + ";Secure"); + } + + // GOOD - Tests set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` with the `HttpOnly` flag set through string concatenation. + public void addCookie5(String accessKey, HttpServletRequest request, HttpServletResponse response) { + response.setHeader("Set-Cookie", new NewCookie("session-access-key", accessKey, "/", null, null, 0, true) + ";HttpOnly"); + } + + // BAD - Tests set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` without the `HttpOnly` flag set. + public void addCookie6(String accessKey, HttpServletRequest request, HttpServletResponse response) { + response.setHeader("Set-Cookie", new NewCookie("session-access-key", accessKey, "/", null, null, 0, true).toString()); + } + + // GOOD - Tests set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` with the `HttpOnly` flag set through the constructor. + public void addCookie7(String accessKey, HttpServletRequest request, HttpServletResponse response) { + NewCookie accessKeyCookie = new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true); + response.setHeader("Set-Cookie", accessKeyCookie.toString()); + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.qlref b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.qlref new file mode 100644 index 00000000000..cc2baaf6f7b --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/options b/java/ql/test/experimental/query-tests/security/CWE-1004/options new file mode 100644 index 00000000000..7f2b253fb20 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/options @@ -0,0 +1 @@ +// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jsr311-api-1.1.1 \ No newline at end of file diff --git a/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/Cookie.java b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/Cookie.java new file mode 100644 index 00000000000..4e4c7585c35 --- /dev/null +++ b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/Cookie.java @@ -0,0 +1,187 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2010-2015 Oracle and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * http://glassfish.java.net/public/CDDL+GPL_1_1.html + * or packager/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at packager/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * Oracle designates this particular file as subject to the "Classpath" + * exception as provided by Oracle in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ + +package javax.ws.rs.core; + +/** + * Represents the value of a HTTP cookie, transferred in a request. + * RFC 2109 specifies the legal characters for name, + * value, path and domain. The default version of 1 corresponds to RFC 2109. + * + * @author Paul Sandoz + * @author Marc Hadley + * @see IETF RFC 2109 + * @since 1.0 + */ +public class Cookie { + /** + * Cookies using the default version correspond to RFC 2109. + */ + public static final int DEFAULT_VERSION = 1; + + /** + * Create a new instance. + * + * @param name the name of the cookie. + * @param value the value of the cookie. + * @param path the URI path for which the cookie is valid. + * @param domain the host domain for which the cookie is valid. + * @param version the version of the specification to which the cookie complies. + * @throws IllegalArgumentException if name is {@code null}. + */ + public Cookie(final String name, final String value, final String path, final String domain, final int version) + throws IllegalArgumentException { + } + + /** + * Create a new instance. + * + * @param name the name of the cookie. + * @param value the value of the cookie. + * @param path the URI path for which the cookie is valid. + * @param domain the host domain for which the cookie is valid. + * @throws IllegalArgumentException if name is {@code null}. + */ + public Cookie(final String name, final String value, final String path, final String domain) + throws IllegalArgumentException { + } + + /** + * Create a new instance. + * + * @param name the name of the cookie. + * @param value the value of the cookie. + * @throws IllegalArgumentException if name is {@code null}. + */ + public Cookie(final String name, final String value) + throws IllegalArgumentException { + } + + /** + * Creates a new instance of {@code Cookie} by parsing the supplied string. + * + * @param value the cookie string. + * @return the newly created {@code Cookie}. + * @throws IllegalArgumentException if the supplied string cannot be parsed + * or is {@code null}. + */ + public static Cookie valueOf(final String value) { + return null; + } + + /** + * Get the name of the cookie. + * + * @return the cookie name. + */ + public String getName() { + return null; + } + + /** + * Get the value of the cookie. + * + * @return the cookie value. + */ + public String getValue() { + return null; + } + + /** + * Get the version of the cookie. + * + * @return the cookie version. + */ + public int getVersion() { + return -1; + } + + /** + * Get the domain of the cookie. + * + * @return the cookie domain. + */ + public String getDomain() { + return null; + } + + /** + * Get the path of the cookie. + * + * @return the cookie path. + */ + public String getPath() { + return null; + } + + /** + * Convert the cookie to a string suitable for use as the value of the + * corresponding HTTP header. + * + * @return a stringified cookie. + */ + @Override + public String toString() { + return null; + } + + /** + * Generate a hash code by hashing all of the cookies properties. + * + * @return the cookie hash code. + */ + @Override + public int hashCode() { + return -1; + } + + /** + * Compare for equality. + * + * @param obj the object to compare to. + * @return {@code true}, if the object is a {@code Cookie} with the same + * value for all properties, {@code false} otherwise. + */ + @SuppressWarnings({"StringEquality", "RedundantIfStatement"}) + @Override + public boolean equals(final Object obj) { + return true; + } +} \ No newline at end of file diff --git a/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java new file mode 100644 index 00000000000..43a4899e0ca --- /dev/null +++ b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java @@ -0,0 +1,359 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2010-2015 Oracle and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * http://glassfish.java.net/public/CDDL+GPL_1_1.html + * or packager/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at packager/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * Oracle designates this particular file as subject to the "Classpath" + * exception as provided by Oracle in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ + +package javax.ws.rs.core; + +import java.util.Date; + +/** + * Used to create a new HTTP cookie, transferred in a response. + * + * @author Paul Sandoz + * @author Marc Hadley + * @see IETF RFC 2109 + * @since 1.0 + */ +public class NewCookie extends Cookie { + + /** + * Specifies that the cookie expires with the current application/browser session. + */ + public static final int DEFAULT_MAX_AGE = -1; + + private final String comment; + private final int maxAge; + private final Date expiry; + private final boolean secure; + private final boolean httpOnly; + + /** + * Create a new instance. + * + * @param name the name of the cookie. + * @param value the value of the cookie. + * @throws IllegalArgumentException if name is {@code null}. + */ + public NewCookie(String name, String value) { + this(name, value, null, null, DEFAULT_VERSION, null, DEFAULT_MAX_AGE, null, false, false); + } + + /** + * Create a new instance. + * + * @param name the name of the cookie. + * @param value the value of the cookie. + * @param path the URI path for which the cookie is valid. + * @param domain the host domain for which the cookie is valid. + * @param comment the comment. + * @param maxAge the maximum age of the cookie in seconds. + * @param secure specifies whether the cookie will only be sent over a secure connection. + * @throws IllegalArgumentException if name is {@code null}. + */ + public NewCookie(String name, + String value, + String path, + String domain, + String comment, + int maxAge, + boolean secure) { + this(name, value, path, domain, DEFAULT_VERSION, comment, maxAge, null, secure, false); + } + + /** + * Create a new instance. + * + * @param name the name of the cookie. + * @param value the value of the cookie. + * @param path the URI path for which the cookie is valid. + * @param domain the host domain for which the cookie is valid. + * @param comment the comment. + * @param maxAge the maximum age of the cookie in seconds. + * @param secure specifies whether the cookie will only be sent over a secure connection. + * @param httpOnly if {@code true} make the cookie HTTP only, i.e. only visible as part of an HTTP request. + * @throws IllegalArgumentException if name is {@code null}. + * @since 2.0 + */ + public NewCookie(String name, + String value, + String path, + String domain, + String comment, + int maxAge, + boolean secure, + boolean httpOnly) { + this(name, value, path, domain, DEFAULT_VERSION, comment, maxAge, null, secure, httpOnly); + } + + /** + * Create a new instance. + * + * @param name the name of the cookie + * @param value the value of the cookie + * @param path the URI path for which the cookie is valid + * @param domain the host domain for which the cookie is valid + * @param version the version of the specification to which the cookie complies + * @param comment the comment + * @param maxAge the maximum age of the cookie in seconds + * @param secure specifies whether the cookie will only be sent over a secure connection + * @throws IllegalArgumentException if name is {@code null}. + */ + public NewCookie(String name, + String value, + String path, + String domain, + int version, + String comment, + int maxAge, + boolean secure) { + this(name, value, path, domain, version, comment, maxAge, null, secure, false); + } + + /** + * Create a new instance. + * + * @param name the name of the cookie + * @param value the value of the cookie + * @param path the URI path for which the cookie is valid + * @param domain the host domain for which the cookie is valid + * @param version the version of the specification to which the cookie complies + * @param comment the comment + * @param maxAge the maximum age of the cookie in seconds + * @param expiry the cookie expiry date. + * @param secure specifies whether the cookie will only be sent over a secure connection + * @param httpOnly if {@code true} make the cookie HTTP only, i.e. only visible as part of an HTTP request. + * @throws IllegalArgumentException if name is {@code null}. + * @since 2.0 + */ + public NewCookie(String name, + String value, + String path, + String domain, + int version, + String comment, + int maxAge, + Date expiry, + boolean secure, + boolean httpOnly) { + super(name, value, path, domain, version); + this.comment = comment; + this.maxAge = maxAge; + this.expiry = expiry; + this.secure = secure; + this.httpOnly = httpOnly; + } + + /** + * Create a new instance copying the information in the supplied cookie. + * + * @param cookie the cookie to clone. + * @throws IllegalArgumentException if cookie is {@code null}. + */ + public NewCookie(Cookie cookie) { + this(cookie, null, DEFAULT_MAX_AGE, null, false, false); + } + + /** + * Create a new instance supplementing the information in the supplied cookie. + * + * @param cookie the cookie to clone. + * @param comment the comment. + * @param maxAge the maximum age of the cookie in seconds. + * @param secure specifies whether the cookie will only be sent over a secure connection. + * @throws IllegalArgumentException if cookie is {@code null}. + */ + public NewCookie(Cookie cookie, String comment, int maxAge, boolean secure) { + this(cookie, comment, maxAge, null, secure, false); + } + + /** + * Create a new instance supplementing the information in the supplied cookie. + * + * @param cookie the cookie to clone. + * @param comment the comment. + * @param maxAge the maximum age of the cookie in seconds. + * @param expiry the cookie expiry date. + * @param secure specifies whether the cookie will only be sent over a secure connection. + * @param httpOnly if {@code true} make the cookie HTTP only, i.e. only visible as part of an HTTP request. + * @throws IllegalArgumentException if cookie is {@code null}. + * @since 2.0 + */ + public NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) { + super(cookie == null ? null : cookie.getName(), + cookie == null ? null : cookie.getValue(), + cookie == null ? null : cookie.getPath(), + cookie == null ? null : cookie.getDomain(), + cookie == null ? Cookie.DEFAULT_VERSION : cookie.getVersion()); + this.comment = comment; + this.maxAge = maxAge; + this.expiry = expiry; + this.secure = secure; + this.httpOnly = httpOnly; + } + + /** + * Creates a new instance of NewCookie by parsing the supplied string. + * + * @param value the cookie string. + * @return the newly created {@code NewCookie}. + * @throws IllegalArgumentException if the supplied string cannot be parsed + * or is {@code null}. + */ + public static NewCookie valueOf(String value) { + return null; + } + + /** + * Get the comment associated with the cookie. + * + * @return the comment or null if none set + */ + public String getComment() { + return null; + } + + /** + * Get the maximum age of the the cookie in seconds. Cookies older than + * the maximum age are discarded. A cookie can be unset by sending a new + * cookie with maximum age of 0 since it will overwrite any existing cookie + * and then be immediately discarded. The default value of {@code -1} indicates + * that the cookie will be discarded at the end of the browser/application session. + *

    + * Note that it is recommended to use {@code Max-Age} to control cookie + * expiration, however some browsers do not understand {@code Max-Age}, in which case + * setting {@link #getExpiry()} Expires} parameter may be necessary. + *

    + * + * @return the maximum age in seconds. + * @see #getExpiry() + */ + public int getMaxAge() { + return -1; + } + + /** + * Get the cookie expiry date. Cookies whose expiry date has passed are discarded. + * A cookie can be unset by setting a new cookie with an expiry date in the past, + * typically the lowest possible date that can be set. + *

    + * Note that it is recommended to use {@link #getMaxAge() Max-Age} to control cookie + * expiration, however some browsers do not understand {@code Max-Age}, in which case + * setting {@code Expires} parameter may be necessary. + *

    + * + * @return cookie expiry date or {@code null} if no expiry date was set. + * @see #getMaxAge() + * @since 2.0 + */ + public Date getExpiry() { + return null; + } + + /** + * Whether the cookie will only be sent over a secure connection. Defaults + * to {@code false}. + * + * @return {@code true} if the cookie will only be sent over a secure connection, + * {@code false} otherwise. + */ + public boolean isSecure() { + return false; + } + + /** + * Returns {@code true} if this cookie contains the {@code HttpOnly} attribute. + * This means that the cookie should not be accessible to scripting engines, + * like javascript. + * + * @return {@code true} if this cookie should be considered http only, {@code false} + * otherwise. + * @since 2.0 + */ + public boolean isHttpOnly() { + return false; + } + + /** + * Obtain a new instance of a {@link Cookie} with the same name, value, path, + * domain and version as this {@code NewCookie}. This method can be used to + * obtain an object that can be compared for equality with another {@code Cookie}; + * since a {@code Cookie} will never compare equal to a {@code NewCookie}. + * + * @return a {@link Cookie} + */ + public Cookie toCookie() { + return null; + } + + /** + * Convert the cookie to a string suitable for use as the value of the + * corresponding HTTP header. + * + * @return a stringified cookie. + */ + @Override + public String toString() { + return null; + } + + /** + * Generate a hash code by hashing all of the properties. + * + * @return the hash code. + */ + @Override + public int hashCode() { + return -1; + } + + /** + * Compare for equality. Use {@link #toCookie()} to compare a + * {@code NewCookie} to a {@code Cookie} considering only the common + * properties. + * + * @param obj the object to compare to + * @return true if the object is a {@code NewCookie} with the same value for + * all properties, false otherwise. + */ + @SuppressWarnings({"StringEquality", "RedundantIfStatement"}) + @Override + public boolean equals(Object obj) { + return true; + } +} \ No newline at end of file diff --git a/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/Cookie.java b/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/Cookie.java index d8348e13e2e..a93fec853e0 100644 --- a/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/Cookie.java +++ b/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/Cookie.java @@ -32,6 +32,7 @@ public class Cookie implements Cloneable { private String path; // ;Path=VALUE ... URLs that see the cookie private boolean secure; // ;Secure ... e.g. use SSL private int version = 0; // ;Version=1 ... means RFC 2109++ style + private boolean isHttpOnly = false; public Cookie(String name, String value) { this.name = name; @@ -81,4 +82,36 @@ public class Cookie implements Cloneable { } public void setVersion(int v) { } + + /** + * Marks or unmarks this Cookie as HttpOnly. + * + *

    If isHttpOnly is set to true, this cookie is + * marked as HttpOnly, by adding the HttpOnly attribute + * to it. + * + *

    HttpOnly cookies are not supposed to be exposed to + * client-side scripting code, and may therefore help mitigate certain + * kinds of cross-site scripting attacks. + * + * @param isHttpOnly true if this cookie is to be marked as + * HttpOnly, false otherwise + * + * @since Servlet 3.0 + */ + public void setHttpOnly(boolean isHttpOnly) { + this.isHttpOnly = isHttpOnly; + } + + /** + * Checks whether this Cookie has been marked as HttpOnly. + * + * @return true if this Cookie has been marked as HttpOnly, + * false otherwise + * + * @since Servlet 3.0 + */ + public boolean isHttpOnly() { + return isHttpOnly; + } } diff --git a/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/HttpServletResponse.java b/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/HttpServletResponse.java index 2971e023390..162ac0db3cc 100644 --- a/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/HttpServletResponse.java +++ b/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/HttpServletResponse.java @@ -24,6 +24,7 @@ package javax.servlet.http; import java.io.IOException; +import java.util.Collection; import javax.servlet.ServletResponse; public interface HttpServletResponse extends ServletResponse { @@ -44,6 +45,11 @@ public interface HttpServletResponse extends ServletResponse { public void addIntHeader(String name, int value); public void setStatus(int sc); public void setStatus(int sc, String sm); + public int getStatus(); + public String getHeader(String name); + public Collection getHeaders(String name); + public Collection getHeaderNames(); + public static final int SC_CONTINUE = 100; public static final int SC_SWITCHING_PROTOCOLS = 101; From b366ffa69e98e21ac8998bb08a8815fc4cae6b4f Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Wed, 3 Mar 2021 13:38:18 +0000 Subject: [PATCH 0009/1429] Revamp source of the query --- .../CWE-1004/SensitiveCookieNotHttpOnly.qhelp | 2 +- .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 116 +++++++++--------- .../SensitiveCookieNotHttpOnly.expected | 12 +- .../javax/ws/rs/core/Cookie.java | 59 +++------ .../javax/ws/rs/core/NewCookie.java | 36 ------ 5 files changed, 82 insertions(+), 143 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.qhelp b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.qhelp index 880ed767be9..ee3e8a4181a 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.qhelp @@ -2,7 +2,7 @@ -

    Cross-Site Scripting (XSS) is categorized as one of the OWASP Top 10 Security Vulnerabilities. The HttpOnly flag directs compatible browsers to prevent client-side script from accessing cookies. Including the HttpOnly flag in the Set-Cookie HTTP response header for a sensitive cookie helps mitigate the risk associated with XSS where an attacker's script code attempts to read the contents of a cookie and exfiltrate information obtained.

    +

    Cross-Site Scripting (XSS) is categorized as one of the OWASP Top 10 Security Vulnerabilities. The HttpOnly flag directs compatible browsers to prevent client-side script from accessing cookies. Including the HttpOnly flag in the Set-Cookie HTTP response header for a sensitive cookie helps mitigate the risk associated with XSS where an attacker's script code attempts to read the contents of a cookie and exfiltrate information obtained.

    diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index bf4e60134c9..842e8b7eabb 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -9,7 +9,6 @@ import java import semmle.code.java.frameworks.Servlets -import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking import DataFlow::PathGraph @@ -18,8 +17,8 @@ string getSensitiveCookieNameRegex() { result = "(?i).*(auth|session|token|key|c /** Holds if a string is concatenated with the name of a sensitive cookie. */ predicate isSensitiveCookieNameExpr(Expr expr) { - expr.(StringLiteral) - .getRepresentedString() + expr.(CompileTimeConstantExpr) + .getStringValue() .toLowerCase() .regexpMatch(getSensitiveCookieNameRegex()) or isSensitiveCookieNameExpr(expr.(AddExpr).getAnOperand()) @@ -27,7 +26,7 @@ predicate isSensitiveCookieNameExpr(Expr expr) { /** Holds if a string is concatenated with the `HttpOnly` flag. */ predicate hasHttpOnlyExpr(Expr expr) { - expr.(StringLiteral).getRepresentedString().toLowerCase().matches("%httponly%") or + expr.(CompileTimeConstantExpr).getStringValue().toLowerCase().matches("%httponly%") or hasHttpOnlyExpr(expr.(AddExpr).getAnOperand()) } @@ -38,37 +37,34 @@ class SetCookieMethodAccess extends MethodAccess { this.getMethod() instanceof ResponseAddHeaderMethod or this.getMethod() instanceof ResponseSetHeaderMethod ) and - this.getArgument(0).(StringLiteral).getRepresentedString().toLowerCase() = "set-cookie" + this.getArgument(0).(CompileTimeConstantExpr).getStringValue().toLowerCase() = "set-cookie" } } /** Sensitive cookie name used in a `Cookie` constructor or a `Set-Cookie` call. */ class SensitiveCookieNameExpr extends Expr { SensitiveCookieNameExpr() { - isSensitiveCookieNameExpr(this) and - ( - exists( - ClassInstanceExpr cie // new Cookie("jwt_token", token) - | - ( - cie.getConstructor().getDeclaringType().hasQualifiedName("javax.servlet.http", "Cookie") or - cie.getConstructor() - .getDeclaringType() - .getASupertype*() - .hasQualifiedName("javax.ws.rs.core", "Cookie") or - cie.getConstructor() - .getDeclaringType() - .getASupertype*() - .hasQualifiedName("jakarta.ws.rs.core", "Cookie") - ) and - DataFlow::localExprFlow(this, cie.getArgument(0)) - ) - or - exists( - SetCookieMethodAccess ma // response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure") - | - DataFlow::localExprFlow(this, ma.getArgument(1)) - ) + exists( + ClassInstanceExpr cie, Expr e // new Cookie("jwt_token", token) + | + ( + cie.getConstructor().getDeclaringType().hasQualifiedName("javax.servlet.http", "Cookie") or + cie.getConstructor() + .getDeclaringType() + .getASupertype*() + .hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "Cookie") + ) and + this = cie and + isSensitiveCookieNameExpr(e) and + DataFlow::localExprFlow(e, cie.getArgument(0)) + ) + or + exists( + SetCookieMethodAccess ma, Expr e // response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure") + | + this = ma.getArgument(1) and + isSensitiveCookieNameExpr(e) and + DataFlow::localExprFlow(e, ma.getArgument(1)) ) } } @@ -78,15 +74,20 @@ class CookieResponseSink extends DataFlow::ExprNode { CookieResponseSink() { exists(MethodAccess ma | ( - ma.getMethod() instanceof ResponseAddCookieMethod or - ma instanceof SetCookieMethodAccess - ) and - ma.getAnArgument() = this.getExpr() + ma.getMethod() instanceof ResponseAddCookieMethod and + this.getExpr() = ma.getArgument(0) + or + ma instanceof SetCookieMethodAccess and + this.getExpr() = ma.getArgument(1) + ) ) } } -/** Holds if the `node` is a method call of `setHttpOnly(true)` on a cookie. */ +/** + * Holds if `node` is an access to a variable which has `setHttpOnly(true)` called on it and is also + * the first argument to a call to the method `addCookie` of `javax.servlet.http.HttpServletResponse`. + */ predicate setHttpOnlyMethodAccess(DataFlow::Node node) { exists( MethodAccess addCookie, Variable cookie, MethodAccess m // jwtCookie.setHttpOnly(true) @@ -100,7 +101,10 @@ predicate setHttpOnlyMethodAccess(DataFlow::Node node) { ) } -/** Holds if the `node` is a method call of `Set-Cookie` header with the `HttpOnly` flag whose cookie name is sensitive. */ +/** + * Holds if `node` is a string that contains `httponly` and which flows to the second argument + * of a method to set a cookie. + */ predicate setHttpOnlyInSetCookie(DataFlow::Node node) { exists(SetCookieMethodAccess sa | hasHttpOnlyExpr(node.asExpr()) and @@ -108,20 +112,17 @@ predicate setHttpOnlyInSetCookie(DataFlow::Node node) { ) } -/** Holds if the `node` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */ -predicate setHttpOnlyInNewCookie(DataFlow::Node node) { - exists(ClassInstanceExpr cie | - cie.getConstructor().getDeclaringType().hasName("NewCookie") and - DataFlow::localExprFlow(node.asExpr(), cie.getArgument(0)) and - ( - cie.getNumArgument() = 6 and cie.getArgument(5).(BooleanLiteral).getBooleanValue() = true // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) - or - cie.getNumArgument() = 8 and - cie.getArgument(6).getType() instanceof BooleanType and - cie.getArgument(7).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) - or - cie.getNumArgument() = 10 and cie.getArgument(9).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) - ) +/** Holds if `cie` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */ +predicate setHttpOnlyInNewCookie(ClassInstanceExpr cie) { + cie.getConstructedType().hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and + ( + cie.getNumArgument() = 6 and cie.getArgument(5).(BooleanLiteral).getBooleanValue() = true // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) + or + cie.getNumArgument() = 8 and + cie.getArgument(6).getType() instanceof BooleanType and + cie.getArgument(7).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) + or + cie.getNumArgument() = 10 and cie.getArgument(9).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) ) } @@ -145,7 +146,10 @@ predicate isTestMethod(DataFlow::Node node) { ) } -/** A taint configuration tracking flow from a sensitive cookie without HttpOnly flag set to its HTTP response. */ +/** + * A taint configuration tracking flow from a sensitive cookie without the `HttpOnly` flag + * set to its HTTP response. + */ class MissingHttpOnlyConfiguration extends TaintTracking::Configuration { MissingHttpOnlyConfiguration() { this = "MissingHttpOnlyConfiguration" } @@ -159,25 +163,17 @@ class MissingHttpOnlyConfiguration extends TaintTracking::Configuration { // cookie.setHttpOnly(true) setHttpOnlyMethodAccess(node) or - // response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure") + // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure") setHttpOnlyInSetCookie(node) or // new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true) - setHttpOnlyInNewCookie(node) + setHttpOnlyInNewCookie(node.asExpr()) or // Test class or method isTestMethod(node) } override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { - exists( - ClassInstanceExpr cie // `NewCookie` constructor - | - cie.getAnArgument() = pred.asExpr() and - cie = succ.asExpr() and - cie.getConstructor().getDeclaringType().hasName("NewCookie") - ) - or exists( MethodAccess ma // `toString` call on a cookie object | diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected index 84d6be3863a..e2d05a9b24d 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected @@ -1,13 +1,13 @@ edges -| SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | -| SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | +| SensitiveCookieNotHttpOnly.java:22:27:22:60 | new Cookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | +| SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | nodes -| SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" : String | semmle.label | "jwt_token" : String | +| SensitiveCookieNotHttpOnly.java:22:27:22:60 | new Cookie(...) : Cookie | semmle.label | new Cookie(...) : Cookie | | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | semmle.label | jwtCookie | | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | semmle.label | ... + ... | +| SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) : NewCookie | semmle.label | new NewCookie(...) : NewCookie | | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | semmle.label | toString(...) | -| SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" : String | semmle.label | "session-access-key" : String | #select -| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:22:27:22:60 | new Cookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:22:27:22:60 | new Cookie(...) | This sensitive cookie | | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) | This sensitive cookie | diff --git a/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/Cookie.java b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/Cookie.java index 4e4c7585c35..f810600a766 100644 --- a/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/Cookie.java +++ b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/Cookie.java @@ -51,10 +51,16 @@ package javax.ws.rs.core; * @since 1.0 */ public class Cookie { + /** * Cookies using the default version correspond to RFC 2109. */ public static final int DEFAULT_VERSION = 1; + private final String name; + private final String value; + private final int version; + private final String path; + private final String domain; /** * Create a new instance. @@ -68,6 +74,11 @@ public class Cookie { */ public Cookie(final String name, final String value, final String path, final String domain, final int version) throws IllegalArgumentException { + this.name = name; + this.value = value; + this.version = version; + this.domain = domain; + this.path = path; } /** @@ -81,6 +92,7 @@ public class Cookie { */ public Cookie(final String name, final String value, final String path, final String domain) throws IllegalArgumentException { + this(name, value, path, domain, DEFAULT_VERSION); } /** @@ -92,6 +104,7 @@ public class Cookie { */ public Cookie(final String name, final String value) throws IllegalArgumentException { + this(name, value, null, null); } /** @@ -112,7 +125,7 @@ public class Cookie { * @return the cookie name. */ public String getName() { - return null; + return name; } /** @@ -121,7 +134,7 @@ public class Cookie { * @return the cookie value. */ public String getValue() { - return null; + return value; } /** @@ -130,7 +143,7 @@ public class Cookie { * @return the cookie version. */ public int getVersion() { - return -1; + return version; } /** @@ -139,7 +152,7 @@ public class Cookie { * @return the cookie domain. */ public String getDomain() { - return null; + return domain; } /** @@ -148,40 +161,6 @@ public class Cookie { * @return the cookie path. */ public String getPath() { - return null; + return path; } - - /** - * Convert the cookie to a string suitable for use as the value of the - * corresponding HTTP header. - * - * @return a stringified cookie. - */ - @Override - public String toString() { - return null; - } - - /** - * Generate a hash code by hashing all of the cookies properties. - * - * @return the cookie hash code. - */ - @Override - public int hashCode() { - return -1; - } - - /** - * Compare for equality. - * - * @param obj the object to compare to. - * @return {@code true}, if the object is a {@code Cookie} with the same - * value for all properties, {@code false} otherwise. - */ - @SuppressWarnings({"StringEquality", "RedundantIfStatement"}) - @Override - public boolean equals(final Object obj) { - return true; - } -} \ No newline at end of file +} diff --git a/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java index 43a4899e0ca..7f2e3ec0535 100644 --- a/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java +++ b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java @@ -320,40 +320,4 @@ public class NewCookie extends Cookie { public Cookie toCookie() { return null; } - - /** - * Convert the cookie to a string suitable for use as the value of the - * corresponding HTTP header. - * - * @return a stringified cookie. - */ - @Override - public String toString() { - return null; - } - - /** - * Generate a hash code by hashing all of the properties. - * - * @return the hash code. - */ - @Override - public int hashCode() { - return -1; - } - - /** - * Compare for equality. Use {@link #toCookie()} to compare a - * {@code NewCookie} to a {@code Cookie} considering only the common - * properties. - * - * @param obj the object to compare to - * @return true if the object is a {@code NewCookie} with the same value for - * all properties, false otherwise. - */ - @SuppressWarnings({"StringEquality", "RedundantIfStatement"}) - @Override - public boolean equals(Object obj) { - return true; - } } \ No newline at end of file From 1b1c3f953b9ebb828e161e4b36d67075a7bca510 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Wed, 3 Mar 2021 13:54:26 +0000 Subject: [PATCH 0010/1429] Remove localflow from the source --- .../CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index 842e8b7eabb..258b2545b27 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -45,7 +45,7 @@ class SetCookieMethodAccess extends MethodAccess { class SensitiveCookieNameExpr extends Expr { SensitiveCookieNameExpr() { exists( - ClassInstanceExpr cie, Expr e // new Cookie("jwt_token", token) + ClassInstanceExpr cie // new Cookie("jwt_token", token) | ( cie.getConstructor().getDeclaringType().hasQualifiedName("javax.servlet.http", "Cookie") or @@ -55,16 +55,14 @@ class SensitiveCookieNameExpr extends Expr { .hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "Cookie") ) and this = cie and - isSensitiveCookieNameExpr(e) and - DataFlow::localExprFlow(e, cie.getArgument(0)) + isSensitiveCookieNameExpr(cie.getArgument(0)) ) or exists( - SetCookieMethodAccess ma, Expr e // response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure") + SetCookieMethodAccess ma // response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure") | this = ma.getArgument(1) and - isSensitiveCookieNameExpr(e) and - DataFlow::localExprFlow(e, ma.getArgument(1)) + isSensitiveCookieNameExpr(this) ) } } From 502cf38fccef4c0b2a1120f2fe175571ec1b9cb5 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Wed, 3 Mar 2021 14:07:43 +0000 Subject: [PATCH 0011/1429] Use concise API --- .../Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index 258b2545b27..d9104cbad97 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -48,9 +48,8 @@ class SensitiveCookieNameExpr extends Expr { ClassInstanceExpr cie // new Cookie("jwt_token", token) | ( - cie.getConstructor().getDeclaringType().hasQualifiedName("javax.servlet.http", "Cookie") or - cie.getConstructor() - .getDeclaringType() + cie.getConstructedType().hasQualifiedName("javax.servlet.http", "Cookie") or + cie.getConstructedType() .getASupertype*() .hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "Cookie") ) and From 86dde6eab16f9f28cc6bb68c8b2ed7fa8c40e3bd Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Wed, 17 Feb 2021 11:34:05 +0100 Subject: [PATCH 0012/1429] Python: start of port --- .../src/Security/CWE-327/InsecureProtocol.ql | 299 +++++++++++++++++- .../examples/secure_default_protocol.py | 13 + .../CWE-327/examples/secure_protocol.py | 11 + 3 files changed, 307 insertions(+), 16 deletions(-) create mode 100644 python/ql/src/Security/CWE-327/examples/secure_default_protocol.py create mode 100644 python/ql/src/Security/CWE-327/examples/secure_protocol.py diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index d1ae714b6be..6f2feedce22 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -10,7 +10,270 @@ */ import python +import semmle.python.ApiGraphs +// The idea is to track flow from the creation of an insecure context to a use +// such as `wrap_socket`. There should be a data-flow path for each insecure version +// and each path should have a version specific sanitizer. This will allow fluent api +// style code to block the paths one by one. +// +// class InsecureContextCreation extends DataFlow::CfgNode { +// override CallNode node; +// InsecureContextCreation() { +// this = API::moduleImport("ssl").getMember("SSLContext").getACall() and +// insecure_version().asCfgNode() in [node.getArg(0), node.getArgByName("protocol")] +// } +// } +// class InsecureSSLContextCreation extends DataFlow::CfgNode { +// override CallNode node; +// InsecureSSLContextCreation() { +// this = API::moduleImport("ssl").getMember("create_default_context").getACall() +// or +// this = API::moduleImport("ssl").getMember("SSLContext").getACall() and +// API::moduleImport("ssl").getMember("PROTOCOL_TLS").getAUse().asCfgNode() in [ +// node.getArg(0), node.getArgByName("protocol") +// ] +// } +// } +abstract class ContextCreation extends DataFlow::CfgNode { + abstract DataFlow::CfgNode getProtocol(); +} + +class SSLContextCreation extends ContextCreation { + override CallNode node; + + SSLContextCreation() { this = API::moduleImport("ssl").getMember("SSLContext").getACall() } + + override DataFlow::CfgNode getProtocol() { + result.getNode() in [node.getArg(0), node.getArgByName("protocol")] + } +} + +class PyOpenSSLContextCreation extends ContextCreation { + override CallNode node; + + PyOpenSSLContextCreation() { + this = API::moduleImport("pyOpenSSL").getMember("SSL").getMember("Context").getACall() + } + + override DataFlow::CfgNode getProtocol() { + result.getNode() in [node.getArg(0), node.getArgByName("method")] + } +} + +abstract class ConnectionCreation extends DataFlow::CfgNode { + abstract DataFlow::CfgNode getContext(); +} + +class WrapSocketCall extends ConnectionCreation { + override CallNode node; + + WrapSocketCall() { node.getFunction().(AttrNode).getName() = "wrap_socket" } + + override DataFlow::CfgNode getContext() { + result.getNode() = node.getFunction().(AttrNode).getObject() + } +} + +class ConnectionCall extends ConnectionCreation { + override CallNode node; + + ConnectionCall() { + this = API::moduleImport("pyOpenSSL").getMember("SSL").getMember("Connection").getACall() + } + + override DataFlow::CfgNode getContext() { + result.getNode() in [node.getArg(0), node.getArgByName("context")] + } +} + +abstract class TlsLibrary extends string { + TlsLibrary() { this in ["ssl"] } + + abstract string specific_insecure_version_name(); + + abstract string unspecific_version_name(); + + abstract API::Node version_constants(); + + DataFlow::Node insecure_version() { + result = version_constants().getMember(specific_insecure_version_name()).getAUse() + } + + DataFlow::Node unspecific_version() { + result = version_constants().getMember(unspecific_version_name()).getAUse() + } + + abstract DataFlow::CfgNode default_context_creation(); + + abstract ContextCreation specific_context_creation(); + + ContextCreation insecure_context_creation() { + result = specific_context_creation() and + result.getProtocol() = insecure_version() + } + + DataFlow::CfgNode unspecific_context_creation() { + result = default_context_creation() + or + result = specific_context_creation() and + result.(ContextCreation).getProtocol() = unspecific_version() + } + + abstract ConnectionCreation connection_creation(); +} + +class Ssl extends TlsLibrary { + Ssl() { this = "ssl" } + + override string specific_insecure_version_name() { + result in [ + "PROTOCOL_SSLv2", "PROTOCOL_SSLv3", "PROTOCOL_SSLv23", "PROTOCOL_TLSv1", "PROTOCOL_TLSv1_1" + ] + } + + override string unspecific_version_name() { result = "PROTOCOL_TLS" } + + override API::Node version_constants() { result = API::moduleImport("ssl") } + + override DataFlow::CfgNode default_context_creation() { + result = API::moduleImport("ssl").getMember("create_default_context").getACall() + } + + override ContextCreation specific_context_creation() { result instanceof SSLContextCreation } + + override ConnectionCreation connection_creation() { result instanceof WrapSocketCall } +} + +class PyOpenSSL extends TlsLibrary { + PyOpenSSL() { this = "pyOpenSSL" } + + override string specific_insecure_version_name() { + result in ["SSLv2_METHOD", "SSLv23_METHOD", "SSLv3_METHOD", "TLSv1_METHOD", "TLSv1_1_METHOD"] + } + + override string unspecific_version_name() { result = "TLS_METHOD" } + + override API::Node version_constants() { + result = API::moduleImport("pyOpenSSL").getMember("SSL") + } + + override DataFlow::CfgNode default_context_creation() { none() } + + override ContextCreation specific_context_creation() { + result instanceof PyOpenSSLContextCreation + } + + override ConnectionCreation connection_creation() { result instanceof ConnectionCall } +} + +module ssl { + string insecure_version_name() { + result = "PROTOCOL_SSLv2" or + result = "PROTOCOL_SSLv3" or + result = "PROTOCOL_SSLv23" or + result = "PROTOCOL_TLSv1" or + result = "PROTOCOL_TLSv1_1" + } + + DataFlow::Node insecure_version() { + result = API::moduleImport("ssl").getMember(insecure_version_name()).getAUse() + } +} + +module pyOpenSSL { + string insecure_version_name() { + result = "SSLv2_METHOD" or + result = "SSLv23_METHOD" or + result = "SSLv3_METHOD" or + result = "TLSv1_METHOD" or + result = "TLSv1_1_METHOD" + } + + DataFlow::Node insecure_version() { + result = + API::moduleImport("pyOpenSSL").getMember("SSL").getMember(insecure_version_name()).getAUse() + } +} + +class InsecureContextConfiguration extends DataFlow::Configuration { + TlsLibrary library; + + InsecureContextConfiguration() { this = library + ["AllowsTLSv1", "AllowsTLSv1_1"] } + + override predicate isSource(DataFlow::Node source) { + source = library.unspecific_context_creation() + } + + override predicate isSink(DataFlow::Node sink) { + sink = library.connection_creation().getContext() + } + + abstract string flag(); + + override predicate isBarrierOut(DataFlow::Node node) { + exists(AugAssign aa, AttrNode attr | + aa.getOperation().getOp() instanceof BitOr and + aa.getTarget() = attr.getNode() and + attr.getName() = "options" and + attr.getObject() = node.asCfgNode() and + aa.getValue() = API::moduleImport("ssl").getMember(flag()).getAUse().asExpr() + ) + } +} + +class AllowsTLSv1 extends InsecureContextConfiguration { + AllowsTLSv1() { this = library + "AllowsTLSv1" } + + override string flag() { result = "OP_NO_TLSv1" } +} + +class AllowsTLSv1_1 extends InsecureContextConfiguration { + AllowsTLSv1_1() { this = library + "AllowsTLSv1_1" } + + override string flag() { result = "OP_NO_TLSv1_2" } +} + +predicate unsafe_connection_creation(DataFlow::Node node) { + exists(AllowsTLSv1 c | c.hasFlowTo(node)) or + exists(AllowsTLSv1_1 c | c.hasFlowTo(node)) //or + // node = API::moduleImport("ssl").getMember("wrap_socket").getACall() +} + +predicate unsafe_context_creation(DataFlow::Node node) { + exists(TlsLibrary l | l.insecure_context_creation() = node) +} + +// class InsecureTLSContextConfiguration extends DataFlow::Configuration { +// InsecureTLSContextConfiguration() { this in ["AllowsTLSv1", "AllowsTLSv1_1"] } +// override predicate isSource(DataFlow::Node source) { +// source instanceof InsecureSSLContextCreation +// } +// override predicate isSink(DataFlow::Node sink) { sink = any(WrapSocketCall c).getContext() } +// abstract string flag(); +// override predicate isBarrierOut(DataFlow::Node node) { +// exists(AugAssign aa, AttrNode attr | +// aa.getOperation().getOp() instanceof BitOr and +// aa.getTarget() = attr.getNode() and +// attr.getName() = "options" and +// attr.getObject() = node.asCfgNode() and +// aa.getValue() = API::moduleImport("ssl").getMember(flag()).getAUse().asExpr() +// ) +// } +// } +// class AllowsTLSv1 extends InsecureTLSContextConfiguration { +// AllowsTLSv1() { this = "AllowsTLSv1" } +// override string flag() { result = "OP_NO_TLSv1" } +// } +// class AllowsTLSv1_1 extends InsecureTLSContextConfiguration { +// AllowsTLSv1_1() { this = "AllowsTLSv1_1" } +// override string flag() { result = "OP_NO_TLSv1_1" } +// } +// predicate unsafe_wrap_socket_call(DataFlow::Node node) { +// exists(AllowsTLSv1 c | c.hasFlowTo(node)) or +// exists(AllowsTLSv1_1 c | c.hasFlowTo(node)) or +// node = API::moduleImport("ssl").getMember("wrap_socket").getACall() +// } private ModuleValue the_ssl_module() { result = Module::named("ssl") } FunctionValue ssl_wrap_socket() { result = the_ssl_module().attr("wrap_socket") } @@ -21,20 +284,24 @@ private ModuleValue the_pyOpenSSL_module() { result = Value::named("pyOpenSSL.SS ClassValue the_pyOpenSSL_Context_class() { result = Value::named("pyOpenSSL.SSL.Context") } -string insecure_version_name() { - // For `pyOpenSSL.SSL` - result = "SSLv2_METHOD" or - result = "SSLv23_METHOD" or - result = "SSLv3_METHOD" or - result = "TLSv1_METHOD" or - // For the `ssl` module - result = "PROTOCOL_SSLv2" or - result = "PROTOCOL_SSLv3" or - result = "PROTOCOL_SSLv23" or - result = "PROTOCOL_TLS" or - result = "PROTOCOL_TLSv1" -} - +// Since version 3.6, it is fine to call `ssl.SSLContext(protocol=PROTOCOL_TLS)` +// if one also specifies either OP_NO_TLSv1 (introduced in 3.2) +// or SSLContext.minimum_version other than TLSVersion.TLSv1 (introduced in 3.7) +// See https://docs.python.org/3/library/ssl.html?highlight=ssl#ssl.SSLContext +// and https://docs.python.org/3/library/ssl.html?highlight=ssl#protocol-versions +// FP reported here: https://github.com/github/codeql/issues/2554 +// string insecure_version_name() { +// // For `pyOpenSSL.SSL` +// result = "SSLv2_METHOD" or +// result = "SSLv23_METHOD" or +// result = "SSLv3_METHOD" or +// result = "TLSv1_METHOD" or +// // For the `ssl` module +// result = "PROTOCOL_SSLv2" or +// result = "PROTOCOL_SSLv3" or +// result = "PROTOCOL_SSLv23" or +// result = "PROTOCOL_TLSv1" +// } /* * A syntactic check for cases where points-to analysis cannot infer the presence of * a protocol constant, e.g. if it has been removed in later versions of the `ssl` @@ -71,7 +338,7 @@ predicate unsafe_ssl_wrap_socket_call( named_argument = "protocol" and method_name = "ssl.SSLContext" ) and - insecure_version = insecure_version_name() and + insecure_version = ssl::insecure_version_name() and ( call.getArgByName(named_argument).pointsTo(the_ssl_module().attr(insecure_version)) or @@ -81,7 +348,7 @@ predicate unsafe_ssl_wrap_socket_call( predicate unsafe_pyOpenSSL_Context_call(CallNode call, string insecure_version) { call = the_pyOpenSSL_Context_class().getACall() and - insecure_version = insecure_version_name() and + insecure_version = pyOpenSSL::insecure_version_name() and call.getArg(0).pointsTo(the_pyOpenSSL_module().attr(insecure_version)) } diff --git a/python/ql/src/Security/CWE-327/examples/secure_default_protocol.py b/python/ql/src/Security/CWE-327/examples/secure_default_protocol.py new file mode 100644 index 00000000000..83c6dbbba0e --- /dev/null +++ b/python/ql/src/Security/CWE-327/examples/secure_default_protocol.py @@ -0,0 +1,13 @@ +# taken from https://docs.python.org/3/library/ssl.html?highlight=ssl#ssl.SSLContext + +import socket +import ssl + +hostname = 'www.python.org' +context = ssl.create_default_context() +context.options |= ssl.OP_NO_TLSv1 # This added by me +context.options |= ssl.OP_NO_TLSv1_1 # This added by me + +with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) diff --git a/python/ql/src/Security/CWE-327/examples/secure_protocol.py b/python/ql/src/Security/CWE-327/examples/secure_protocol.py new file mode 100644 index 00000000000..94b3557d017 --- /dev/null +++ b/python/ql/src/Security/CWE-327/examples/secure_protocol.py @@ -0,0 +1,11 @@ +import socket +import ssl + +hostname = 'www.python.org' +context = ssl.SSLContext(ssl.PROTOCOL_TLS) +context.options |= ssl.OP_NO_TLSv1 +context.options |= ssl.OP_NO_TLSv1_1 # This added by me + +with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) From 72b37a5b1bb344dcdd9b4e2d1172c540df678408 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Wed, 17 Feb 2021 13:21:33 +0100 Subject: [PATCH 0013/1429] Python: factor out barrier --- .../src/Security/CWE-327/InsecureProtocol.ql | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index 6f2feedce22..6b5b2e75d22 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -87,6 +87,46 @@ class ConnectionCall extends ConnectionCreation { } } +class ProtocolRestriction extends DataFlow::CfgNode { + abstract DataFlow::CfgNode getContext(); + + abstract string getRestriction(); +} + +class OptionsAugOr extends ProtocolRestriction { + string restriction; + + OptionsAugOr() { + exists(AugAssign aa, AttrNode attr | + aa.getOperation().getOp() instanceof BitOr and + aa.getTarget() = attr.getNode() and + attr.getName() = "options" and + attr.getObject() = node and + aa.getValue() = API::moduleImport("ssl").getMember(restriction).getAUse().asExpr() + ) + } + + override DataFlow::CfgNode getContext() { result = this } + + override string getRestriction() { result = restriction } +} + +class SetOptionsCall extends ProtocolRestriction { + override CallNode node; + + SetOptionsCall() { node.getFunction().(AttrNode).getName() = "set_options" } + + override DataFlow::CfgNode getContext() { + result.getNode() = node.getFunction().(AttrNode).getObject() + } + + override string getRestriction() { + API::moduleImport("PyOpenSSL").getMember("SSL").getMember(result).getAUse().asCfgNode() in [ + node.getArg(0), node.getArgByName("options") + ] + } +} + abstract class TlsLibrary extends string { TlsLibrary() { this in ["ssl"] } @@ -121,6 +161,8 @@ abstract class TlsLibrary extends string { } abstract ConnectionCreation connection_creation(); + + abstract ProtocolRestriction protocol_restriction(); } class Ssl extends TlsLibrary { @@ -143,6 +185,8 @@ class Ssl extends TlsLibrary { override ContextCreation specific_context_creation() { result instanceof SSLContextCreation } override ConnectionCreation connection_creation() { result instanceof WrapSocketCall } + + override ProtocolRestriction protocol_restriction() { result instanceof OptionsAugOr } } class PyOpenSSL extends TlsLibrary { @@ -165,6 +209,8 @@ class PyOpenSSL extends TlsLibrary { } override ConnectionCreation connection_creation() { result instanceof ConnectionCall } + + override ProtocolRestriction protocol_restriction() { result instanceof OptionsAugOr } } module ssl { @@ -212,12 +258,10 @@ class InsecureContextConfiguration extends DataFlow::Configuration { abstract string flag(); override predicate isBarrierOut(DataFlow::Node node) { - exists(AugAssign aa, AttrNode attr | - aa.getOperation().getOp() instanceof BitOr and - aa.getTarget() = attr.getNode() and - attr.getName() = "options" and - attr.getObject() = node.asCfgNode() and - aa.getValue() = API::moduleImport("ssl").getMember(flag()).getAUse().asExpr() + exists(ProtocolRestriction r | + r = library.protocol_restriction() and + node = r.getContext() and + r.getRestriction() = flag() ) } } @@ -231,7 +275,7 @@ class AllowsTLSv1 extends InsecureContextConfiguration { class AllowsTLSv1_1 extends InsecureContextConfiguration { AllowsTLSv1_1() { this = library + "AllowsTLSv1_1" } - override string flag() { result = "OP_NO_TLSv1_2" } + override string flag() { result = "OP_NO_TLSv1_1" } } predicate unsafe_connection_creation(DataFlow::Node node) { From 7ed018aff6c9f79a3ea60ef8ae4d2f2067b81442 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Wed, 17 Feb 2021 14:30:28 +0100 Subject: [PATCH 0014/1429] Python: refactor into modules and turn on the pyOpenSSL module --- .../src/Security/CWE-327/InsecureProtocol.ql | 429 +++++++----------- 1 file changed, 160 insertions(+), 269 deletions(-) diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index 6b5b2e75d22..81b3558907e 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -39,96 +39,18 @@ abstract class ContextCreation extends DataFlow::CfgNode { abstract DataFlow::CfgNode getProtocol(); } -class SSLContextCreation extends ContextCreation { - override CallNode node; - - SSLContextCreation() { this = API::moduleImport("ssl").getMember("SSLContext").getACall() } - - override DataFlow::CfgNode getProtocol() { - result.getNode() in [node.getArg(0), node.getArgByName("protocol")] - } -} - -class PyOpenSSLContextCreation extends ContextCreation { - override CallNode node; - - PyOpenSSLContextCreation() { - this = API::moduleImport("pyOpenSSL").getMember("SSL").getMember("Context").getACall() - } - - override DataFlow::CfgNode getProtocol() { - result.getNode() in [node.getArg(0), node.getArgByName("method")] - } -} - abstract class ConnectionCreation extends DataFlow::CfgNode { abstract DataFlow::CfgNode getContext(); } -class WrapSocketCall extends ConnectionCreation { - override CallNode node; - - WrapSocketCall() { node.getFunction().(AttrNode).getName() = "wrap_socket" } - - override DataFlow::CfgNode getContext() { - result.getNode() = node.getFunction().(AttrNode).getObject() - } -} - -class ConnectionCall extends ConnectionCreation { - override CallNode node; - - ConnectionCall() { - this = API::moduleImport("pyOpenSSL").getMember("SSL").getMember("Connection").getACall() - } - - override DataFlow::CfgNode getContext() { - result.getNode() in [node.getArg(0), node.getArgByName("context")] - } -} - class ProtocolRestriction extends DataFlow::CfgNode { abstract DataFlow::CfgNode getContext(); abstract string getRestriction(); } -class OptionsAugOr extends ProtocolRestriction { - string restriction; - - OptionsAugOr() { - exists(AugAssign aa, AttrNode attr | - aa.getOperation().getOp() instanceof BitOr and - aa.getTarget() = attr.getNode() and - attr.getName() = "options" and - attr.getObject() = node and - aa.getValue() = API::moduleImport("ssl").getMember(restriction).getAUse().asExpr() - ) - } - - override DataFlow::CfgNode getContext() { result = this } - - override string getRestriction() { result = restriction } -} - -class SetOptionsCall extends ProtocolRestriction { - override CallNode node; - - SetOptionsCall() { node.getFunction().(AttrNode).getName() = "set_options" } - - override DataFlow::CfgNode getContext() { - result.getNode() = node.getFunction().(AttrNode).getObject() - } - - override string getRestriction() { - API::moduleImport("PyOpenSSL").getMember("SSL").getMember(result).getAUse().asCfgNode() in [ - node.getArg(0), node.getArgByName("options") - ] - } -} - abstract class TlsLibrary extends string { - TlsLibrary() { this in ["ssl"] } + TlsLibrary() { this in ["ssl", "pyOpenSSL"] } abstract string specific_insecure_version_name(); @@ -160,85 +82,149 @@ abstract class TlsLibrary extends string { result.(ContextCreation).getProtocol() = unspecific_version() } + /** A connection is created in an outright insecure manner. */ + abstract DataFlow::CfgNode insecure_connection_creation(); + + /** A connection is created from a context. */ abstract ConnectionCreation connection_creation(); abstract ProtocolRestriction protocol_restriction(); } -class Ssl extends TlsLibrary { - Ssl() { this = "ssl" } - - override string specific_insecure_version_name() { - result in [ - "PROTOCOL_SSLv2", "PROTOCOL_SSLv3", "PROTOCOL_SSLv23", "PROTOCOL_TLSv1", "PROTOCOL_TLSv1_1" - ] - } - - override string unspecific_version_name() { result = "PROTOCOL_TLS" } - - override API::Node version_constants() { result = API::moduleImport("ssl") } - - override DataFlow::CfgNode default_context_creation() { - result = API::moduleImport("ssl").getMember("create_default_context").getACall() - } - - override ContextCreation specific_context_creation() { result instanceof SSLContextCreation } - - override ConnectionCreation connection_creation() { result instanceof WrapSocketCall } - - override ProtocolRestriction protocol_restriction() { result instanceof OptionsAugOr } -} - -class PyOpenSSL extends TlsLibrary { - PyOpenSSL() { this = "pyOpenSSL" } - - override string specific_insecure_version_name() { - result in ["SSLv2_METHOD", "SSLv23_METHOD", "SSLv3_METHOD", "TLSv1_METHOD", "TLSv1_1_METHOD"] - } - - override string unspecific_version_name() { result = "TLS_METHOD" } - - override API::Node version_constants() { - result = API::moduleImport("pyOpenSSL").getMember("SSL") - } - - override DataFlow::CfgNode default_context_creation() { none() } - - override ContextCreation specific_context_creation() { - result instanceof PyOpenSSLContextCreation - } - - override ConnectionCreation connection_creation() { result instanceof ConnectionCall } - - override ProtocolRestriction protocol_restriction() { result instanceof OptionsAugOr } -} - module ssl { - string insecure_version_name() { - result = "PROTOCOL_SSLv2" or - result = "PROTOCOL_SSLv3" or - result = "PROTOCOL_SSLv23" or - result = "PROTOCOL_TLSv1" or - result = "PROTOCOL_TLSv1_1" + class SSLContextCreation extends ContextCreation { + override CallNode node; + + SSLContextCreation() { this = API::moduleImport("ssl").getMember("SSLContext").getACall() } + + override DataFlow::CfgNode getProtocol() { + result.getNode() in [node.getArg(0), node.getArgByName("protocol")] + } } - DataFlow::Node insecure_version() { - result = API::moduleImport("ssl").getMember(insecure_version_name()).getAUse() + class WrapSocketCall extends ConnectionCreation { + override CallNode node; + + WrapSocketCall() { node.getFunction().(AttrNode).getName() = "wrap_socket" } + + override DataFlow::CfgNode getContext() { + result.getNode() = node.getFunction().(AttrNode).getObject() + } + } + + class OptionsAugOr extends ProtocolRestriction { + string restriction; + + OptionsAugOr() { + exists(AugAssign aa, AttrNode attr | + aa.getOperation().getOp() instanceof BitOr and + aa.getTarget() = attr.getNode() and + attr.getName() = "options" and + attr.getObject() = node and + aa.getValue() = API::moduleImport("ssl").getMember(restriction).getAUse().asExpr() + ) + } + + override DataFlow::CfgNode getContext() { result = this } + + override string getRestriction() { result = restriction } + } + + class Ssl extends TlsLibrary { + Ssl() { this = "ssl" } + + override string specific_insecure_version_name() { + result in [ + "PROTOCOL_SSLv2", "PROTOCOL_SSLv3", "PROTOCOL_SSLv23", "PROTOCOL_TLSv1", + "PROTOCOL_TLSv1_1" + ] + } + + override string unspecific_version_name() { result = "PROTOCOL_TLS" } + + override API::Node version_constants() { result = API::moduleImport("ssl") } + + override DataFlow::CfgNode default_context_creation() { + result = API::moduleImport("ssl").getMember("create_default_context").getACall() + } + + override ContextCreation specific_context_creation() { result instanceof SSLContextCreation } + + override DataFlow::CfgNode insecure_connection_creation() { + result = API::moduleImport("ssl").getMember("wrap_socket").getACall() + } + + override ConnectionCreation connection_creation() { result instanceof WrapSocketCall } + + override ProtocolRestriction protocol_restriction() { result instanceof OptionsAugOr } } } module pyOpenSSL { - string insecure_version_name() { - result = "SSLv2_METHOD" or - result = "SSLv23_METHOD" or - result = "SSLv3_METHOD" or - result = "TLSv1_METHOD" or - result = "TLSv1_1_METHOD" + class PyOpenSSLContextCreation extends ContextCreation { + override CallNode node; + + PyOpenSSLContextCreation() { + this = API::moduleImport("pyOpenSSL").getMember("SSL").getMember("Context").getACall() + } + + override DataFlow::CfgNode getProtocol() { + result.getNode() in [node.getArg(0), node.getArgByName("method")] + } } - DataFlow::Node insecure_version() { - result = - API::moduleImport("pyOpenSSL").getMember("SSL").getMember(insecure_version_name()).getAUse() + class ConnectionCall extends ConnectionCreation { + override CallNode node; + + ConnectionCall() { + this = API::moduleImport("pyOpenSSL").getMember("SSL").getMember("Connection").getACall() + } + + override DataFlow::CfgNode getContext() { + result.getNode() in [node.getArg(0), node.getArgByName("context")] + } + } + + class SetOptionsCall extends ProtocolRestriction { + override CallNode node; + + SetOptionsCall() { node.getFunction().(AttrNode).getName() = "set_options" } + + override DataFlow::CfgNode getContext() { + result.getNode() = node.getFunction().(AttrNode).getObject() + } + + override string getRestriction() { + API::moduleImport("PyOpenSSL").getMember("SSL").getMember(result).getAUse().asCfgNode() in [ + node.getArg(0), node.getArgByName("options") + ] + } + } + + class PyOpenSSL extends TlsLibrary { + PyOpenSSL() { this = "pyOpenSSL" } + + override string specific_insecure_version_name() { + result in ["SSLv2_METHOD", "SSLv23_METHOD", "SSLv3_METHOD", "TLSv1_METHOD", "TLSv1_1_METHOD"] + } + + override string unspecific_version_name() { result = "TLS_METHOD" } + + override API::Node version_constants() { + result = API::moduleImport("pyOpenSSL").getMember("SSL") + } + + override DataFlow::CfgNode default_context_creation() { none() } + + override ContextCreation specific_context_creation() { + result instanceof PyOpenSSLContextCreation + } + + override DataFlow::CfgNode insecure_connection_creation() { none() } + + override ConnectionCreation connection_creation() { result instanceof ConnectionCall } + + override ProtocolRestriction protocol_restriction() { result instanceof SetOptionsCall } } } @@ -278,129 +264,34 @@ class AllowsTLSv1_1 extends InsecureContextConfiguration { override string flag() { result = "OP_NO_TLSv1_1" } } -predicate unsafe_connection_creation(DataFlow::Node node) { - exists(AllowsTLSv1 c | c.hasFlowTo(node)) or - exists(AllowsTLSv1_1 c | c.hasFlowTo(node)) //or - // node = API::moduleImport("ssl").getMember("wrap_socket").getACall() -} - -predicate unsafe_context_creation(DataFlow::Node node) { - exists(TlsLibrary l | l.insecure_context_creation() = node) -} - -// class InsecureTLSContextConfiguration extends DataFlow::Configuration { -// InsecureTLSContextConfiguration() { this in ["AllowsTLSv1", "AllowsTLSv1_1"] } -// override predicate isSource(DataFlow::Node source) { -// source instanceof InsecureSSLContextCreation -// } -// override predicate isSink(DataFlow::Node sink) { sink = any(WrapSocketCall c).getContext() } -// abstract string flag(); -// override predicate isBarrierOut(DataFlow::Node node) { -// exists(AugAssign aa, AttrNode attr | -// aa.getOperation().getOp() instanceof BitOr and -// aa.getTarget() = attr.getNode() and -// attr.getName() = "options" and -// attr.getObject() = node.asCfgNode() and -// aa.getValue() = API::moduleImport("ssl").getMember(flag()).getAUse().asExpr() -// ) -// } -// } -// class AllowsTLSv1 extends InsecureTLSContextConfiguration { -// AllowsTLSv1() { this = "AllowsTLSv1" } -// override string flag() { result = "OP_NO_TLSv1" } -// } -// class AllowsTLSv1_1 extends InsecureTLSContextConfiguration { -// AllowsTLSv1_1() { this = "AllowsTLSv1_1" } -// override string flag() { result = "OP_NO_TLSv1_1" } -// } -// predicate unsafe_wrap_socket_call(DataFlow::Node node) { -// exists(AllowsTLSv1 c | c.hasFlowTo(node)) or -// exists(AllowsTLSv1_1 c | c.hasFlowTo(node)) or -// node = API::moduleImport("ssl").getMember("wrap_socket").getACall() -// } -private ModuleValue the_ssl_module() { result = Module::named("ssl") } - -FunctionValue ssl_wrap_socket() { result = the_ssl_module().attr("wrap_socket") } - -ClassValue ssl_Context_class() { result = the_ssl_module().attr("SSLContext") } - -private ModuleValue the_pyOpenSSL_module() { result = Value::named("pyOpenSSL.SSL") } - -ClassValue the_pyOpenSSL_Context_class() { result = Value::named("pyOpenSSL.SSL.Context") } - -// Since version 3.6, it is fine to call `ssl.SSLContext(protocol=PROTOCOL_TLS)` -// if one also specifies either OP_NO_TLSv1 (introduced in 3.2) -// or SSLContext.minimum_version other than TLSVersion.TLSv1 (introduced in 3.7) -// See https://docs.python.org/3/library/ssl.html?highlight=ssl#ssl.SSLContext -// and https://docs.python.org/3/library/ssl.html?highlight=ssl#protocol-versions -// FP reported here: https://github.com/github/codeql/issues/2554 -// string insecure_version_name() { -// // For `pyOpenSSL.SSL` -// result = "SSLv2_METHOD" or -// result = "SSLv23_METHOD" or -// result = "SSLv3_METHOD" or -// result = "TLSv1_METHOD" or -// // For the `ssl` module -// result = "PROTOCOL_SSLv2" or -// result = "PROTOCOL_SSLv3" or -// result = "PROTOCOL_SSLv23" or -// result = "PROTOCOL_TLSv1" -// } -/* - * A syntactic check for cases where points-to analysis cannot infer the presence of - * a protocol constant, e.g. if it has been removed in later versions of the `ssl` - * library. - */ - -bindingset[named_argument] -predicate probable_insecure_ssl_constant( - CallNode call, string insecure_version, string named_argument -) { - exists(ControlFlowNode arg | - arg = call.getArgByName(named_argument) or - arg = call.getArg(0) - | - arg.(AttrNode).getObject(insecure_version).pointsTo(the_ssl_module()) - or - arg.(NameNode).getId() = insecure_version and - exists(Import imp | - imp.getAnImportedModuleName() = "ssl" and - imp.getAName().getAsname().(Name).getId() = insecure_version - ) - ) -} - -predicate unsafe_ssl_wrap_socket_call( - CallNode call, string method_name, string insecure_version, string named_argument -) { - ( - call = ssl_wrap_socket().getACall() and - method_name = "deprecated method ssl.wrap_socket" and - named_argument = "ssl_version" - or - call = ssl_Context_class().getACall() and - named_argument = "protocol" and - method_name = "ssl.SSLContext" - ) and - insecure_version = ssl::insecure_version_name() and - ( - call.getArgByName(named_argument).pointsTo(the_ssl_module().attr(insecure_version)) - or - probable_insecure_ssl_constant(call, insecure_version, named_argument) - ) -} - -predicate unsafe_pyOpenSSL_Context_call(CallNode call, string insecure_version) { - call = the_pyOpenSSL_Context_class().getACall() and - insecure_version = pyOpenSSL::insecure_version_name() and - call.getArg(0).pointsTo(the_pyOpenSSL_module().attr(insecure_version)) -} - -from CallNode call, string method_name, string insecure_version -where - unsafe_ssl_wrap_socket_call(call, method_name, insecure_version, _) +predicate unsafe_connection_creation(DataFlow::Node node, string insecure_version) { + exists(AllowsTLSv1 c | c.hasFlowTo(node)) and + insecure_version = "TLSv1" or - unsafe_pyOpenSSL_Context_call(call, insecure_version) and method_name = "pyOpenSSL.SSL.Context" -select call, - "Insecure SSL/TLS protocol version " + insecure_version + " specified in call to " + method_name + - "." + exists(AllowsTLSv1_1 c | c.hasFlowTo(node)) and + insecure_version = "TLSv1" + or + exists(TlsLibrary l | l.insecure_connection_creation() = node) and + insecure_version = "[multiple]" +} + +predicate unsafe_context_creation(DataFlow::Node node, string insecure_version) { + exists(TlsLibrary l, ContextCreation cc | cc = l.insecure_context_creation() | + cc = node and insecure_version = cc.getProtocol().toString() + ) +} + +from DataFlow::Node node, string insecure_version +where + unsafe_connection_creation(node, insecure_version) + or + unsafe_context_creation(node, insecure_version) +select node, "Insecure SSL/TLS protocol version " + insecure_version //+ " specified in call to " + method_name + "." +// from CallNode call, string method_name, string insecure_version +// where +// unsafe_ssl_wrap_socket_call(call, method_name, insecure_version, _) +// or +// unsafe_pyOpenSSL_Context_call(call, insecure_version) and method_name = "pyOpenSSL.SSL.Context" +// select call, +// "Insecure SSL/TLS protocol version " + insecure_version + " specified in call to " + method_name + +// "." From 186db7f43ec254792ca3b0ded440fd0a06080711 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Sun, 21 Feb 2021 12:07:48 +0100 Subject: [PATCH 0015/1429] Python: factor into modules and files --- .../src/Security/CWE-327/FluentApiModel.qll | 54 ++++ .../src/Security/CWE-327/InsecureProtocol.ql | 268 +----------------- python/ql/src/Security/CWE-327/PyOpenSSL.qll | 73 +++++ python/ql/src/Security/CWE-327/Ssl.qll | 106 +++++++ .../src/Security/CWE-327/TlsLibraryModel.qll | 80 ++++++ 5 files changed, 315 insertions(+), 266 deletions(-) create mode 100644 python/ql/src/Security/CWE-327/FluentApiModel.qll create mode 100644 python/ql/src/Security/CWE-327/PyOpenSSL.qll create mode 100644 python/ql/src/Security/CWE-327/Ssl.qll create mode 100644 python/ql/src/Security/CWE-327/TlsLibraryModel.qll diff --git a/python/ql/src/Security/CWE-327/FluentApiModel.qll b/python/ql/src/Security/CWE-327/FluentApiModel.qll new file mode 100644 index 00000000000..4c2278b8694 --- /dev/null +++ b/python/ql/src/Security/CWE-327/FluentApiModel.qll @@ -0,0 +1,54 @@ +import python +import TlsLibraryModel + +class InsecureContextConfiguration extends DataFlow::Configuration { + TlsLibrary library; + + InsecureContextConfiguration() { this = library + ["AllowsTLSv1", "AllowsTLSv1_1"] } + + override predicate isSource(DataFlow::Node source) { + source = library.unspecific_context_creation() + } + + override predicate isSink(DataFlow::Node sink) { + sink = library.connection_creation().getContext() + } + + abstract string flag(); + + override predicate isBarrierOut(DataFlow::Node node) { + exists(ProtocolRestriction r | + r = library.protocol_restriction() and + node = r.getContext() and + r.getRestriction() = flag() + ) + } +} + +class AllowsTLSv1 extends InsecureContextConfiguration { + AllowsTLSv1() { this = library + "AllowsTLSv1" } + + override string flag() { result = "TLSv1" } +} + +class AllowsTLSv1_1 extends InsecureContextConfiguration { + AllowsTLSv1_1() { this = library + "AllowsTLSv1_1" } + + override string flag() { result = "TLSv1_1" } +} + +predicate unsafe_connection_creation(DataFlow::Node node, ProtocolVersion insecure_version) { + exists(AllowsTLSv1 c | c.hasFlowTo(node)) and + insecure_version = "TLSv1" + or + exists(AllowsTLSv1_1 c | c.hasFlowTo(node)) and + insecure_version = "TLSv1_1" + or + exists(TlsLibrary l | l.insecure_connection_creation(insecure_version) = node) +} + +predicate unsafe_context_creation(DataFlow::Node node, string insecure_version) { + exists(TlsLibrary l, ContextCreation cc | cc = l.insecure_context_creation(insecure_version) | + cc = node + ) +} diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index 81b3558907e..b873e5f3ce8 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -10,277 +10,13 @@ */ import python -import semmle.python.ApiGraphs +import FluentApiModel +// string foo(ProtocolRestriction r) { result = r.getRestriction() } // The idea is to track flow from the creation of an insecure context to a use // such as `wrap_socket`. There should be a data-flow path for each insecure version // and each path should have a version specific sanitizer. This will allow fluent api // style code to block the paths one by one. -// -// class InsecureContextCreation extends DataFlow::CfgNode { -// override CallNode node; -// InsecureContextCreation() { -// this = API::moduleImport("ssl").getMember("SSLContext").getACall() and -// insecure_version().asCfgNode() in [node.getArg(0), node.getArgByName("protocol")] -// } -// } -// class InsecureSSLContextCreation extends DataFlow::CfgNode { -// override CallNode node; -// InsecureSSLContextCreation() { -// this = API::moduleImport("ssl").getMember("create_default_context").getACall() -// or -// this = API::moduleImport("ssl").getMember("SSLContext").getACall() and -// API::moduleImport("ssl").getMember("PROTOCOL_TLS").getAUse().asCfgNode() in [ -// node.getArg(0), node.getArgByName("protocol") -// ] -// } -// } -abstract class ContextCreation extends DataFlow::CfgNode { - abstract DataFlow::CfgNode getProtocol(); -} - -abstract class ConnectionCreation extends DataFlow::CfgNode { - abstract DataFlow::CfgNode getContext(); -} - -class ProtocolRestriction extends DataFlow::CfgNode { - abstract DataFlow::CfgNode getContext(); - - abstract string getRestriction(); -} - -abstract class TlsLibrary extends string { - TlsLibrary() { this in ["ssl", "pyOpenSSL"] } - - abstract string specific_insecure_version_name(); - - abstract string unspecific_version_name(); - - abstract API::Node version_constants(); - - DataFlow::Node insecure_version() { - result = version_constants().getMember(specific_insecure_version_name()).getAUse() - } - - DataFlow::Node unspecific_version() { - result = version_constants().getMember(unspecific_version_name()).getAUse() - } - - abstract DataFlow::CfgNode default_context_creation(); - - abstract ContextCreation specific_context_creation(); - - ContextCreation insecure_context_creation() { - result = specific_context_creation() and - result.getProtocol() = insecure_version() - } - - DataFlow::CfgNode unspecific_context_creation() { - result = default_context_creation() - or - result = specific_context_creation() and - result.(ContextCreation).getProtocol() = unspecific_version() - } - - /** A connection is created in an outright insecure manner. */ - abstract DataFlow::CfgNode insecure_connection_creation(); - - /** A connection is created from a context. */ - abstract ConnectionCreation connection_creation(); - - abstract ProtocolRestriction protocol_restriction(); -} - -module ssl { - class SSLContextCreation extends ContextCreation { - override CallNode node; - - SSLContextCreation() { this = API::moduleImport("ssl").getMember("SSLContext").getACall() } - - override DataFlow::CfgNode getProtocol() { - result.getNode() in [node.getArg(0), node.getArgByName("protocol")] - } - } - - class WrapSocketCall extends ConnectionCreation { - override CallNode node; - - WrapSocketCall() { node.getFunction().(AttrNode).getName() = "wrap_socket" } - - override DataFlow::CfgNode getContext() { - result.getNode() = node.getFunction().(AttrNode).getObject() - } - } - - class OptionsAugOr extends ProtocolRestriction { - string restriction; - - OptionsAugOr() { - exists(AugAssign aa, AttrNode attr | - aa.getOperation().getOp() instanceof BitOr and - aa.getTarget() = attr.getNode() and - attr.getName() = "options" and - attr.getObject() = node and - aa.getValue() = API::moduleImport("ssl").getMember(restriction).getAUse().asExpr() - ) - } - - override DataFlow::CfgNode getContext() { result = this } - - override string getRestriction() { result = restriction } - } - - class Ssl extends TlsLibrary { - Ssl() { this = "ssl" } - - override string specific_insecure_version_name() { - result in [ - "PROTOCOL_SSLv2", "PROTOCOL_SSLv3", "PROTOCOL_SSLv23", "PROTOCOL_TLSv1", - "PROTOCOL_TLSv1_1" - ] - } - - override string unspecific_version_name() { result = "PROTOCOL_TLS" } - - override API::Node version_constants() { result = API::moduleImport("ssl") } - - override DataFlow::CfgNode default_context_creation() { - result = API::moduleImport("ssl").getMember("create_default_context").getACall() - } - - override ContextCreation specific_context_creation() { result instanceof SSLContextCreation } - - override DataFlow::CfgNode insecure_connection_creation() { - result = API::moduleImport("ssl").getMember("wrap_socket").getACall() - } - - override ConnectionCreation connection_creation() { result instanceof WrapSocketCall } - - override ProtocolRestriction protocol_restriction() { result instanceof OptionsAugOr } - } -} - -module pyOpenSSL { - class PyOpenSSLContextCreation extends ContextCreation { - override CallNode node; - - PyOpenSSLContextCreation() { - this = API::moduleImport("pyOpenSSL").getMember("SSL").getMember("Context").getACall() - } - - override DataFlow::CfgNode getProtocol() { - result.getNode() in [node.getArg(0), node.getArgByName("method")] - } - } - - class ConnectionCall extends ConnectionCreation { - override CallNode node; - - ConnectionCall() { - this = API::moduleImport("pyOpenSSL").getMember("SSL").getMember("Connection").getACall() - } - - override DataFlow::CfgNode getContext() { - result.getNode() in [node.getArg(0), node.getArgByName("context")] - } - } - - class SetOptionsCall extends ProtocolRestriction { - override CallNode node; - - SetOptionsCall() { node.getFunction().(AttrNode).getName() = "set_options" } - - override DataFlow::CfgNode getContext() { - result.getNode() = node.getFunction().(AttrNode).getObject() - } - - override string getRestriction() { - API::moduleImport("PyOpenSSL").getMember("SSL").getMember(result).getAUse().asCfgNode() in [ - node.getArg(0), node.getArgByName("options") - ] - } - } - - class PyOpenSSL extends TlsLibrary { - PyOpenSSL() { this = "pyOpenSSL" } - - override string specific_insecure_version_name() { - result in ["SSLv2_METHOD", "SSLv23_METHOD", "SSLv3_METHOD", "TLSv1_METHOD", "TLSv1_1_METHOD"] - } - - override string unspecific_version_name() { result = "TLS_METHOD" } - - override API::Node version_constants() { - result = API::moduleImport("pyOpenSSL").getMember("SSL") - } - - override DataFlow::CfgNode default_context_creation() { none() } - - override ContextCreation specific_context_creation() { - result instanceof PyOpenSSLContextCreation - } - - override DataFlow::CfgNode insecure_connection_creation() { none() } - - override ConnectionCreation connection_creation() { result instanceof ConnectionCall } - - override ProtocolRestriction protocol_restriction() { result instanceof SetOptionsCall } - } -} - -class InsecureContextConfiguration extends DataFlow::Configuration { - TlsLibrary library; - - InsecureContextConfiguration() { this = library + ["AllowsTLSv1", "AllowsTLSv1_1"] } - - override predicate isSource(DataFlow::Node source) { - source = library.unspecific_context_creation() - } - - override predicate isSink(DataFlow::Node sink) { - sink = library.connection_creation().getContext() - } - - abstract string flag(); - - override predicate isBarrierOut(DataFlow::Node node) { - exists(ProtocolRestriction r | - r = library.protocol_restriction() and - node = r.getContext() and - r.getRestriction() = flag() - ) - } -} - -class AllowsTLSv1 extends InsecureContextConfiguration { - AllowsTLSv1() { this = library + "AllowsTLSv1" } - - override string flag() { result = "OP_NO_TLSv1" } -} - -class AllowsTLSv1_1 extends InsecureContextConfiguration { - AllowsTLSv1_1() { this = library + "AllowsTLSv1_1" } - - override string flag() { result = "OP_NO_TLSv1_1" } -} - -predicate unsafe_connection_creation(DataFlow::Node node, string insecure_version) { - exists(AllowsTLSv1 c | c.hasFlowTo(node)) and - insecure_version = "TLSv1" - or - exists(AllowsTLSv1_1 c | c.hasFlowTo(node)) and - insecure_version = "TLSv1" - or - exists(TlsLibrary l | l.insecure_connection_creation() = node) and - insecure_version = "[multiple]" -} - -predicate unsafe_context_creation(DataFlow::Node node, string insecure_version) { - exists(TlsLibrary l, ContextCreation cc | cc = l.insecure_context_creation() | - cc = node and insecure_version = cc.getProtocol().toString() - ) -} - from DataFlow::Node node, string insecure_version where unsafe_connection_creation(node, insecure_version) diff --git a/python/ql/src/Security/CWE-327/PyOpenSSL.qll b/python/ql/src/Security/CWE-327/PyOpenSSL.qll new file mode 100644 index 00000000000..b9a68648f93 --- /dev/null +++ b/python/ql/src/Security/CWE-327/PyOpenSSL.qll @@ -0,0 +1,73 @@ +import python +import semmle.python.ApiGraphs +import TlsLibraryModel + +class PyOpenSSLContextCreation extends ContextCreation { + override CallNode node; + + PyOpenSSLContextCreation() { + this = API::moduleImport("OpenSSL").getMember("SSL").getMember("Context").getACall() + } + + override DataFlow::CfgNode getProtocol() { + result.getNode() in [node.getArg(0), node.getArgByName("method")] + } +} + +class ConnectionCall extends ConnectionCreation { + override CallNode node; + + ConnectionCall() { + this = API::moduleImport("OpenSSL").getMember("SSL").getMember("Connection").getACall() + } + + override DataFlow::CfgNode getContext() { + result.getNode() in [node.getArg(0), node.getArgByName("context")] + } +} + +class SetOptionsCall extends ProtocolRestriction { + override CallNode node; + + SetOptionsCall() { node.getFunction().(AttrNode).getName() = "set_options" } + + override DataFlow::CfgNode getContext() { + result.getNode() = node.getFunction().(AttrNode).getObject() + } + + override ProtocolVersion getRestriction() { + API::moduleImport("OpenSSL").getMember("SSL").getMember("OP_NO_" + result).getAUse().asCfgNode() in [ + node.getArg(0), node.getArgByName("options") + ] + } +} + +class PyOpenSSL extends TlsLibrary { + PyOpenSSL() { this = "pyOpenSSL" } + + override string specific_insecure_version_name(ProtocolVersion version) { + version in ["SSLv2", "SSLv3", "TLSv1", "TLSv1_1"] and + result = version + "_METHOD" + } + + override string unspecific_version_name() { + result in [ + "TLS_METHOD", // This is not actually available in pyOpenSSL yet + "SSLv23_METHOD" // This is what can negotiate TLS 1.3 (indeed, I know, I did test that..) + ] + } + + override API::Node version_constants() { result = API::moduleImport("OpenSSL").getMember("SSL") } + + override DataFlow::CfgNode default_context_creation() { none() } + + override ContextCreation specific_context_creation() { + result instanceof PyOpenSSLContextCreation + } + + override DataFlow::CfgNode insecure_connection_creation(ProtocolVersion version) { none() } + + override ConnectionCreation connection_creation() { result instanceof ConnectionCall } + + override ProtocolRestriction protocol_restriction() { result instanceof SetOptionsCall } +} diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll new file mode 100644 index 00000000000..369149fd638 --- /dev/null +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -0,0 +1,106 @@ +import python +import semmle.python.ApiGraphs +import semmle.python.dataflow.new.internal.Attributes as Attributes +import TlsLibraryModel + +class SSLContextCreation extends ContextCreation { + override CallNode node; + + SSLContextCreation() { this = API::moduleImport("ssl").getMember("SSLContext").getACall() } + + override DataFlow::CfgNode getProtocol() { + result.getNode() in [node.getArg(0), node.getArgByName("protocol")] + } +} + +class WrapSocketCall extends ConnectionCreation { + override CallNode node; + + WrapSocketCall() { node.getFunction().(AttrNode).getName() = "wrap_socket" } + + override DataFlow::CfgNode getContext() { + result.getNode() = node.getFunction().(AttrNode).getObject() + } +} + +class OptionsAugOr extends ProtocolRestriction { + ProtocolVersion restriction; + + OptionsAugOr() { + exists(AugAssign aa, AttrNode attr | + aa.getOperation().getOp() instanceof BitOr and + aa.getTarget() = attr.getNode() and + attr.getName() = "options" and + attr.getObject() = node and + API::moduleImport("ssl").getMember("OP_NO_" + restriction).getAUse().asExpr() in [ + aa.getValue(), aa.getValue().getAChildNode() + ] + ) + } + + override DataFlow::CfgNode getContext() { result = this } + + override ProtocolVersion getRestriction() { result = restriction } +} + +class ContextSetVersion extends ProtocolRestriction { + string restriction; + + ContextSetVersion() { + exists(Attributes::AttrWrite aw | + aw.getObject().asCfgNode() = node and + aw.getAttributeName() = "minimum_version" and + aw.getValue() = + API::moduleImport("ssl").getMember("TLSVersion").getMember(restriction).getAUse() + ) + } + + override DataFlow::CfgNode getContext() { result = this } + + override ProtocolVersion getRestriction() { result.lessThan(restriction) } +} + +class Ssl extends TlsLibrary { + Ssl() { this = "ssl" } + + override string specific_insecure_version_name(ProtocolVersion version) { + version in ["SSLv2", "SSLv3", "TLSv1", "TLSv1_1"] and + result = "PROTOCOL_" + version + // result in ["PROTOCOL_SSLv2", "PROTOCOL_SSLv3", "PROTOCOL_TLSv1", "PROTOCOL_TLSv1_1"] + } + + override string unspecific_version_name() { + result = + "PROTOCOL_" + + [ + "TLS", + // This can negotiate a TLS 1.3 connection (!) + // see https://docs.python.org/3/library/ssl.html#ssl-contexts + "SSLv23" + ] + } + + override API::Node version_constants() { result = API::moduleImport("ssl") } + + override DataFlow::CfgNode default_context_creation() { + result = API::moduleImport("ssl").getMember("create_default_context").getACall() //and + // see https://docs.python.org/3/library/ssl.html#context-creation + // version in ["TLSv1", "TLSv1_1"] + } + + override ContextCreation specific_context_creation() { result instanceof SSLContextCreation } + + override DataFlow::CfgNode insecure_connection_creation(ProtocolVersion version) { + result = API::moduleImport("ssl").getMember("wrap_socket").getACall() and + insecure_version(version).asCfgNode() = + result.asCfgNode().(CallNode).getArgByName("ssl_version") + } + + override ConnectionCreation connection_creation() { result instanceof WrapSocketCall } + + override ProtocolRestriction protocol_restriction() { + result instanceof OptionsAugOr + or + result instanceof ContextSetVersion + } +} diff --git a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll new file mode 100644 index 00000000000..41db81ad1c8 --- /dev/null +++ b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll @@ -0,0 +1,80 @@ +import python +import semmle.python.ApiGraphs +import Ssl +import PyOpenSSL + +/** + * A specific protocol version. + * We use this to identify a protocol. + */ +class ProtocolVersion extends string { + ProtocolVersion() { this in ["SSLv2", "SSLv3", "TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] } + + predicate lessThan(ProtocolVersion version) { + this = "SSLv2" and version = "SSLv3" + or + this = "TLSv1" and version = ["TLSv1_1", "TLSv1_2", "TLSv1_3"] + or + this = ["TLSv1", "TLSv1_1"] and version = ["TLSv1_2", "TLSv1_3"] + or + this = ["TLSv1", "TLSv1_1", "TLSv1_2"] and version = "TLSv1_3" + } +} + +abstract class ContextCreation extends DataFlow::CfgNode { + abstract DataFlow::CfgNode getProtocol(); +} + +abstract class ConnectionCreation extends DataFlow::CfgNode { + abstract DataFlow::CfgNode getContext(); +} + +abstract class ProtocolRestriction extends DataFlow::CfgNode { + abstract DataFlow::CfgNode getContext(); + + abstract ProtocolVersion getRestriction(); +} + +abstract class TlsLibrary extends string { + TlsLibrary() { this in ["ssl", "pyOpenSSL"] } + + /** The name of a specific protocol version, known to be insecure. */ + abstract string specific_insecure_version_name(ProtocolVersion version); + + /** The name of an unspecific protocol version, say TLS, known to have insecure insatnces. */ + abstract string unspecific_version_name(); + + abstract API::Node version_constants(); + + DataFlow::Node insecure_version(ProtocolVersion version) { + result = version_constants().getMember(specific_insecure_version_name(version)).getAUse() + } + + DataFlow::Node unspecific_version() { + result = version_constants().getMember(unspecific_version_name()).getAUse() + } + + abstract DataFlow::CfgNode default_context_creation(); + + abstract ContextCreation specific_context_creation(); + + ContextCreation insecure_context_creation(ProtocolVersion version) { + result = specific_context_creation() and + result.getProtocol() = insecure_version(version) + } + + DataFlow::CfgNode unspecific_context_creation() { + result = default_context_creation() + or + result = specific_context_creation() and + result.(ContextCreation).getProtocol() = unspecific_version() + } + + /** A connection is created in an outright insecure manner. */ + abstract DataFlow::CfgNode insecure_connection_creation(ProtocolVersion version); + + /** A connection is created from a context. */ + abstract ConnectionCreation connection_creation(); + + abstract ProtocolRestriction protocol_restriction(); +} From 87e1a062ea750a3b2acc95d75583099c055defe9 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Sun, 21 Feb 2021 12:08:11 +0100 Subject: [PATCH 0016/1429] Python: fluent api tests --- .../Security/CWE-327/pyOpenSSL_fluent.py | 31 +++++++ .../Security/CWE-327/ssl_fluent.py | 87 +++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py create mode 100644 python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py diff --git a/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py b/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py new file mode 100644 index 00000000000..028c318be66 --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py @@ -0,0 +1,31 @@ +import socket +from OpenSSL import SSL + +def test_fluent(): + hostname = 'www.python.org' + context = SSL.Context(SSL.SSLv23_METHOD) + + conn = SSL.Connection(context, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) + r = conn.connect((hostname, 443)) + print(conn.get_protocol_version_name()) + + +def test_fluent_no_TLSv1(): + hostname = 'www.python.org' + context = SSL.Context(SSL.SSLv23_METHOD) + context.set_options(SSL.OP_NO_TLSv1) + + conn = SSL.Connection(context, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) + r = conn.connect((hostname, 443)) + print(conn.get_protocol_version_name()) + + +def test_fluent_safe(): + hostname = 'www.python.org' + context = SSL.Context(SSL.SSLv23_METHOD) + context.set_options(SSL.OP_NO_TLSv1) + context.set_options(SSL.OP_NO_TLSv1_1) + + conn = SSL.Connection(context, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) + r = conn.connect((hostname, 443)) + print(r, conn.get_protocol_version_name()) diff --git a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py new file mode 100644 index 00000000000..9f9e01294cb --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py @@ -0,0 +1,87 @@ +import socket +import ssl + +def test_fluent_tls(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_TLS) + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + + +def test_fluent_tls_no_TLSv1(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_TLS) + context.options |= ssl.OP_NO_TLSv1 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_fluent_tls_safe(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_TLS) + context.options |= ssl.OP_NO_TLSv1 + context.options |= ssl.OP_NO_TLSv1_1 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_fluent_ssl(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + + +def test_fluent_ssl_no_TLSv1(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_TLSv1 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_fluent_ssl_safe(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_TLSv1 + context.options |= ssl.OP_NO_TLSv1_1 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_fluent_ssl_safe_combined(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +# From Python 3.7 +# see https://docs.python.org/3/library/ssl.html#ssl.SSLContext.minimum_version +def test_fluent_ssl_unsafe_version(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_TLS) + context.minimum_version = ssl.TLSVersion.TLSv1_1 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_fluent_ssl_safe_version(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_TLS) + context.minimum_version = ssl.TLSVersion.TLSv1_3 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) From ea8c6f04e2ba137b6501152f22b93f642e17b372 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Sun, 21 Feb 2021 12:09:56 +0100 Subject: [PATCH 0017/1429] Python: Update old test and qlhelp --- .../Security/CWE-327/InsecureProtocol.qhelp | 6 ++--- python/ql/src/Security/CWE-327/ReadMe.md | 24 +++++++++++++++++++ .../Security/CWE-327/InsecureProtocol.py | 8 +++---- 3 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 python/ql/src/Security/CWE-327/ReadMe.md diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp b/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp index 63545129cc7..4a7364ca14e 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp @@ -13,8 +13,8 @@

    Ensure that a modern, strong protocol is used. All versions of SSL, - and TLS 1.0 are known to be vulnerable to attacks. Using TLS 1.1 or - above is strongly recommended. + and TLS versions 1.0 and 1.1 are known to be vulnerable to attacks. + Using TLS 1.2 or above is strongly recommended.

    @@ -30,7 +30,7 @@

    All cases should be updated to use a secure protocol, such as - PROTOCOL_TLSv1_1. + PROTOCOL_TLSv1_2.

    Note that ssl.wrap_socket has been deprecated in diff --git a/python/ql/src/Security/CWE-327/ReadMe.md b/python/ql/src/Security/CWE-327/ReadMe.md new file mode 100644 index 00000000000..c5330a78fda --- /dev/null +++ b/python/ql/src/Security/CWE-327/ReadMe.md @@ -0,0 +1,24 @@ +# Current status (Feb 2021) + +This should be kept up to date; the world is moving fast and protocols are being broken. + +## Protocols + +- All versions of SSL are insecure +- TLS 1.0 and TLS 1.1 are insecure +- TLS 1.2 have some issues. but TLS 1.3 is not widely supported + +## Conection methods + +- `ssl.wrap_socket` is creating insecure connections, use `SSLContext.wrap_socket` instead. [link](https://docs.python.org/3/library/ssl.html#ssl.wrap_socket) + > Deprecated since version 3.7: Since Python 3.2 and 2.7.9, it is recommended to use the `SSLContext.wrap_socket()` instead of `wrap_socket()`. The top-level function is limited and creates an insecure client socket without server name indication or hostname matching. +- Default consteructors are fine, a sluent api is used to constrain possible protocols later. + +## Current recomendation + +TLS 1.2 or TLS 1.3 + +## Queries + +- `InsecureProtocol` detects uses of insecure protocols. +- `InsecureDefaultProtocol` detect default constructions, this is no longer unsafe. diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py index 5a6d044aa6d..cb21a6623c9 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py @@ -1,5 +1,5 @@ import ssl -from pyOpenSSL import SSL +from OpenSSL import SSL from ssl import SSLContext # true positives @@ -33,9 +33,9 @@ SSL.Context(METHOD) # secure versions -ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_1) -SSLContext(protocol=ssl.PROTOCOL_TLSv1_1) -SSL.Context(SSL.TLSv1_1_METHOD) +ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) +SSLContext(protocol=ssl.PROTOCOL_TLSv1_2) +SSL.Context(SSL.TLSv1_2_METHOD) # possibly insecure default ssl.wrap_socket() From 3b856010f28ade83481b3e9fe553c4bb56f0f432 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 23 Feb 2021 19:29:30 +0100 Subject: [PATCH 0018/1429] Python: add TODO comment --- python/ql/src/Security/CWE-327/Ssl.qll | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index 369149fd638..e3247d5143c 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -32,6 +32,7 @@ class OptionsAugOr extends ProtocolRestriction { aa.getTarget() = attr.getNode() and attr.getName() = "options" and attr.getObject() = node and + // TODO: Use something like BoolExpr::impliesValue here API::moduleImport("ssl").getMember("OP_NO_" + restriction).getAUse().asExpr() in [ aa.getValue(), aa.getValue().getAChildNode() ] From d5171fc043cd4a66372780bcbe5c758cd7c594e2 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Feb 2021 21:12:34 +0100 Subject: [PATCH 0019/1429] Python: Comment everything --- .../src/Security/CWE-327/FluentApiModel.qll | 15 +++++++++++++ .../src/Security/CWE-327/InsecureProtocol.ql | 13 ------------ python/ql/src/Security/CWE-327/PyOpenSSL.qll | 2 +- python/ql/src/Security/CWE-327/Ssl.qll | 17 ++++++++++----- .../src/Security/CWE-327/TlsLibraryModel.qll | 21 ++++++++++++++++--- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/python/ql/src/Security/CWE-327/FluentApiModel.qll b/python/ql/src/Security/CWE-327/FluentApiModel.qll index 4c2278b8694..3dc12e2f434 100644 --- a/python/ql/src/Security/CWE-327/FluentApiModel.qll +++ b/python/ql/src/Security/CWE-327/FluentApiModel.qll @@ -1,6 +1,11 @@ import python import TlsLibraryModel +/** + * Configuration to track flow from the creation of a context to + * that context being used to create a connection. + * Flow is broken if the insecure protocol of interest is being restricted. + */ class InsecureContextConfiguration extends DataFlow::Configuration { TlsLibrary library; @@ -25,28 +30,38 @@ class InsecureContextConfiguration extends DataFlow::Configuration { } } +/** Configuration to specifically track the insecure protocol TLS 1.0 */ class AllowsTLSv1 extends InsecureContextConfiguration { AllowsTLSv1() { this = library + "AllowsTLSv1" } override string flag() { result = "TLSv1" } } +/** Configuration to specifically track the insecure protocol TLS 1.1 */ class AllowsTLSv1_1 extends InsecureContextConfiguration { AllowsTLSv1_1() { this = library + "AllowsTLSv1_1" } override string flag() { result = "TLSv1_1" } } +/** + * A connection is created from a context allowing an insecure protocol, + * and that protocol has not been restricted appropriately. + */ predicate unsafe_connection_creation(DataFlow::Node node, ProtocolVersion insecure_version) { + // Connection created from a context allowing TLS 1.0. exists(AllowsTLSv1 c | c.hasFlowTo(node)) and insecure_version = "TLSv1" or + // Connection created from a context allowing TLS 1.1. exists(AllowsTLSv1_1 c | c.hasFlowTo(node)) and insecure_version = "TLSv1_1" or + // Connection created from a context for an insecure protocol. exists(TlsLibrary l | l.insecure_connection_creation(insecure_version) = node) } +/** A connection is created insecurely without reference to a context. */ predicate unsafe_context_creation(DataFlow::Node node, string insecure_version) { exists(TlsLibrary l, ContextCreation cc | cc = l.insecure_context_creation(insecure_version) | cc = node diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index b873e5f3ce8..c902d9a284d 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -12,22 +12,9 @@ import python import FluentApiModel -// string foo(ProtocolRestriction r) { result = r.getRestriction() } -// The idea is to track flow from the creation of an insecure context to a use -// such as `wrap_socket`. There should be a data-flow path for each insecure version -// and each path should have a version specific sanitizer. This will allow fluent api -// style code to block the paths one by one. from DataFlow::Node node, string insecure_version where unsafe_connection_creation(node, insecure_version) or unsafe_context_creation(node, insecure_version) select node, "Insecure SSL/TLS protocol version " + insecure_version //+ " specified in call to " + method_name + "." -// from CallNode call, string method_name, string insecure_version -// where -// unsafe_ssl_wrap_socket_call(call, method_name, insecure_version, _) -// or -// unsafe_pyOpenSSL_Context_call(call, insecure_version) and method_name = "pyOpenSSL.SSL.Context" -// select call, -// "Insecure SSL/TLS protocol version " + insecure_version + " specified in call to " + method_name + -// "." diff --git a/python/ql/src/Security/CWE-327/PyOpenSSL.qll b/python/ql/src/Security/CWE-327/PyOpenSSL.qll index b9a68648f93..a89c7ff0886 100644 --- a/python/ql/src/Security/CWE-327/PyOpenSSL.qll +++ b/python/ql/src/Security/CWE-327/PyOpenSSL.qll @@ -59,7 +59,7 @@ class PyOpenSSL extends TlsLibrary { override API::Node version_constants() { result = API::moduleImport("OpenSSL").getMember("SSL") } - override DataFlow::CfgNode default_context_creation() { none() } + override ContextCreation default_context_creation() { none() } override ContextCreation specific_context_creation() { result instanceof PyOpenSSLContextCreation diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index e3247d5143c..ba91b39bdeb 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -13,6 +13,16 @@ class SSLContextCreation extends ContextCreation { } } +class SSLDefaultContextCreation extends ContextCreation { + SSLDefaultContextCreation() { + this = API::moduleImport("ssl").getMember("create_default_context").getACall() + } + + // Allowed insecure versions are "TLSv1" and "TLSv1_1" + // see https://docs.python.org/3/library/ssl.html#context-creation + override DataFlow::CfgNode getProtocol() { none() } +} + class WrapSocketCall extends ConnectionCreation { override CallNode node; @@ -67,7 +77,6 @@ class Ssl extends TlsLibrary { override string specific_insecure_version_name(ProtocolVersion version) { version in ["SSLv2", "SSLv3", "TLSv1", "TLSv1_1"] and result = "PROTOCOL_" + version - // result in ["PROTOCOL_SSLv2", "PROTOCOL_SSLv3", "PROTOCOL_TLSv1", "PROTOCOL_TLSv1_1"] } override string unspecific_version_name() { @@ -83,10 +92,8 @@ class Ssl extends TlsLibrary { override API::Node version_constants() { result = API::moduleImport("ssl") } - override DataFlow::CfgNode default_context_creation() { - result = API::moduleImport("ssl").getMember("create_default_context").getACall() //and - // see https://docs.python.org/3/library/ssl.html#context-creation - // version in ["TLSv1", "TLSv1_1"] + override ContextCreation default_context_creation() { + result instanceof SSLDefaultContextCreation } override ContextCreation specific_context_creation() { result instanceof SSLContextCreation } diff --git a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll index 41db81ad1c8..36e58acd926 100644 --- a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll +++ b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll @@ -21,17 +21,24 @@ class ProtocolVersion extends string { } } +/** The creation of a context. */ abstract class ContextCreation extends DataFlow::CfgNode { + /** Gets the requested protocol if any. */ abstract DataFlow::CfgNode getProtocol(); } +/** The creation of a connection from a context. */ abstract class ConnectionCreation extends DataFlow::CfgNode { + /** Gets the context used to create the connection. */ abstract DataFlow::CfgNode getContext(); } +/** A context is being restricted on which protocols it can accepts. */ abstract class ProtocolRestriction extends DataFlow::CfgNode { + /** Gets the context being restricted. */ abstract DataFlow::CfgNode getContext(); + /** Gets the protocol version being disallowed. */ abstract ProtocolVersion getRestriction(); } @@ -41,28 +48,35 @@ abstract class TlsLibrary extends string { /** The name of a specific protocol version, known to be insecure. */ abstract string specific_insecure_version_name(ProtocolVersion version); - /** The name of an unspecific protocol version, say TLS, known to have insecure insatnces. */ + /** The name of an unspecific protocol version, say TLS, known to have insecure instances. */ abstract string unspecific_version_name(); + /** The module or class holding the version constants. */ abstract API::Node version_constants(); + /** A dataflow node representing a specific protocol version, known to be insecure. */ DataFlow::Node insecure_version(ProtocolVersion version) { result = version_constants().getMember(specific_insecure_version_name(version)).getAUse() } + /** A dataflow node representing an unspecific protocol version, say TLS, known to have insecure instances. */ DataFlow::Node unspecific_version() { result = version_constants().getMember(unspecific_version_name()).getAUse() } - abstract DataFlow::CfgNode default_context_creation(); + /** The creation of a context with a deafult protocol. */ + abstract ContextCreation default_context_creation(); + /** The creation of a context with a specific protocol. */ abstract ContextCreation specific_context_creation(); + /** The creation of a context with a specific protocol version, known to be insecure. */ ContextCreation insecure_context_creation(ProtocolVersion version) { result = specific_context_creation() and result.getProtocol() = insecure_version(version) } + /** The creation of a context with an unspecific protocol version, say TLS, known to have insecure instances. */ DataFlow::CfgNode unspecific_context_creation() { result = default_context_creation() or @@ -70,11 +84,12 @@ abstract class TlsLibrary extends string { result.(ContextCreation).getProtocol() = unspecific_version() } - /** A connection is created in an outright insecure manner. */ + /** A connection is created in an insecure manner, not from a context. */ abstract DataFlow::CfgNode insecure_connection_creation(ProtocolVersion version); /** A connection is created from a context. */ abstract ConnectionCreation connection_creation(); + /** A context is being restricted on which protocols it can accepts. */ abstract ProtocolRestriction protocol_restriction(); } From 9e696ff0fbaac008a12bfa1a7241a0f0140bbc9d Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Feb 2021 21:58:00 +0100 Subject: [PATCH 0020/1429] Python: Add false negative to test --- .../ql/test/query-tests/Security/CWE-327/ssl_fluent.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py index 9f9e01294cb..252ab0a9e49 100644 --- a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py +++ b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py @@ -85,3 +85,13 @@ def test_fluent_ssl_safe_version(): with socket.create_connection((hostname, 443)) as sock: with context.wrap_socket(sock, server_hostname=hostname) as ssock: print(ssock.version()) + +# Taken from https://docs.python.org/3/library/ssl.html#context-creation +def test_fluent_explicitly_unsafe(): + hostname = 'www.python.org' + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.options &= ~ssl.OP_NO_SSLv3 # This not recognized + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: # SSLv3 not flagged here + print(ssock.version()) From 60525ec3018924898d47d72d133cb0b1431a2bea Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Feb 2021 21:58:30 +0100 Subject: [PATCH 0021/1429] Python: Also track offending call update test expectations at this point --- .../src/Security/CWE-327/FluentApiModel.qll | 20 +++++++--- .../src/Security/CWE-327/InsecureProtocol.ql | 18 ++++++--- .../CWE-327/InsecureProtocol.expected | 38 ++++++++++++------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/python/ql/src/Security/CWE-327/FluentApiModel.qll b/python/ql/src/Security/CWE-327/FluentApiModel.qll index 3dc12e2f434..23922a90595 100644 --- a/python/ql/src/Security/CWE-327/FluentApiModel.qll +++ b/python/ql/src/Security/CWE-327/FluentApiModel.qll @@ -48,22 +48,30 @@ class AllowsTLSv1_1 extends InsecureContextConfiguration { * A connection is created from a context allowing an insecure protocol, * and that protocol has not been restricted appropriately. */ -predicate unsafe_connection_creation(DataFlow::Node node, ProtocolVersion insecure_version) { +predicate unsafe_connection_creation( + DataFlow::Node node, ProtocolVersion insecure_version, CallNode call +) { // Connection created from a context allowing TLS 1.0. - exists(AllowsTLSv1 c | c.hasFlowTo(node)) and + exists(AllowsTLSv1 c, ContextCreation cc | c.hasFlow(cc, node) | cc.getNode() = call) and insecure_version = "TLSv1" or // Connection created from a context allowing TLS 1.1. - exists(AllowsTLSv1_1 c | c.hasFlowTo(node)) and + exists(AllowsTLSv1_1 c, ContextCreation cc | c.hasFlow(cc, node) | cc.getNode() = call) and insecure_version = "TLSv1_1" or // Connection created from a context for an insecure protocol. - exists(TlsLibrary l | l.insecure_connection_creation(insecure_version) = node) + exists(TlsLibrary l, DataFlow::CfgNode cc | + cc = l.insecure_connection_creation(insecure_version) + | + cc = node and + cc.getNode() = call + ) } /** A connection is created insecurely without reference to a context. */ -predicate unsafe_context_creation(DataFlow::Node node, string insecure_version) { +predicate unsafe_context_creation(DataFlow::Node node, string insecure_version, CallNode call) { exists(TlsLibrary l, ContextCreation cc | cc = l.insecure_context_creation(insecure_version) | - cc = node + cc = node and + cc.getNode() = call ) } diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index c902d9a284d..51247dbd75a 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -12,9 +12,17 @@ import python import FluentApiModel -from DataFlow::Node node, string insecure_version -where - unsafe_connection_creation(node, insecure_version) +string callName(AstNode call) { + result = call.(Name).getId() or - unsafe_context_creation(node, insecure_version) -select node, "Insecure SSL/TLS protocol version " + insecure_version //+ " specified in call to " + method_name + "." + exists(Attribute a | a = call | result = callName(a.getObject()) + "." + a.getName()) +} + +from DataFlow::Node node, string insecure_version, CallNode call +where + unsafe_connection_creation(node, insecure_version, call) + or + unsafe_context_creation(node, insecure_version, call) +select node, "Insecure SSL/TLS protocol version " + insecure_version + " specified in $@ ", call, + "call to " + callName(call.getFunction().getNode()) +//+ " specified in call to " + method_name + "." diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected index 33542e4a048..7b646f9fd3e 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected @@ -1,14 +1,24 @@ -| InsecureProtocol.py:6:1:6:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version PROTOCOL_SSLv2 specified in call to deprecated method ssl.wrap_socket. | -| InsecureProtocol.py:7:1:7:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version PROTOCOL_SSLv3 specified in call to deprecated method ssl.wrap_socket. | -| InsecureProtocol.py:8:1:8:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version PROTOCOL_TLSv1 specified in call to deprecated method ssl.wrap_socket. | -| InsecureProtocol.py:10:1:10:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version PROTOCOL_SSLv2 specified in call to ssl.SSLContext. | -| InsecureProtocol.py:11:1:11:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version PROTOCOL_SSLv3 specified in call to ssl.SSLContext. | -| InsecureProtocol.py:12:1:12:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version PROTOCOL_TLSv1 specified in call to ssl.SSLContext. | -| InsecureProtocol.py:14:1:14:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2_METHOD specified in call to pyOpenSSL.SSL.Context. | -| InsecureProtocol.py:15:1:15:30 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv23_METHOD specified in call to pyOpenSSL.SSL.Context. | -| InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv3_METHOD specified in call to pyOpenSSL.SSL.Context. | -| InsecureProtocol.py:17:1:17:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version TLSv1_METHOD specified in call to pyOpenSSL.SSL.Context. | -| InsecureProtocol.py:32:1:32:19 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2_METHOD specified in call to pyOpenSSL.SSL.Context. | -| InsecureProtocol.py:48:1:48:43 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version PROTOCOL_SSLv2 specified in call to deprecated method ssl.wrap_socket. | -| InsecureProtocol.py:49:1:49:35 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version PROTOCOL_SSLv2 specified in call to ssl.SSLContext. | -| InsecureProtocol.py:52:1:52:33 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv23_METHOD specified in call to ssl.SSLContext. | +| InsecureProtocol.py:6:1:6:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:6:1:6:47 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | +| InsecureProtocol.py:7:1:7:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv3 specified in $@ | InsecureProtocol.py:7:1:7:47 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | +| InsecureProtocol.py:8:1:8:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version TLSv1 specified in $@ | InsecureProtocol.py:8:1:8:47 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | +| InsecureProtocol.py:10:1:10:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:10:1:10:39 | ControlFlowNode for SSLContext() | call to SSLContext | +| InsecureProtocol.py:11:1:11:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv3 specified in $@ | InsecureProtocol.py:11:1:11:39 | ControlFlowNode for SSLContext() | call to SSLContext | +| InsecureProtocol.py:12:1:12:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version TLSv1 specified in $@ | InsecureProtocol.py:12:1:12:39 | ControlFlowNode for SSLContext() | call to SSLContext | +| InsecureProtocol.py:14:1:14:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:14:1:14:29 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv3 specified in $@ | InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:17:1:17:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version TLSv1 specified in $@ | InsecureProtocol.py:17:1:17:29 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:32:1:32:19 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:32:1:32:19 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:48:1:48:43 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:48:1:48:43 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | +| InsecureProtocol.py:49:1:49:35 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:49:1:49:35 | ControlFlowNode for SSLContext() | call to SSLContext | +| pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:18:27:18:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | pyOpenSSL_fluent.py:15:15:15:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:19:14:19:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:15:15:15:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:47:14:47:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:43:15:43:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:77:14:77:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:73:15:73:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:96:14:96:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:92:15:92:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | +| ssl_fluent.py:96:14:96:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:92:15:92:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | From 7a1d953fcac8d159440345dd26dd13b4136f1481 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 2 Mar 2021 22:46:01 +0100 Subject: [PATCH 0022/1429] Python: More tests --- .../CWE-327/InsecureProtocol.expected | 14 ++- .../Security/CWE-327/ssl_fluent.py | 96 +++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected index 7b646f9fd3e..e116662ce65 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected @@ -19,6 +19,14 @@ | ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:47:14:47:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:43:15:43:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:77:14:77:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:73:15:73:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:96:14:96:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:92:15:92:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | -| ssl_fluent.py:96:14:96:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:92:15:92:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | +| ssl_fluent.py:75:14:75:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:71:15:71:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:75:14:75:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:71:15:71:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:128:15:128:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:128:15:128:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:104:14:104:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:104:14:104:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:173:14:173:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:169:15:169:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:188:15:188:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | +| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:188:15:188:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | diff --git a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py index 252ab0a9e49..eceacaebe21 100644 --- a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py +++ b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py @@ -66,6 +66,102 @@ def test_fluent_ssl_safe_combined(): with context.wrap_socket(sock, server_hostname=hostname) as ssock: print(ssock.version()) +def test_fluent_ssl_unsafe_combined_wrongly(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_TLSv1 & ssl.OP_NO_TLSv1_1 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_fluent_ssl_safe_combined_multiple(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + + +def create_relaxed_context(): + return ssl.SSLContext(ssl.PROTOCOL_SSLv23) + +def create_secure_context(): + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + return context + +def create_connection(context): + with socket.create_connection(('www.python.org', 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_delegated_context_unsafe(): + context = create_relaxed_context() + with socket.create_connection(('www.python.org', 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_delegated_context_safe(): + context = create_secure_context() + with socket.create_connection(('www.python.org', 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_delegated_context_made_safe(): + context = create_relaxed_context() + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + with socket.create_connection(('www.python.org', 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_delegated_context_made_unsafe(): + context = create_secure_context() + context.options &= ~ssl.OP_NO_TLSv1_1 + with socket.create_connection(('www.python.org', 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_delegated_connection_unsafe(): + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + create_connection(context) + +def test_delegated_connection_safe(): + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + create_connection(context) + +def test_delegated_connection_made_safe(): + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + create_connection(context) + +def test_delegated_connection_made_unsafe(): + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + context.options &= ~ssl.OP_NO_TLSv1_1 + create_connection(context) + +def test_delegated_unsafe(): + context = create_relaxed_context() + create_connection(context) + +def test_delegated_safe(): + context = create_secure_context() + create_connection(context) + +def test_delegated_made_safe(): + context = create_relaxed_context() + context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + create_connection(context) + +def test_delegated_made_unsafe(): + context = create_secure_context() + context.options &= ~ssl.OP_NO_TLSv1_1 + create_connection(context) + # From Python 3.7 # see https://docs.python.org/3/library/ssl.html#ssl.SSLContext.minimum_version def test_fluent_ssl_unsafe_version(): From 97d26687fed6b102c0c2be075ec6a0350a9ef246 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Mon, 1 Mar 2021 15:04:48 +0100 Subject: [PATCH 0023/1429] Python: Improve logic of bit fields --- python/ql/src/Security/CWE-327/Ssl.qll | 31 +++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index ba91b39bdeb..4caa0ae7302 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -37,15 +37,17 @@ class OptionsAugOr extends ProtocolRestriction { ProtocolVersion restriction; OptionsAugOr() { - exists(AugAssign aa, AttrNode attr | + exists(AugAssign aa, AttrNode attr, Expr flag | aa.getOperation().getOp() instanceof BitOr and aa.getTarget() = attr.getNode() and attr.getName() = "options" and attr.getObject() = node and - // TODO: Use something like BoolExpr::impliesValue here - API::moduleImport("ssl").getMember("OP_NO_" + restriction).getAUse().asExpr() in [ - aa.getValue(), aa.getValue().getAChildNode() - ] + flag = API::moduleImport("ssl").getMember("OP_NO_" + restriction).getAUse().asExpr() and + ( + aa.getValue() = flag + or + impliesValue(aa.getValue(), flag, false, false) + ) ) } @@ -54,6 +56,25 @@ class OptionsAugOr extends ProtocolRestriction { override ProtocolVersion getRestriction() { result = restriction } } +/** Whether `part` evaluates to `partIsTrue` if `whole` evaluates to `wholeIsTrue`. */ +predicate impliesValue(BinaryExpr whole, Expr part, boolean partIsTrue, boolean wholeIsTrue) { + whole.getOp() instanceof BitAnd and + ( + wholeIsTrue = true and partIsTrue = true and part in [whole.getLeft(), whole.getRight()] + or + wholeIsTrue = true and + impliesValue([whole.getLeft(), whole.getRight()], part, partIsTrue, wholeIsTrue) + ) + or + whole.getOp() instanceof BitOr and + ( + wholeIsTrue = false and partIsTrue = false and part in [whole.getLeft(), whole.getRight()] + or + wholeIsTrue = false and + impliesValue([whole.getLeft(), whole.getRight()], part, partIsTrue, wholeIsTrue) + ) +} + class ContextSetVersion extends ProtocolRestriction { string restriction; From cbbc7b2bcd160bdfc268252644f35daf4c1bcfd9 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Wed, 3 Mar 2021 23:42:48 +0100 Subject: [PATCH 0024/1429] Python: support unrestrictions Also pyOpenSSL allows SSL 2 and SSL 3 on `SSLv23` --- .../src/Security/CWE-327/FluentApiModel.qll | 62 ++++++++-------- .../src/Security/CWE-327/InsecureProtocol.ql | 28 ++++++-- python/ql/src/Security/CWE-327/PyOpenSSL.qll | 18 +++-- python/ql/src/Security/CWE-327/Ssl.qll | 63 ++++++++++++---- .../src/Security/CWE-327/TlsLibraryModel.qll | 48 +++++++++++-- .../CWE-327/InsecureProtocol.expected | 72 ++++++++++--------- 6 files changed, 201 insertions(+), 90 deletions(-) diff --git a/python/ql/src/Security/CWE-327/FluentApiModel.qll b/python/ql/src/Security/CWE-327/FluentApiModel.qll index 23922a90595..d222af70499 100644 --- a/python/ql/src/Security/CWE-327/FluentApiModel.qll +++ b/python/ql/src/Security/CWE-327/FluentApiModel.qll @@ -8,40 +8,44 @@ import TlsLibraryModel */ class InsecureContextConfiguration extends DataFlow::Configuration { TlsLibrary library; + ProtocolVersion tracked_version; - InsecureContextConfiguration() { this = library + ["AllowsTLSv1", "AllowsTLSv1_1"] } + InsecureContextConfiguration() { + this = library + "Allows" + tracked_version and + tracked_version.isInsecure() + } + + ProtocolVersion getTrackedVersion() { result = tracked_version } override predicate isSource(DataFlow::Node source) { - source = library.unspecific_context_creation() + // source = library.unspecific_context_creation() + exists(ProtocolUnrestriction pu | + pu = library.protocol_unrestriction() and + pu.getUnrestriction() = tracked_version + | + source = pu.getContext() + ) } override predicate isSink(DataFlow::Node sink) { sink = library.connection_creation().getContext() } - abstract string flag(); - override predicate isBarrierOut(DataFlow::Node node) { exists(ProtocolRestriction r | r = library.protocol_restriction() and node = r.getContext() and - r.getRestriction() = flag() + r.getRestriction() = tracked_version ) } -} -/** Configuration to specifically track the insecure protocol TLS 1.0 */ -class AllowsTLSv1 extends InsecureContextConfiguration { - AllowsTLSv1() { this = library + "AllowsTLSv1" } - - override string flag() { result = "TLSv1" } -} - -/** Configuration to specifically track the insecure protocol TLS 1.1 */ -class AllowsTLSv1_1 extends InsecureContextConfiguration { - AllowsTLSv1_1() { this = library + "AllowsTLSv1_1" } - - override string flag() { result = "TLSv1_1" } + override predicate isBarrierIn(DataFlow::Node node) { + exists(ProtocolUnrestriction r | + r = library.protocol_unrestriction() and + node = r.getContext() and + r.getUnrestriction() = tracked_version + ) + } } /** @@ -49,22 +53,22 @@ class AllowsTLSv1_1 extends InsecureContextConfiguration { * and that protocol has not been restricted appropriately. */ predicate unsafe_connection_creation( - DataFlow::Node node, ProtocolVersion insecure_version, CallNode call + DataFlow::Node creation, ProtocolVersion insecure_version, DataFlow::Node source, boolean specific ) { - // Connection created from a context allowing TLS 1.0. - exists(AllowsTLSv1 c, ContextCreation cc | c.hasFlow(cc, node) | cc.getNode() = call) and - insecure_version = "TLSv1" + // Connection created from a context allowing `insecure_version`. + exists(InsecureContextConfiguration c, ProtocolUnrestriction cc | c.hasFlow(cc, creation) | + insecure_version = c.getTrackedVersion() and + source = cc and + specific = false + ) or - // Connection created from a context allowing TLS 1.1. - exists(AllowsTLSv1_1 c, ContextCreation cc | c.hasFlow(cc, node) | cc.getNode() = call) and - insecure_version = "TLSv1_1" - or - // Connection created from a context for an insecure protocol. + // Connection created from a context specifying `insecure_version`. exists(TlsLibrary l, DataFlow::CfgNode cc | cc = l.insecure_connection_creation(insecure_version) | - cc = node and - cc.getNode() = call + creation = cc and + source = cc and + specific = true ) } diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index 51247dbd75a..194cc1f5ec1 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -18,11 +18,25 @@ string callName(AstNode call) { exists(Attribute a | a = call | result = callName(a.getObject()) + "." + a.getName()) } -from DataFlow::Node node, string insecure_version, CallNode call -where - unsafe_connection_creation(node, insecure_version, call) +string sourceName(DataFlow::Node source) { + result = "call to " + callName(source.asCfgNode().(CallNode).getFunction().getNode()) or - unsafe_context_creation(node, insecure_version, call) -select node, "Insecure SSL/TLS protocol version " + insecure_version + " specified in $@ ", call, - "call to " + callName(call.getFunction().getNode()) -//+ " specified in call to " + method_name + "." + not source.asCfgNode() instanceof CallNode and + not source instanceof ContextCreation and + result = "context modification" +} + +string verb(boolean specific) { + specific = true and result = "specified" + or + specific = false and result = "allowed" +} + +from DataFlow::Node creation, string insecure_version, DataFlow::Node source, boolean specific +where + unsafe_connection_creation(creation, insecure_version, source, specific) + or + unsafe_context_creation(creation, insecure_version, source.asCfgNode()) and specific = true +select creation, + "Insecure SSL/TLS protocol version " + insecure_version + " " + verb(specific) + " by $@ ", + source, sourceName(source) diff --git a/python/ql/src/Security/CWE-327/PyOpenSSL.qll b/python/ql/src/Security/CWE-327/PyOpenSSL.qll index a89c7ff0886..3c36c568ef5 100644 --- a/python/ql/src/Security/CWE-327/PyOpenSSL.qll +++ b/python/ql/src/Security/CWE-327/PyOpenSSL.qll @@ -26,6 +26,8 @@ class ConnectionCall extends ConnectionCreation { } } +// This cannot be used to unrestrict, +// see https://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Context.set_options class SetOptionsCall extends ProtocolRestriction { override CallNode node; @@ -42,6 +44,10 @@ class SetOptionsCall extends ProtocolRestriction { } } +class UnspecificPyOpenSSLContextCreation extends PyOpenSSLContextCreation, UnspecificContextCreation { + UnspecificPyOpenSSLContextCreation() { library = "pyOpenSSL" } +} + class PyOpenSSL extends TlsLibrary { PyOpenSSL() { this = "pyOpenSSL" } @@ -50,11 +56,9 @@ class PyOpenSSL extends TlsLibrary { result = version + "_METHOD" } - override string unspecific_version_name() { - result in [ - "TLS_METHOD", // This is not actually available in pyOpenSSL yet - "SSLv23_METHOD" // This is what can negotiate TLS 1.3 (indeed, I know, I did test that..) - ] + override string unspecific_version_name(ProtocolFamily family) { + // `"TLS_METHOD"` is not actually available in pyOpenSSL yet, but should be coming soon.. + result = family + "_METHOD" } override API::Node version_constants() { result = API::moduleImport("OpenSSL").getMember("SSL") } @@ -70,4 +74,8 @@ class PyOpenSSL extends TlsLibrary { override ConnectionCreation connection_creation() { result instanceof ConnectionCall } override ProtocolRestriction protocol_restriction() { result instanceof SetOptionsCall } + + override ProtocolUnrestriction protocol_unrestriction() { + result instanceof UnspecificPyOpenSSLContextCreation + } } diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index 4caa0ae7302..e749bb9bc3c 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -56,6 +56,31 @@ class OptionsAugOr extends ProtocolRestriction { override ProtocolVersion getRestriction() { result = restriction } } +class OptionsAugAndNot extends ProtocolUnrestriction { + ProtocolVersion restriction; + + OptionsAugAndNot() { + exists(AugAssign aa, AttrNode attr, Expr flag, UnaryExpr notFlag | + aa.getOperation().getOp() instanceof BitAnd and + aa.getTarget() = attr.getNode() and + attr.getName() = "options" and + attr.getObject() = node and + notFlag.getOp() instanceof Invert and + notFlag.getOperand() = flag and + flag = API::moduleImport("ssl").getMember("OP_NO_" + restriction).getAUse().asExpr() and + ( + aa.getValue() = notFlag + or + impliesValue(aa.getValue(), notFlag, true, true) + ) + ) + } + + override DataFlow::CfgNode getContext() { result = this } + + override ProtocolVersion getUnrestriction() { result = restriction } +} + /** Whether `part` evaluates to `partIsTrue` if `whole` evaluates to `wholeIsTrue`. */ predicate impliesValue(BinaryExpr whole, Expr part, boolean partIsTrue, boolean wholeIsTrue) { whole.getOp() instanceof BitAnd and @@ -75,8 +100,8 @@ predicate impliesValue(BinaryExpr whole, Expr part, boolean partIsTrue, boolean ) } -class ContextSetVersion extends ProtocolRestriction { - string restriction; +class ContextSetVersion extends ProtocolRestriction, ProtocolUnrestriction { + ProtocolVersion restriction; ContextSetVersion() { exists(Attributes::AttrWrite aw | @@ -90,6 +115,21 @@ class ContextSetVersion extends ProtocolRestriction { override DataFlow::CfgNode getContext() { result = this } override ProtocolVersion getRestriction() { result.lessThan(restriction) } + + override ProtocolVersion getUnrestriction() { + restriction = result or restriction.lessThan(result) + } +} + +class UnspecificSSLContextCreation extends SSLContextCreation, UnspecificContextCreation { + UnspecificSSLContextCreation() { library = "ssl" } + + override ProtocolVersion getUnrestriction() { + result = UnspecificContextCreation.super.getUnrestriction() and + // These are turned off by default + // see https://docs.python.org/3/library/ssl.html#ssl-contexts + not result in ["SSLv2", "SSLv3"] + } } class Ssl extends TlsLibrary { @@ -100,16 +140,7 @@ class Ssl extends TlsLibrary { result = "PROTOCOL_" + version } - override string unspecific_version_name() { - result = - "PROTOCOL_" + - [ - "TLS", - // This can negotiate a TLS 1.3 connection (!) - // see https://docs.python.org/3/library/ssl.html#ssl-contexts - "SSLv23" - ] - } + override string unspecific_version_name(ProtocolFamily family) { result = "PROTOCOL_" + family } override API::Node version_constants() { result = API::moduleImport("ssl") } @@ -132,4 +163,12 @@ class Ssl extends TlsLibrary { or result instanceof ContextSetVersion } + + override ProtocolUnrestriction protocol_unrestriction() { + result instanceof OptionsAugAndNot + or + result instanceof ContextSetVersion + or + result instanceof UnspecificSSLContextCreation + } } diff --git a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll index 36e58acd926..97ceec00688 100644 --- a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll +++ b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll @@ -19,6 +19,13 @@ class ProtocolVersion extends string { or this = ["TLSv1", "TLSv1_1", "TLSv1_2"] and version = "TLSv1_3" } + + predicate isInsecure() { this in ["SSLv2", "SSLv3", "TLSv1", "TLSv1_1"] } +} + +/** An unspecific protocol version */ +class ProtocolFamily extends string { + ProtocolFamily() { this in ["SSLv23", "TLS"] } } /** The creation of a context. */ @@ -42,6 +49,34 @@ abstract class ProtocolRestriction extends DataFlow::CfgNode { abstract ProtocolVersion getRestriction(); } +/** A context is being relaxed on which protocols it can accepts. */ +abstract class ProtocolUnrestriction extends DataFlow::CfgNode { + /** Gets the context being relaxed. */ + abstract DataFlow::CfgNode getContext(); + + /** Gets the protocol version being allowed. */ + abstract ProtocolVersion getUnrestriction(); +} + +abstract class UnspecificContextCreation extends ContextCreation, ProtocolUnrestriction { + TlsLibrary library; + ProtocolFamily family; + + UnspecificContextCreation() { this.getProtocol() = library.unspecific_version(family) } + + override DataFlow::CfgNode getContext() { result = this } + + override ProtocolVersion getUnrestriction() { + family = "TLS" and + result in ["TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] + or + // This can negotiate a TLS 1.3 connection (!) + // see https://docs.python.org/3/library/ssl.html#ssl-contexts + family = "SSLv23" and + result in ["SSLv2", "SSLv3", "TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] + } +} + abstract class TlsLibrary extends string { TlsLibrary() { this in ["ssl", "pyOpenSSL"] } @@ -49,7 +84,7 @@ abstract class TlsLibrary extends string { abstract string specific_insecure_version_name(ProtocolVersion version); /** The name of an unspecific protocol version, say TLS, known to have insecure instances. */ - abstract string unspecific_version_name(); + abstract string unspecific_version_name(ProtocolFamily family); /** The module or class holding the version constants. */ abstract API::Node version_constants(); @@ -60,8 +95,8 @@ abstract class TlsLibrary extends string { } /** A dataflow node representing an unspecific protocol version, say TLS, known to have insecure instances. */ - DataFlow::Node unspecific_version() { - result = version_constants().getMember(unspecific_version_name()).getAUse() + DataFlow::Node unspecific_version(ProtocolFamily family) { + result = version_constants().getMember(unspecific_version_name(family)).getAUse() } /** The creation of a context with a deafult protocol. */ @@ -77,11 +112,11 @@ abstract class TlsLibrary extends string { } /** The creation of a context with an unspecific protocol version, say TLS, known to have insecure instances. */ - DataFlow::CfgNode unspecific_context_creation() { + DataFlow::CfgNode unspecific_context_creation(ProtocolFamily family) { result = default_context_creation() or result = specific_context_creation() and - result.(ContextCreation).getProtocol() = unspecific_version() + result.(ContextCreation).getProtocol() = unspecific_version(family) } /** A connection is created in an insecure manner, not from a context. */ @@ -92,4 +127,7 @@ abstract class TlsLibrary extends string { /** A context is being restricted on which protocols it can accepts. */ abstract ProtocolRestriction protocol_restriction(); + + /** A context is being relaxed on which protocols it can accepts. */ + abstract ProtocolUnrestriction protocol_unrestriction(); } diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected index e116662ce65..afd9cc15d9f 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected @@ -1,32 +1,40 @@ -| InsecureProtocol.py:6:1:6:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:6:1:6:47 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | -| InsecureProtocol.py:7:1:7:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv3 specified in $@ | InsecureProtocol.py:7:1:7:47 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | -| InsecureProtocol.py:8:1:8:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version TLSv1 specified in $@ | InsecureProtocol.py:8:1:8:47 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | -| InsecureProtocol.py:10:1:10:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:10:1:10:39 | ControlFlowNode for SSLContext() | call to SSLContext | -| InsecureProtocol.py:11:1:11:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv3 specified in $@ | InsecureProtocol.py:11:1:11:39 | ControlFlowNode for SSLContext() | call to SSLContext | -| InsecureProtocol.py:12:1:12:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version TLSv1 specified in $@ | InsecureProtocol.py:12:1:12:39 | ControlFlowNode for SSLContext() | call to SSLContext | -| InsecureProtocol.py:14:1:14:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:14:1:14:29 | ControlFlowNode for Attribute() | call to SSL.Context | -| InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv3 specified in $@ | InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | call to SSL.Context | -| InsecureProtocol.py:17:1:17:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version TLSv1 specified in $@ | InsecureProtocol.py:17:1:17:29 | ControlFlowNode for Attribute() | call to SSL.Context | -| InsecureProtocol.py:32:1:32:19 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:32:1:32:19 | ControlFlowNode for Attribute() | call to SSL.Context | -| InsecureProtocol.py:48:1:48:43 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:48:1:48:43 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | -| InsecureProtocol.py:49:1:49:35 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv2 specified in $@ | InsecureProtocol.py:49:1:49:35 | ControlFlowNode for SSLContext() | call to SSLContext | -| pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | -| pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | -| pyOpenSSL_fluent.py:18:27:18:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | pyOpenSSL_fluent.py:15:15:15:44 | ControlFlowNode for Attribute() | call to SSL.Context | -| ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:19:14:19:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:15:15:15:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:47:14:47:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:43:15:43:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:75:14:75:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:71:15:71:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:75:14:75:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:71:15:71:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:128:15:128:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:128:15:128:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:104:14:104:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:104:14:104:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:173:14:173:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:169:15:169:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 specified in $@ | ssl_fluent.py:188:15:188:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | -| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 specified in $@ | ssl_fluent.py:188:15:188:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | +| InsecureProtocol.py:6:1:6:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:6:1:6:47 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | +| InsecureProtocol.py:7:1:7:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv3 specified by $@ | InsecureProtocol.py:7:1:7:47 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | +| InsecureProtocol.py:8:1:8:47 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version TLSv1 specified by $@ | InsecureProtocol.py:8:1:8:47 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | +| InsecureProtocol.py:10:1:10:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:10:1:10:39 | ControlFlowNode for SSLContext() | call to SSLContext | +| InsecureProtocol.py:11:1:11:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv3 specified by $@ | InsecureProtocol.py:11:1:11:39 | ControlFlowNode for SSLContext() | call to SSLContext | +| InsecureProtocol.py:12:1:12:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version TLSv1 specified by $@ | InsecureProtocol.py:12:1:12:39 | ControlFlowNode for SSLContext() | call to SSLContext | +| InsecureProtocol.py:14:1:14:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:14:1:14:29 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv3 specified by $@ | InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:17:1:17:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version TLSv1 specified by $@ | InsecureProtocol.py:17:1:17:29 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:32:1:32:19 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:32:1:32:19 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:48:1:48:43 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:48:1:48:43 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | +| InsecureProtocol.py:49:1:49:35 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:49:1:49:35 | ControlFlowNode for SSLContext() | call to SSLContext | +| pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv2 allowed by $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:18:27:18:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv2 allowed by $@ | pyOpenSSL_fluent.py:15:15:15:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:18:27:18:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | pyOpenSSL_fluent.py:15:15:15:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:18:27:18:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | pyOpenSSL_fluent.py:15:15:15:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:29:27:29:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv2 allowed by $@ | pyOpenSSL_fluent.py:25:15:25:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| pyOpenSSL_fluent.py:29:27:29:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | pyOpenSSL_fluent.py:25:15:25:44 | ControlFlowNode for Attribute() | call to SSL.Context | +| ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:19:14:19:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:15:15:15:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:47:14:47:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:43:15:43:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:75:14:75:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:71:15:71:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:75:14:75:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:71:15:71:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:128:15:128:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:128:15:128:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:144:5:144:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:162:5:162:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:104:14:104:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:104:14:104:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:124:14:124:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:122:5:122:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:173:14:173:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:170:5:170:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | ssl_fluent.py:189:5:189:11 | ControlFlowNode for context | context modification | From ee038373571b4d3fe2490c2fd19eaeb9afbbfa49 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Wed, 3 Mar 2021 23:46:18 +0100 Subject: [PATCH 0025/1429] Python: small refactor --- python/ql/src/Security/CWE-327/PyOpenSSL.qll | 5 +---- python/ql/src/Security/CWE-327/Ssl.qll | 5 +---- python/ql/src/Security/CWE-327/TlsLibraryModel.qll | 7 ++++--- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/python/ql/src/Security/CWE-327/PyOpenSSL.qll b/python/ql/src/Security/CWE-327/PyOpenSSL.qll index 3c36c568ef5..d2cfb74b3ab 100644 --- a/python/ql/src/Security/CWE-327/PyOpenSSL.qll +++ b/python/ql/src/Security/CWE-327/PyOpenSSL.qll @@ -51,10 +51,7 @@ class UnspecificPyOpenSSLContextCreation extends PyOpenSSLContextCreation, Unspe class PyOpenSSL extends TlsLibrary { PyOpenSSL() { this = "pyOpenSSL" } - override string specific_insecure_version_name(ProtocolVersion version) { - version in ["SSLv2", "SSLv3", "TLSv1", "TLSv1_1"] and - result = version + "_METHOD" - } + override string specific_version_name(ProtocolVersion version) { result = version + "_METHOD" } override string unspecific_version_name(ProtocolFamily family) { // `"TLS_METHOD"` is not actually available in pyOpenSSL yet, but should be coming soon.. diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index e749bb9bc3c..66a821e83ac 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -135,10 +135,7 @@ class UnspecificSSLContextCreation extends SSLContextCreation, UnspecificContext class Ssl extends TlsLibrary { Ssl() { this = "ssl" } - override string specific_insecure_version_name(ProtocolVersion version) { - version in ["SSLv2", "SSLv3", "TLSv1", "TLSv1_1"] and - result = "PROTOCOL_" + version - } + override string specific_version_name(ProtocolVersion version) { result = "PROTOCOL_" + version } override string unspecific_version_name(ProtocolFamily family) { result = "PROTOCOL_" + family } diff --git a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll index 97ceec00688..3ab880e8bd9 100644 --- a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll +++ b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll @@ -80,8 +80,8 @@ abstract class UnspecificContextCreation extends ContextCreation, ProtocolUnrest abstract class TlsLibrary extends string { TlsLibrary() { this in ["ssl", "pyOpenSSL"] } - /** The name of a specific protocol version, known to be insecure. */ - abstract string specific_insecure_version_name(ProtocolVersion version); + /** The name of a specific protocol version. */ + abstract string specific_version_name(ProtocolVersion version); /** The name of an unspecific protocol version, say TLS, known to have insecure instances. */ abstract string unspecific_version_name(ProtocolFamily family); @@ -91,7 +91,8 @@ abstract class TlsLibrary extends string { /** A dataflow node representing a specific protocol version, known to be insecure. */ DataFlow::Node insecure_version(ProtocolVersion version) { - result = version_constants().getMember(specific_insecure_version_name(version)).getAUse() + version.isInsecure() and + result = version_constants().getMember(specific_version_name(version)).getAUse() } /** A dataflow node representing an unspecific protocol version, say TLS, known to have insecure instances. */ From de9469bbfc1769e6e705165b6e53efa12a0c97eb Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Thu, 4 Mar 2021 00:01:44 +0100 Subject: [PATCH 0026/1429] Python: complete `ssl.create_default_context` --- python/ql/src/Security/CWE-327/Ssl.qll | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index 66a821e83ac..d4886219d08 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -132,6 +132,15 @@ class UnspecificSSLContextCreation extends SSLContextCreation, UnspecificContext } } +class UnspecificSSLDefaultContextCreation extends SSLDefaultContextCreation, ProtocolUnrestriction { + override DataFlow::CfgNode getContext() { result = this } + + // see https://docs.python.org/3/library/ssl.html#ssl.create_default_context + override ProtocolVersion getUnrestriction() { + result in ["TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] + } +} + class Ssl extends TlsLibrary { Ssl() { this = "ssl" } @@ -167,5 +176,7 @@ class Ssl extends TlsLibrary { result instanceof ContextSetVersion or result instanceof UnspecificSSLContextCreation + or + result instanceof UnspecificSSLDefaultContextCreation } } From d02c5298725695e1fe3dc663922619a7f0a8b35d Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Thu, 4 Mar 2021 00:06:36 +0100 Subject: [PATCH 0027/1429] Python: Update annotation --- python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py index eceacaebe21..ab65e3bd206 100644 --- a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py +++ b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py @@ -186,7 +186,7 @@ def test_fluent_ssl_safe_version(): def test_fluent_explicitly_unsafe(): hostname = 'www.python.org' context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - context.options &= ~ssl.OP_NO_SSLv3 # This not recognized + context.options &= ~ssl.OP_NO_SSLv3 with socket.create_connection((hostname, 443)) as sock: with context.wrap_socket(sock, server_hostname=hostname) as ssock: # SSLv3 not flagged here From c5577cb09a073c2fb4f6feeca887c437d5fbe2fb Mon Sep 17 00:00:00 2001 From: haby0 Date: Thu, 4 Mar 2021 19:54:49 +0800 Subject: [PATCH 0028/1429] Fix the problem --- .../Security/CWE/CWE-352/JsonpInjection.ql | 68 -------- .../CWE/CWE-352/JsonpInjectionFilterLib.qll | 77 --------- .../CWE/CWE-352/JsonpInjectionLib.qll | 155 ------------------ .../Security/CWE/CWE-352/JsonStringLib.qll | 0 .../CWE/CWE-352/JsonpController.java} | 48 +----- .../Security/CWE/CWE-352/JsonpInjection.java | 0 .../Security/CWE/CWE-352/JsonpInjection.qhelp | 2 +- .../Security/CWE/CWE-352/JsonpInjection.ql | 64 ++++++++ .../CWE/CWE-352/JsonpInjectionLib.qll | 130 +++++++++++++++ .../CWE/CWE-352/JsonpInjectionServlet1.java | 0 .../CWE/CWE-352/JsonpInjectionServlet2.java | 0 .../Security/CWE/CWE-352/RefererFilter.java | 43 +++++ .../semmle/code/java/frameworks/Servlets.qll | 12 +- .../security/CWE-352/JsonpController.java | 128 +++++++++++++++ .../security/CWE-352/JsonpInjection.expected | 60 ------- .../CWE-352/JsonpInjectionServlet1.java | 64 ++++++++ .../CWE-352/JsonpInjectionServlet2.java} | 16 +- .../CWE-352/JsonpInjection_1.expected | 60 +++++++ .../CWE-352/JsonpInjection_2.expected | 78 +++++++++ .../CWE-352/JsonpInjection_3.expected | 66 ++++++++ .../query-tests/security/CWE-352/Readme | 3 + .../security/CWE-352/RefererFilter.java | 43 +++++ .../gson-2.8.6/com/google/gson/Gson.java | 7 + .../org/springframework/util/StringUtils.java | 8 + .../javax/servlet/Filter.java | 13 ++ .../javax/servlet/FilterChain.java | 8 + .../javax/servlet/FilterConfig.java | 3 + .../javax/servlet/ServletException.java | 8 + .../javax/servlet/ServletRequest.java | 87 ++++++++++ .../javax/servlet/ServletResponse.java | 39 +++++ .../servlet/http/HttpServletRequest.java | 116 +++++++++++++ .../servlet/http/HttpServletResponse.java | 106 ++++++++++++ 32 files changed, 1084 insertions(+), 428 deletions(-) delete mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql delete mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjectionFilterLib.qll delete mode 100644 java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll rename java/ql/src/{ => experimental}/Security/CWE/CWE-352/JsonStringLib.qll (100%) rename java/ql/{test/experimental/query-tests/security/CWE-352/JsonpInjection.java => src/experimental/Security/CWE/CWE-352/JsonpController.java} (72%) rename java/ql/src/{ => experimental}/Security/CWE/CWE-352/JsonpInjection.java (100%) rename java/ql/src/{ => experimental}/Security/CWE/CWE-352/JsonpInjection.qhelp (96%) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll rename java/ql/src/{ => experimental}/Security/CWE/CWE-352/JsonpInjectionServlet1.java (100%) rename java/ql/src/{ => experimental}/Security/CWE/CWE-352/JsonpInjectionServlet2.java (100%) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-352/RefererFilter.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpController.java delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet1.java rename java/ql/{src/Security/CWE/CWE-352/JsonpInjectionServlet.java => test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet2.java} (75%) create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_1.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_2.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_3.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/Readme create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/RefererFilter.java create mode 100644 java/ql/test/stubs/gson-2.8.6/com/google/gson/Gson.java create mode 100644 java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/StringUtils.java create mode 100644 java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/Filter.java create mode 100644 java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/FilterChain.java create mode 100644 java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/FilterConfig.java create mode 100644 java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletException.java create mode 100644 java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletRequest.java create mode 100644 java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletResponse.java create mode 100644 java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/http/HttpServletRequest.java create mode 100644 java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/http/HttpServletResponse.java diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql b/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql deleted file mode 100644 index 53ee6182511..00000000000 --- a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.ql +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @name JSONP Injection - * @description User-controlled callback function names that are not verified are vulnerable - * to jsonp injection attacks. - * @kind path-problem - * @problem.severity error - * @precision high - * @id java/JSONP-Injection - * @tags security - * external/cwe/cwe-352 - */ - -import java -import JsonpInjectionLib -import JsonpInjectionFilterLib -import semmle.code.java.dataflow.FlowSources -import semmle.code.java.deadcode.WebEntryPoints -import DataFlow::PathGraph - - -/** If there is a method to verify `token`, `auth`, `referer`, and `origin`, it will not pass. */ -class ServletVerifAuth extends DataFlow::BarrierGuard { - ServletVerifAuth() { - exists(MethodAccess ma, Node prod, Node succ | - ma.getMethod().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and - prod instanceof RemoteFlowSource and - succ.asExpr() = ma.getAnArgument() and - ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and - localFlowStep*(prod, succ) and - this = ma - ) - } - - override predicate checks(Expr e, boolean branch) { - exists(Node node | - node instanceof JsonpInjectionSink and - e = node.asExpr() and - branch = true - ) - } -} - -/** Taint-tracking configuration tracing flow from get method request sources to output jsonp data. */ -class JsonpInjectionConfig extends TaintTracking::Configuration { - JsonpInjectionConfig() { this = "JsonpInjectionConfig" } - - override predicate isSource(DataFlow::Node source) { source instanceof GetHttpRequestSource } - - override predicate isSink(DataFlow::Node sink) { sink instanceof JsonpInjectionSink } - - override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { - guard instanceof ServletVerifAuth - } - - override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { - exists(MethodAccess ma | - isRequestGetParamMethod(ma) and pred.asExpr() = ma.getQualifier() and succ.asExpr() = ma - ) - } -} - -from DataFlow::PathNode source, DataFlow::PathNode sink, JsonpInjectionConfig conf -where - not checks() = false and - conf.hasFlowPath(source, sink) and - exists(JsonpInjectionFlowConfig jhfc | jhfc.hasFlowTo(sink.getNode())) -select sink.getNode(), source, sink, "Jsonp Injection query might include code from $@.", - source.getNode(), "this user input" diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionFilterLib.qll b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionFilterLib.qll deleted file mode 100644 index b349bed2641..00000000000 --- a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionFilterLib.qll +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @name JSONP Injection - * @description User-controlled callback function names that are not verified are vulnerable - * to json hijacking attacks. - * @kind path-problem - */ - -import java -import DataFlow -import semmle.code.java.dataflow.FlowSources -import semmle.code.java.dataflow.TaintTracking2 -import DataFlow::PathGraph - -class FilterVerifAuth extends DataFlow::BarrierGuard { - FilterVerifAuth() { - exists(MethodAccess ma, Node prod, Node succ | - ma.getMethod().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and - prod instanceof RemoteFlowSource and - succ.asExpr() = ma.getAnArgument() and - ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and - localFlowStep*(prod, succ) and - this = ma - ) - } - - override predicate checks(Expr e, boolean branch) { - exists(Node node | - node instanceof DoFilterMethodSink and - e = node.asExpr() and - branch = true - ) - } -} - -/** A data flow source for `Filter.doFilter` method paramters. */ -private class DoFilterMethodSource extends DataFlow::Node { - DoFilterMethodSource() { - exists(Method m | - isDoFilterMethod(m) and - m.getAParameter().getAnAccess() = this.asExpr() - ) - } -} - -/** A data flow sink for `FilterChain.doFilter` method qualifying expression. */ -private class DoFilterMethodSink extends DataFlow::Node { - DoFilterMethodSink() { - exists(MethodAccess ma, Method m | ma.getMethod() = m | - m.hasName("doFilter") and - m.getDeclaringType*().hasQualifiedName("javax.servlet", "FilterChain") and - ma.getQualifier() = this.asExpr() - ) - } -} - -/** Taint-tracking configuration tracing flow from `doFilter` method paramter source to output - * `FilterChain.doFilter` method qualifying expression. - * */ -class DoFilterMethodConfig extends TaintTracking::Configuration { - DoFilterMethodConfig() { this = "DoFilterMethodConfig" } - - override predicate isSource(DataFlow::Node source) { source instanceof DoFilterMethodSource } - - override predicate isSink(DataFlow::Node sink) { sink instanceof DoFilterMethodSink } - - override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { - guard instanceof FilterVerifAuth - } -} - -/** Implement class modeling verification for `Filter.doFilter`, return false if it fails. */ -boolean checks() { - exists(DataFlow::PathNode source, DataFlow::PathNode sink, DoFilterMethodConfig conf | - conf.hasFlowPath(source, sink) and - result = false - ) -} diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll b/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll deleted file mode 100644 index 3f730425823..00000000000 --- a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionLib.qll +++ /dev/null @@ -1,155 +0,0 @@ -import java -import DataFlow -import JsonStringLib -import semmle.code.java.dataflow.DataFlow -import semmle.code.java.dataflow.FlowSources -import semmle.code.java.frameworks.spring.SpringController - -/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ -private predicate isGetServletMethod(Method m) { - isServletRequestMethod(m) and m.getName() = "doGet" -} - -/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ -private predicate isGetSpringControllerMethod(Method m) { - exists(Annotation a | - a = m.getAnAnnotation() and - a.getType().hasQualifiedName("org.springframework.web.bind.annotation", "GetMapping") - ) - or - exists(Annotation a | - a = m.getAnAnnotation() and - a.getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestMapping") and - a.getValue("method").toString().regexpMatch("RequestMethod.GET|\\{...\\}") - ) -} - -/** Method parameters use the annotation `@RequestParam` or the parameter type is `ServletRequest`, `String`, `Object` */ -predicate checkSpringMethodParameterType(Method m, int i) { - m.getParameter(i).getType() instanceof ServletRequest - or - exists(Parameter p | - p = m.getParameter(i) and - p.hasAnnotation() and - p.getAnAnnotation() - .getType() - .hasQualifiedName("org.springframework.web.bind.annotation", "RequestParam") and - p.getType().getName().regexpMatch("String|Object") - ) - or - exists(Parameter p | - p = m.getParameter(i) and - not p.hasAnnotation() and - p.getType().getName().regexpMatch("String|Object") - ) -} - -/** A data flow source for get method request parameters. */ -abstract class GetHttpRequestSource extends DataFlow::Node { } - -/** A data flow source for servlet get method request parameters. */ -private class ServletGetHttpRequestSource extends GetHttpRequestSource { - ServletGetHttpRequestSource() { - exists(Method m | - isGetServletMethod(m) and - m.getParameter(0).getAnAccess() = this.asExpr() - ) - } -} - -/** A data flow source for spring controller get method request parameters. */ -private class SpringGetHttpRequestSource extends GetHttpRequestSource { - SpringGetHttpRequestSource() { - exists(SpringControllerMethod scm, int i | - isGetSpringControllerMethod(scm) and - checkSpringMethodParameterType(scm, i) and - scm.getParameter(i).getAnAccess() = this.asExpr() - ) - } -} - -/** A data flow sink for unvalidated user input that is used to jsonp. */ -abstract class JsonpInjectionSink extends DataFlow::Node { } - -/** Use ```print```, ```println```, ```write``` to output result. */ -private class WriterPrintln extends JsonpInjectionSink { - WriterPrintln() { - exists(MethodAccess ma | - ma.getMethod().getName().regexpMatch("print|println|write") and - ma.getMethod() - .getDeclaringType() - .getASourceSupertype*() - .hasQualifiedName("java.io", "PrintWriter") and - ma.getArgument(0) = this.asExpr() - ) - } -} - -/** Spring Request Method return result. */ -private class SpringReturn extends JsonpInjectionSink { - SpringReturn() { - exists(ReturnStmt rs, Method m | m = rs.getEnclosingCallable() | - isGetSpringControllerMethod(m) and - rs.getResult() = this.asExpr() - ) - } -} - -/** A concatenate expression using `(` and `)` or `);`. */ -class JsonpInjectionExpr extends AddExpr { - JsonpInjectionExpr() { - getRightOperand().toString().regexpMatch("\"\\)\"|\"\\);\"") and - getLeftOperand() - .(AddExpr) - .getLeftOperand() - .(AddExpr) - .getRightOperand() - .toString() - .regexpMatch("\"\\(\"") - } - - /** Get the jsonp function name of this expression */ - Expr getFunctionName() { - result = getLeftOperand().(AddExpr).getLeftOperand().(AddExpr).getLeftOperand() - } - - /** Get the json data of this expression */ - Expr getJsonExpr() { result = getLeftOperand().(AddExpr).getRightOperand() } -} - -/** A data flow configuration tracing flow from remote sources to jsonp function name. */ -class RemoteFlowConfig extends DataFlow2::Configuration { - RemoteFlowConfig() { this = "RemoteFlowConfig" } - - override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } - - override predicate isSink(DataFlow::Node sink) { - exists(JsonpInjectionExpr jhe | jhe.getFunctionName() = sink.asExpr()) - } -} - -/** A data flow configuration tracing flow from json data to splicing jsonp data. */ -class JsonDataFlowConfig extends DataFlow2::Configuration { - JsonDataFlowConfig() { this = "JsonDataFlowConfig" } - - override predicate isSource(DataFlow::Node src) { src instanceof JsonpStringSource } - - override predicate isSink(DataFlow::Node sink) { - exists(JsonpInjectionExpr jhe | jhe.getJsonExpr() = sink.asExpr()) - } -} - -/** Taint-tracking configuration tracing flow from user-controllable function name jsonp data to output jsonp data. */ -class JsonpInjectionFlowConfig extends DataFlow::Configuration { - JsonpInjectionFlowConfig() { this = "JsonpInjectionFlowConfig" } - - override predicate isSource(DataFlow::Node src) { - exists(JsonpInjectionExpr jhe, JsonDataFlowConfig jdfc, RemoteFlowConfig rfc | - jhe = src.asExpr() and - jdfc.hasFlowTo(DataFlow::exprNode(jhe.getJsonExpr())) and - rfc.hasFlowTo(DataFlow::exprNode(jhe.getFunctionName())) - ) - } - - override predicate isSink(DataFlow::Node sink) { sink instanceof JsonpInjectionSink } -} diff --git a/java/ql/src/Security/CWE/CWE-352/JsonStringLib.qll b/java/ql/src/experimental/Security/CWE/CWE-352/JsonStringLib.qll similarity index 100% rename from java/ql/src/Security/CWE/CWE-352/JsonStringLib.qll rename to java/ql/src/experimental/Security/CWE/CWE-352/JsonStringLib.qll diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpController.java similarity index 72% rename from java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java rename to java/ql/src/experimental/Security/CWE/CWE-352/JsonpController.java index 9f079513a8b..84a172a7aeb 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.java +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpController.java @@ -3,17 +3,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; import java.io.PrintWriter; import java.util.HashMap; -import java.util.Random; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @Controller -public class JsonpInjection { +public class JsonpController { private static HashMap hashMap = new HashMap(); static { @@ -96,54 +93,13 @@ public class JsonpInjection { @GetMapping(value = "jsonp7") @ResponseBody - public String good(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - - String val = ""; - Random random = new Random(); - for (int i = 0; i < 10; i++) { - val += String.valueOf(random.nextInt(10)); - } - // good - jsonpCallback = jsonpCallback + "_" + val; - String jsonStr = getJsonStr(hashMap); - resultStr = jsonpCallback + "(" + jsonStr + ")"; - return resultStr; - } - - @GetMapping(value = "jsonp8") - @ResponseBody public String good1(HttpServletRequest request) { String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); String token = request.getParameter("token"); - // good if (verifToken(token)){ - System.out.println(token); - String jsonStr = getJsonStr(hashMap); - resultStr = jsonpCallback + "(" + jsonStr + ")"; - return resultStr; - } - - return "error"; - } - - @GetMapping(value = "jsonp9") - @ResponseBody - public String good2(HttpServletRequest request) { - String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - - String referer = request.getHeader("Referer"); - - boolean result = verifReferer(referer); - - boolean test = result; - // good - if (test){ + String jsonpCallback = request.getParameter("jsonpCallback"); String jsonStr = getJsonStr(hashMap); resultStr = jsonpCallback + "(" + jsonStr + ")"; return resultStr; diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.java b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java similarity index 100% rename from java/ql/src/Security/CWE/CWE-352/JsonpInjection.java rename to java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp similarity index 96% rename from java/ql/src/Security/CWE/CWE-352/JsonpInjection.qhelp rename to java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp index b063b409d3a..bb5d628ac0b 100644 --- a/java/ql/src/Security/CWE/CWE-352/JsonpInjection.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp @@ -16,7 +16,7 @@ there is a problem of sensitive information leakage.

    The following example shows the case of no verification processing and verification processing for the external input function name.

    - + diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql new file mode 100644 index 00000000000..f3ae25daa03 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql @@ -0,0 +1,64 @@ +/** + * @name JSONP Injection + * @description User-controlled callback function names that are not verified are vulnerable + * to jsonp injection attacks. + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/JSONP-Injection + * @tags security + * external/cwe/cwe-352 + */ + +import java +import JsonpInjectionLib +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.deadcode.WebEntryPoints +import semmle.code.java.security.XSS +import DataFlow::PathGraph + +/** Determine whether there is a verification method for the remote streaming source data flow path method. */ +predicate existsFilterVerificationMethod() { + exists(MethodAccess ma,Node existsNode, Method m| + ma.getMethod() instanceof VerificationMethodClass and + existsNode.asExpr() = ma and + m = getAnMethod(existsNode.getEnclosingCallable()) and + isDoFilterMethod(m) + ) +} + +/** Determine whether there is a verification method for the remote streaming source data flow path method. */ +predicate existsServletVerificationMethod(Node checkNode) { + exists(MethodAccess ma,Node existsNode| + ma.getMethod() instanceof VerificationMethodClass and + existsNode.asExpr() = ma and + getAnMethod(existsNode.getEnclosingCallable()) = getAnMethod(checkNode.getEnclosingCallable()) + ) +} + +/** Taint-tracking configuration tracing flow from get method request sources to output jsonp data. */ +class RequestResponseFlowConfig extends TaintTracking::Configuration { + RequestResponseFlowConfig() { this = "RequestResponseFlowConfig" } + + override predicate isSource(DataFlow::Node source) { + source instanceof RemoteFlowSource and + getAnMethod(source.getEnclosingCallable()) instanceof RequestGetMethod + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof XssSink } + + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { + exists(MethodAccess ma | + isRequestGetParamMethod(ma) and pred.asExpr() = ma.getQualifier() and succ.asExpr() = ma + ) + } +} + +from DataFlow::PathNode source, DataFlow::PathNode sink, RequestResponseFlowConfig conf +where + not existsServletVerificationMethod(source.getNode()) and + not existsFilterVerificationMethod() and + conf.hasFlowPath(source, sink) and + exists(JsonpInjectionFlowConfig jhfc | jhfc.hasFlowTo(sink.getNode())) +select sink.getNode(), source, sink, "Jsonp Injection query might include code from $@.", + source.getNode(), "this user input" \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll new file mode 100644 index 00000000000..b8964524a9f --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll @@ -0,0 +1,130 @@ +import java +import DataFlow +import JsonStringLib +import semmle.code.java.security.XSS +import semmle.code.java.dataflow.DataFlow +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.frameworks.spring.SpringController + +/** Taint-tracking configuration tracing flow from user-controllable function name jsonp data to output jsonp data. */ +class VerificationMethodFlowConfig extends TaintTracking::Configuration { + VerificationMethodFlowConfig() { this = "VerificationMethodFlowConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + exists(MethodAccess ma, BarrierGuard bg | + ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and + bg = ma and + sink.asExpr() = ma.getAnArgument() + ) + } +} + +/** The parameter name of the method is `token`, `auth`, `referer`, `origin`. */ +class VerificationMethodClass extends Method { + VerificationMethodClass() { + exists(MethodAccess ma, BarrierGuard bg, VerificationMethodFlowConfig vmfc, Node node | + this = ma.getMethod() and + this.getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and + bg = ma and + node.asExpr() = ma.getAnArgument() and + vmfc.hasFlowTo(node) + ) + } +} + +/** Get Callable by recursive method. */ +Callable getAnMethod(Callable call) { + result = call + or + result = getAnMethod(call.getAReference().getEnclosingCallable()) +} + +abstract class RequestGetMethod extends Method { } + +/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ +private class ServletGetMethod extends RequestGetMethod { + ServletGetMethod() { + exists(Method m | + m = this and + isServletRequestMethod(m) and + m.getName() = "doGet" + ) + } +} + +/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ +private class SpringControllerGetMethod extends RequestGetMethod { + SpringControllerGetMethod() { + exists(Annotation a | + a = this.getAnAnnotation() and + a.getType().hasQualifiedName("org.springframework.web.bind.annotation", "GetMapping") + ) + or + exists(Annotation a | + a = this.getAnAnnotation() and + a.getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestMapping") and + a.getValue("method").toString().regexpMatch("RequestMethod.GET|\\{...\\}") + ) + } +} + +/** A concatenate expression using `(` and `)` or `);`. */ +class JsonpInjectionExpr extends AddExpr { + JsonpInjectionExpr() { + getRightOperand().toString().regexpMatch("\"\\)\"|\"\\);\"") and + getLeftOperand() + .(AddExpr) + .getLeftOperand() + .(AddExpr) + .getRightOperand() + .toString() + .regexpMatch("\"\\(\"") + } + + /** Get the jsonp function name of this expression */ + Expr getFunctionName() { + result = getLeftOperand().(AddExpr).getLeftOperand().(AddExpr).getLeftOperand() + } + + /** Get the json data of this expression */ + Expr getJsonExpr() { result = getLeftOperand().(AddExpr).getRightOperand() } +} + +/** A data flow configuration tracing flow from remote sources to jsonp function name. */ +class RemoteFlowConfig extends DataFlow2::Configuration { + RemoteFlowConfig() { this = "RemoteFlowConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + exists(JsonpInjectionExpr jhe | jhe.getFunctionName() = sink.asExpr()) + } +} + +/** A data flow configuration tracing flow from json data to splicing jsonp data. */ +class JsonDataFlowConfig extends DataFlow2::Configuration { + JsonDataFlowConfig() { this = "JsonDataFlowConfig" } + + override predicate isSource(DataFlow::Node src) { src instanceof JsonpStringSource } + + override predicate isSink(DataFlow::Node sink) { + exists(JsonpInjectionExpr jhe | jhe.getJsonExpr() = sink.asExpr()) + } +} + +/** Taint-tracking configuration tracing flow from user-controllable function name jsonp data to output jsonp data. */ +class JsonpInjectionFlowConfig extends TaintTracking::Configuration { + JsonpInjectionFlowConfig() { this = "JsonpInjectionFlowConfig" } + + override predicate isSource(DataFlow::Node src) { + exists(JsonpInjectionExpr jhe, JsonDataFlowConfig jdfc, RemoteFlowConfig rfc | + jhe = src.asExpr() and + jdfc.hasFlowTo(DataFlow::exprNode(jhe.getJsonExpr())) and + rfc.hasFlowTo(DataFlow::exprNode(jhe.getFunctionName())) + ) + } + + override predicate isSink(DataFlow::Node sink) { sink instanceof XssSink } +} diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet1.java b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionServlet1.java similarity index 100% rename from java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet1.java rename to java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionServlet1.java diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet2.java b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionServlet2.java similarity index 100% rename from java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet2.java rename to java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionServlet2.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/RefererFilter.java b/java/ql/src/experimental/Security/CWE/CWE-352/RefererFilter.java new file mode 100644 index 00000000000..97444932ae1 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-352/RefererFilter.java @@ -0,0 +1,43 @@ +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.util.StringUtils; + +public class RefererFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + String refefer = request.getHeader("Referer"); + boolean result = verifReferer(refefer); + if (result){ + filterChain.doFilter(servletRequest, servletResponse); + } + response.sendError(444, "Referer xxx."); + } + + @Override + public void destroy() { + } + + public static boolean verifReferer(String referer){ + if (StringUtils.isEmpty(referer)){ + return false; + } + if (referer.startsWith("http://www.baidu.com/")){ + return true; + } + return false; + } +} diff --git a/java/ql/src/semmle/code/java/frameworks/Servlets.qll b/java/ql/src/semmle/code/java/frameworks/Servlets.qll index b2054dc30cb..5cccf62122f 100644 --- a/java/ql/src/semmle/code/java/frameworks/Servlets.qll +++ b/java/ql/src/semmle/code/java/frameworks/Servlets.qll @@ -338,7 +338,6 @@ predicate isRequestGetParamMethod(MethodAccess ma) { ma.getMethod() instanceof HttpServletRequestGetQueryStringMethod } - /** * A class that has `javax.servlet.Filter` as an ancestor. */ @@ -346,21 +345,18 @@ class FilterClass extends Class { FilterClass() { getAnAncestor().hasQualifiedName("javax.servlet", "Filter") } } - /** * The interface `javax.servlet.FilterChain` */ -class FilterChain extends RefType { - FilterChain() { - hasQualifiedName("javax.servlet", "FilterChain") - } +class FilterChain extends Interface { + FilterChain() { hasQualifiedName("javax.servlet", "FilterChain") } } -/** Holds if `m` is a request handler method (for example `doGet` or `doPost`). */ +/** Holds if `m` is a filter handler method (for example `doFilter`). */ predicate isDoFilterMethod(Method m) { m.getDeclaringType() instanceof FilterClass and m.getNumberOfParameters() = 3 and m.getParameter(0).getType() instanceof ServletRequest and m.getParameter(1).getType() instanceof ServletResponse and m.getParameter(2).getType() instanceof FilterChain -} \ No newline at end of file +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpController.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpController.java new file mode 100644 index 00000000000..cf860c75640 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpController.java @@ -0,0 +1,128 @@ +import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import java.io.PrintWriter; +import java.util.HashMap; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +public class JsonpController { + private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + + @GetMapping(value = "jsonp1", produces="text/javascript") + @ResponseBody + public String bad1(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp2") + @ResponseBody + public String bad2(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + + resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; + + return resultStr; + } + + @GetMapping(value = "jsonp3") + @ResponseBody + public String bad3(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp4") + @ResponseBody + public String bad4(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @GetMapping(value = "jsonp5") + @ResponseBody + public void bad5(HttpServletRequest request, + HttpServletResponse response) throws Exception { + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp6") + @ResponseBody + public void bad6(HttpServletRequest request, + HttpServletResponse response) throws Exception { + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + ObjectMapper mapper = new ObjectMapper(); + String result = mapper.writeValueAsString(hashMap); + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp7") + @ResponseBody + public String good1(HttpServletRequest request) { + String resultStr = null; + + String token = request.getParameter("token"); + + if (verifToken(token)){ + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + return "error"; + } + + public static String getJsonStr(Object result) { + return JSONObject.toJSONString(result); + } + + public static boolean verifToken(String token){ + if (token != "xxxx"){ + return false; + } + return true; + } + + public static boolean verifReferer(String referer){ + if (!referer.startsWith("http://test.com/")){ + return false; + } + return true; + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected deleted file mode 100644 index 7e3069cf1d9..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.expected +++ /dev/null @@ -1,60 +0,0 @@ -edges -| JsonpInjection.java:29:32:29:38 | request : HttpServletRequest | JsonpInjection.java:34:16:34:24 | resultStr | -| JsonpInjection.java:33:21:33:54 | ... + ... : String | JsonpInjection.java:34:16:34:24 | resultStr | -| JsonpInjection.java:41:32:41:38 | request : HttpServletRequest | JsonpInjection.java:45:16:45:24 | resultStr | -| JsonpInjection.java:43:21:43:80 | ... + ... : String | JsonpInjection.java:45:16:45:24 | resultStr | -| JsonpInjection.java:52:32:52:38 | request : HttpServletRequest | JsonpInjection.java:55:16:55:24 | resultStr | -| JsonpInjection.java:54:21:54:55 | ... + ... : String | JsonpInjection.java:55:16:55:24 | resultStr | -| JsonpInjection.java:62:32:62:38 | request : HttpServletRequest | JsonpInjection.java:65:16:65:24 | resultStr | -| JsonpInjection.java:64:21:64:54 | ... + ... : String | JsonpInjection.java:65:16:65:24 | resultStr | -| JsonpInjection.java:72:32:72:38 | request : HttpServletRequest | JsonpInjection.java:80:20:80:28 | resultStr | -| JsonpInjection.java:79:21:79:54 | ... + ... : String | JsonpInjection.java:80:20:80:28 | resultStr | -| JsonpInjection.java:87:32:87:38 | request : HttpServletRequest | JsonpInjection.java:94:20:94:28 | resultStr | -| JsonpInjection.java:93:21:93:54 | ... + ... : String | JsonpInjection.java:94:20:94:28 | resultStr | -| JsonpInjection.java:101:32:101:38 | request : HttpServletRequest | JsonpInjection.java:112:16:112:24 | resultStr | -| JsonpInjection.java:127:25:127:59 | ... + ... : String | JsonpInjection.java:128:20:128:28 | resultStr | -| JsonpInjection.java:148:25:148:59 | ... + ... : String | JsonpInjection.java:149:20:149:28 | resultStr | -nodes -| JsonpInjection.java:29:32:29:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | -| JsonpInjection.java:33:21:33:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:34:16:34:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:34:16:34:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:41:32:41:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | -| JsonpInjection.java:43:21:43:80 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:45:16:45:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:45:16:45:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:52:32:52:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | -| JsonpInjection.java:54:21:54:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:55:16:55:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:55:16:55:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:62:32:62:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | -| JsonpInjection.java:64:21:64:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:65:16:65:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:65:16:65:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:72:32:72:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | -| JsonpInjection.java:79:21:79:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:80:20:80:28 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:80:20:80:28 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:87:32:87:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | -| JsonpInjection.java:93:21:93:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:94:20:94:28 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:94:20:94:28 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:101:32:101:38 | request : HttpServletRequest | semmle.label | request : HttpServletRequest | -| JsonpInjection.java:112:16:112:24 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:127:25:127:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:128:20:128:28 | resultStr | semmle.label | resultStr | -| JsonpInjection.java:148:25:148:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjection.java:149:20:149:28 | resultStr | semmle.label | resultStr | -#select -| JsonpInjection.java:34:16:34:24 | resultStr | JsonpInjection.java:29:32:29:38 | request : HttpServletRequest | JsonpInjection.java:34:16:34:24 | -resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:29:32:29:38 | request | this user input | -| JsonpInjection.java:45:16:45:24 | resultStr | JsonpInjection.java:41:32:41:38 | request : HttpServletRequest | JsonpInjection.java:45:16:45:24 | -resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:41:32:41:38 | request | this user input | -| JsonpInjection.java:55:16:55:24 | resultStr | JsonpInjection.java:52:32:52:38 | request : HttpServletRequest | JsonpInjection.java:55:16:55:24 | -resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:52:32:52:38 | request | this user input | -| JsonpInjection.java:65:16:65:24 | resultStr | JsonpInjection.java:62:32:62:38 | request : HttpServletRequest | JsonpInjection.java:65:16:65:24 | -resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:62:32:62:38 | request | this user input | -| JsonpInjection.java:80:20:80:28 | resultStr | JsonpInjection.java:72:32:72:38 | request : HttpServletRequest | JsonpInjection.java:80:20:80:28 | -resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:72:32:72:38 | request | this user input | -| JsonpInjection.java:94:20:94:28 | resultStr | JsonpInjection.java:87:32:87:38 | request : HttpServletRequest | JsonpInjection.java:94:20:94:28 | -resultStr | Jsonp Injection query might include code from $@. | JsonpInjection.java:87:32:87:38 | request | this user input | \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet1.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet1.java new file mode 100644 index 00000000000..14ef76275b1 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet1.java @@ -0,0 +1,64 @@ +import com.google.gson.Gson; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class JsonpInjectionServlet1 extends HttpServlet { + + private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + private static final long serialVersionUID = 1L; + + private String key = "test"; + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doPost(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("application/json"); + String jsonpCallback = req.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String jsonResult = gson.toJson(hashMap); + + String referer = req.getHeader("Referer"); + + boolean result = verifReferer(referer); + + // good + if (result){ + String resultStr = null; + pw = resp.getWriter(); + resultStr = jsonpCallback + "(" + jsonResult + ")"; + pw.println(resultStr); + pw.flush(); + } + } + + public static boolean verifReferer(String referer){ + if (!referer.startsWith("http://test.com/")){ + return false; + } + return true; + } + + @Override + public void init(ServletConfig config) throws ServletException { + this.key = config.getInitParameter("key"); + System.out.println("åˆå§‹åŒ–" + this.key); + super.init(config); + } + +} diff --git a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet2.java similarity index 75% rename from java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet.java rename to java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet2.java index 916cd9bf676..bbfbc2dc436 100644 --- a/java/ql/src/Security/CWE/CWE-352/JsonpInjectionServlet.java +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet2.java @@ -4,12 +4,11 @@ import java.io.PrintWriter; import java.util.HashMap; import javax.servlet.ServletConfig; import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -public class JsonpInjectionServlet extends HttpServlet { +public class JsonpInjectionServlet2 extends HttpServlet { private static HashMap hashMap = new HashMap(); @@ -23,21 +22,12 @@ public class JsonpInjectionServlet extends HttpServlet { private String key = "test"; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String jsonpCallback = req.getParameter("jsonpCallback"); - - PrintWriter pw = null; - Gson gson = new Gson(); - String result = gson.toJson(hashMap); - - String resultStr = null; - pw = resp.getWriter(); - resultStr = jsonpCallback + "(" + result + ")"; - pw.println(resultStr); - pw.flush(); + doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("application/json"); String jsonpCallback = req.getParameter("jsonpCallback"); PrintWriter pw = null; Gson gson = new Gson(); diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_1.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_1.expected new file mode 100644 index 00000000000..a89d03b67a7 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_1.expected @@ -0,0 +1,60 @@ +edges +| JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | resultStr | +| JsonpController.java:30:21:30:54 | ... + ... : String | JsonpController.java:31:16:31:24 | resultStr | +| JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | resultStr | +| JsonpController.java:40:21:40:80 | ... + ... : String | JsonpController.java:42:16:42:24 | resultStr | +| JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | resultStr | +| JsonpController.java:51:21:51:55 | ... + ... : String | JsonpController.java:52:16:52:24 | resultStr | +| JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | resultStr | +| JsonpController.java:61:21:61:54 | ... + ... : String | JsonpController.java:62:16:62:24 | resultStr | +| JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | resultStr | +| JsonpController.java:76:21:76:54 | ... + ... : String | JsonpController.java:77:20:77:28 | resultStr | +| JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | resultStr | +| JsonpController.java:90:21:90:54 | ... + ... : String | JsonpController.java:91:20:91:28 | resultStr | +| JsonpController.java:99:24:99:52 | getParameter(...) : String | JsonpController.java:101:24:101:28 | token | +| JsonpController.java:102:36:102:72 | getParameter(...) : String | JsonpController.java:105:20:105:28 | resultStr | +| JsonpController.java:104:25:104:59 | ... + ... : String | JsonpController.java:105:20:105:28 | resultStr | +nodes +| JsonpController.java:26:32:26:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:30:21:30:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:38:32:38:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:40:21:40:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:49:32:49:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:51:21:51:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:59:32:59:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:61:21:61:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:69:32:69:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:76:21:76:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:84:32:84:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:90:21:90:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:99:24:99:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:101:24:101:28 | token | semmle.label | token | +| JsonpController.java:102:36:102:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:104:25:104:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | +#select +| JsonpController.java:31:16:31:24 | resultStr | JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:26:32:26:68 | getParameter(...) | this user input | +| JsonpController.java:42:16:42:24 | resultStr | JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:38:32:38:68 | getParameter(...) | this user input | +| JsonpController.java:52:16:52:24 | resultStr | JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:49:32:49:68 | getParameter(...) | this user input | +| JsonpController.java:62:16:62:24 | resultStr | JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:59:32:59:68 | getParameter(...) | this user input | +| JsonpController.java:77:20:77:28 | resultStr | JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:69:32:69:68 | getParameter(...) | this user input | +| JsonpController.java:91:20:91:28 | resultStr | JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:84:32:84:68 | getParameter(...) | this user input | \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_2.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_2.expected new file mode 100644 index 00000000000..4b12308a212 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_2.expected @@ -0,0 +1,78 @@ +edges +| JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | resultStr | +| JsonpController.java:30:21:30:54 | ... + ... : String | JsonpController.java:31:16:31:24 | resultStr | +| JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | resultStr | +| JsonpController.java:40:21:40:80 | ... + ... : String | JsonpController.java:42:16:42:24 | resultStr | +| JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | resultStr | +| JsonpController.java:51:21:51:55 | ... + ... : String | JsonpController.java:52:16:52:24 | resultStr | +| JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | resultStr | +| JsonpController.java:61:21:61:54 | ... + ... : String | JsonpController.java:62:16:62:24 | resultStr | +| JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | resultStr | +| JsonpController.java:76:21:76:54 | ... + ... : String | JsonpController.java:77:20:77:28 | resultStr | +| JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | resultStr | +| JsonpController.java:90:21:90:54 | ... + ... : String | JsonpController.java:91:20:91:28 | resultStr | +| JsonpController.java:99:24:99:52 | getParameter(...) : String | JsonpController.java:101:24:101:28 | token | +| JsonpController.java:102:36:102:72 | getParameter(...) : String | JsonpController.java:105:20:105:28 | resultStr | +| JsonpController.java:104:25:104:59 | ... + ... : String | JsonpController.java:105:20:105:28 | resultStr | +| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | +| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | JsonpInjectionServlet1.java:38:39:38:45 | referer | +| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | +| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | +| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | +nodes +| JsonpController.java:26:32:26:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:30:21:30:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:38:32:38:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:40:21:40:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:49:32:49:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:51:21:51:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:59:32:59:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:61:21:61:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:69:32:69:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:76:21:76:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:84:32:84:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:90:21:90:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:99:24:99:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:101:24:101:28 | token | semmle.label | token | +| JsonpController.java:102:36:102:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:104:25:104:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | semmle.label | getHeader(...) : String | +| JsonpInjectionServlet1.java:38:39:38:45 | referer | semmle.label | referer | +| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | +#select +| JsonpController.java:31:16:31:24 | resultStr | JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:26:32:26:68 | getParameter(...) | this user input | +| JsonpController.java:42:16:42:24 | resultStr | JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:38:32:38:68 | getParameter(...) | this user input | +| JsonpController.java:52:16:52:24 | resultStr | JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:49:32:49:68 | getParameter(...) | this user input | +| JsonpController.java:62:16:62:24 | resultStr | JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:59:32:59:68 | getParameter(...) | this user input | +| JsonpController.java:77:20:77:28 | resultStr | JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:69:32:69:68 | getParameter(...) | this user input | +| JsonpController.java:91:20:91:28 | resultStr | JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | + resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:84:32:84:68 | getParameter(...) | this user input | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServle +t2.java:39:20:39:28 | resultStr | Jsonp Injection query might include code from $@. | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) | + this user input | \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_3.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_3.expected new file mode 100644 index 00000000000..8e33ca6984c --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_3.expected @@ -0,0 +1,66 @@ +edges +| JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | resultStr | +| JsonpController.java:30:21:30:54 | ... + ... : String | JsonpController.java:31:16:31:24 | resultStr | +| JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | resultStr | +| JsonpController.java:40:21:40:80 | ... + ... : String | JsonpController.java:42:16:42:24 | resultStr | +| JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | resultStr | +| JsonpController.java:51:21:51:55 | ... + ... : String | JsonpController.java:52:16:52:24 | resultStr | +| JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | resultStr | +| JsonpController.java:61:21:61:54 | ... + ... : String | JsonpController.java:62:16:62:24 | resultStr | +| JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | resultStr | +| JsonpController.java:76:21:76:54 | ... + ... : String | JsonpController.java:77:20:77:28 | resultStr | +| JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | resultStr | +| JsonpController.java:90:21:90:54 | ... + ... : String | JsonpController.java:91:20:91:28 | resultStr | +| JsonpController.java:99:24:99:52 | getParameter(...) : String | JsonpController.java:101:24:101:28 | token | +| JsonpController.java:102:36:102:72 | getParameter(...) : String | JsonpController.java:105:20:105:28 | resultStr | +| JsonpController.java:104:25:104:59 | ... + ... : String | JsonpController.java:105:20:105:28 | resultStr | +| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | +| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | JsonpInjectionServlet1.java:38:39:38:45 | referer | +| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | +| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | +| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | +| RefererFilter.java:22:26:22:53 | getHeader(...) : String | RefererFilter.java:23:39:23:45 | refefer | +nodes +| JsonpController.java:26:32:26:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:30:21:30:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:38:32:38:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:40:21:40:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:49:32:49:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:51:21:51:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:59:32:59:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:61:21:61:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:69:32:69:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:76:21:76:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:84:32:84:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:90:21:90:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:99:24:99:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:101:24:101:28 | token | semmle.label | token | +| JsonpController.java:102:36:102:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:104:25:104:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | semmle.label | getHeader(...) : String | +| JsonpInjectionServlet1.java:38:39:38:45 | referer | semmle.label | referer | +| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | +| RefererFilter.java:22:26:22:53 | getHeader(...) : String | semmle.label | getHeader(...) : String | +| RefererFilter.java:23:39:23:45 | refefer | semmle.label | refefer | +#select \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/Readme b/java/ql/test/experimental/query-tests/security/CWE-352/Readme new file mode 100644 index 00000000000..15715d6187c --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/Readme @@ -0,0 +1,3 @@ +1. The JsonpInjection_1.expected result is obtained through the test of `JsonpController.java`. +2. The JsonpInjection_2.expected result is obtained through the test of `JsonpController.java`, `JsonpInjectionServlet1.java`, `JsonpInjectionServlet2.java`. +3. The JsonpInjection_3.expected result is obtained through the test of `JsonpController.java`, `JsonpInjectionServlet1.java`, `JsonpInjectionServlet2.java`, `RefererFilter.java`. \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/RefererFilter.java b/java/ql/test/experimental/query-tests/security/CWE-352/RefererFilter.java new file mode 100644 index 00000000000..97444932ae1 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/RefererFilter.java @@ -0,0 +1,43 @@ +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.util.StringUtils; + +public class RefererFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + String refefer = request.getHeader("Referer"); + boolean result = verifReferer(refefer); + if (result){ + filterChain.doFilter(servletRequest, servletResponse); + } + response.sendError(444, "Referer xxx."); + } + + @Override + public void destroy() { + } + + public static boolean verifReferer(String referer){ + if (StringUtils.isEmpty(referer)){ + return false; + } + if (referer.startsWith("http://www.baidu.com/")){ + return true; + } + return false; + } +} diff --git a/java/ql/test/stubs/gson-2.8.6/com/google/gson/Gson.java b/java/ql/test/stubs/gson-2.8.6/com/google/gson/Gson.java new file mode 100644 index 00000000000..bbe53dc2a5f --- /dev/null +++ b/java/ql/test/stubs/gson-2.8.6/com/google/gson/Gson.java @@ -0,0 +1,7 @@ +package com.google.gson; + +public final class Gson { + public String toJson(Object src) { + return null; + } +} diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/StringUtils.java b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/StringUtils.java new file mode 100644 index 00000000000..6ee07f84593 --- /dev/null +++ b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/StringUtils.java @@ -0,0 +1,8 @@ +package org.springframework.util; + +public abstract class StringUtils { + + public static boolean isEmpty(Object str) { + return str == null || "".equals(str); + } +} \ No newline at end of file diff --git a/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/Filter.java b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/Filter.java new file mode 100644 index 00000000000..5833e3c909d --- /dev/null +++ b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/Filter.java @@ -0,0 +1,13 @@ +package javax.servlet; + +import java.io.IOException; + +public interface Filter { + default void init(FilterConfig filterConfig) throws ServletException { + } + + void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException; + + default void destroy() { + } +} diff --git a/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/FilterChain.java b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/FilterChain.java new file mode 100644 index 00000000000..6a1dfc588b6 --- /dev/null +++ b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/FilterChain.java @@ -0,0 +1,8 @@ +package javax.servlet; + +import java.io.IOException; + +public interface FilterChain { + void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException; +} + diff --git a/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/FilterConfig.java b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/FilterConfig.java new file mode 100644 index 00000000000..66c13eb54f0 --- /dev/null +++ b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/FilterConfig.java @@ -0,0 +1,3 @@ +package javax.servlet; + +public interface FilterConfig {} diff --git a/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletException.java b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletException.java new file mode 100644 index 00000000000..ce5f7c4465a --- /dev/null +++ b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletException.java @@ -0,0 +1,8 @@ +package javax.servlet; + +public class ServletException extends Exception { + private static final long serialVersionUID = 1L; + + public ServletException() { + } +} diff --git a/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletRequest.java b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletRequest.java new file mode 100644 index 00000000000..4ee0026d066 --- /dev/null +++ b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletRequest.java @@ -0,0 +1,87 @@ +package javax.servlet; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; + +public interface ServletRequest { + Object getAttribute(String var1); + + Enumeration getAttributeNames(); + + String getCharacterEncoding(); + + void setCharacterEncoding(String var1) throws UnsupportedEncodingException; + + int getContentLength(); + + long getContentLengthLong(); + + String getContentType(); + + ServletInputStream getInputStream() throws IOException; + + String getParameter(String var1); + + Enumeration getParameterNames(); + + String[] getParameterValues(String var1); + + Map getParameterMap(); + + String getProtocol(); + + String getScheme(); + + String getServerName(); + + int getServerPort(); + + BufferedReader getReader() throws IOException; + + String getRemoteAddr(); + + String getRemoteHost(); + + void setAttribute(String var1, Object var2); + + void removeAttribute(String var1); + + Locale getLocale(); + + Enumeration getLocales(); + + boolean isSecure(); + + RequestDispatcher getRequestDispatcher(String var1); + + /** @deprecated */ + @Deprecated + String getRealPath(String var1); + + int getRemotePort(); + + String getLocalName(); + + String getLocalAddr(); + + int getLocalPort(); + + ServletContext getServletContext(); + + AsyncContext startAsync() throws IllegalStateException; + + AsyncContext startAsync(ServletRequest var1, ServletResponse var2) throws IllegalStateException; + + boolean isAsyncStarted(); + + boolean isAsyncSupported(); + + AsyncContext getAsyncContext(); + + DispatcherType getDispatcherType(); +} + diff --git a/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletResponse.java b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletResponse.java new file mode 100644 index 00000000000..0aa6121e686 --- /dev/null +++ b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/ServletResponse.java @@ -0,0 +1,39 @@ +package javax.servlet; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Locale; + +public interface ServletResponse { + String getCharacterEncoding(); + + String getContentType(); + + ServletOutputStream getOutputStream() throws IOException; + + PrintWriter getWriter() throws IOException; + + void setCharacterEncoding(String var1); + + void setContentLength(int var1); + + void setContentLengthLong(long var1); + + void setContentType(String var1); + + void setBufferSize(int var1); + + int getBufferSize(); + + void flushBuffer() throws IOException; + + void resetBuffer(); + + boolean isCommitted(); + + void reset(); + + void setLocale(Locale var1); + + Locale getLocale(); +} diff --git a/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/http/HttpServletRequest.java b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/http/HttpServletRequest.java new file mode 100644 index 00000000000..02d53a96a33 --- /dev/null +++ b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/http/HttpServletRequest.java @@ -0,0 +1,116 @@ +package javax.servlet.http; + +import java.io.IOException; +import java.security.Principal; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; + +public interface HttpServletRequest extends ServletRequest { + String BASIC_AUTH = "BASIC"; + String FORM_AUTH = "FORM"; + String CLIENT_CERT_AUTH = "CLIENT_CERT"; + String DIGEST_AUTH = "DIGEST"; + + String getAuthType(); + + Cookie[] getCookies(); + + long getDateHeader(String var1); + + String getHeader(String var1); + + Enumeration getHeaders(String var1); + + Enumeration getHeaderNames(); + + int getIntHeader(String var1); + + default HttpServletMapping getHttpServletMapping() { + return new HttpServletMapping() { + public String getMatchValue() { + return ""; + } + + public String getPattern() { + return ""; + } + + public String getServletName() { + return ""; + } + + public MappingMatch getMappingMatch() { + return null; + } + }; + } + + String getMethod(); + + String getPathInfo(); + + String getPathTranslated(); + + default PushBuilder newPushBuilder() { + return null; + } + + String getContextPath(); + + String getQueryString(); + + String getRemoteUser(); + + boolean isUserInRole(String var1); + + Principal getUserPrincipal(); + + String getRequestedSessionId(); + + String getRequestURI(); + + StringBuffer getRequestURL(); + + String getServletPath(); + + HttpSession getSession(boolean var1); + + HttpSession getSession(); + + String changeSessionId(); + + boolean isRequestedSessionIdValid(); + + boolean isRequestedSessionIdFromCookie(); + + boolean isRequestedSessionIdFromURL(); + + /** @deprecated */ + @Deprecated + boolean isRequestedSessionIdFromUrl(); + + boolean authenticate(HttpServletResponse var1) throws IOException, ServletException; + + void login(String var1, String var2) throws ServletException; + + void logout() throws ServletException; + + Collection getParts() throws IOException, ServletException; + + Part getPart(String var1) throws IOException, ServletException; + + T upgrade(Class var1) throws IOException, ServletException; + + default Map getTrailerFields() { + return Collections.emptyMap(); + } + + default boolean isTrailerFieldsReady() { + return false; + } +} + diff --git a/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/http/HttpServletResponse.java b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/http/HttpServletResponse.java new file mode 100644 index 00000000000..0a2c6af0913 --- /dev/null +++ b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/http/HttpServletResponse.java @@ -0,0 +1,106 @@ +package javax.servlet.http; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; +import javax.servlet.ServletResponse; + +public interface HttpServletResponse extends ServletResponse { + int SC_CONTINUE = 100; + int SC_SWITCHING_PROTOCOLS = 101; + int SC_OK = 200; + int SC_CREATED = 201; + int SC_ACCEPTED = 202; + int SC_NON_AUTHORITATIVE_INFORMATION = 203; + int SC_NO_CONTENT = 204; + int SC_RESET_CONTENT = 205; + int SC_PARTIAL_CONTENT = 206; + int SC_MULTIPLE_CHOICES = 300; + int SC_MOVED_PERMANENTLY = 301; + int SC_MOVED_TEMPORARILY = 302; + int SC_FOUND = 302; + int SC_SEE_OTHER = 303; + int SC_NOT_MODIFIED = 304; + int SC_USE_PROXY = 305; + int SC_TEMPORARY_REDIRECT = 307; + int SC_BAD_REQUEST = 400; + int SC_UNAUTHORIZED = 401; + int SC_PAYMENT_REQUIRED = 402; + int SC_FORBIDDEN = 403; + int SC_NOT_FOUND = 404; + int SC_METHOD_NOT_ALLOWED = 405; + int SC_NOT_ACCEPTABLE = 406; + int SC_PROXY_AUTHENTICATION_REQUIRED = 407; + int SC_REQUEST_TIMEOUT = 408; + int SC_CONFLICT = 409; + int SC_GONE = 410; + int SC_LENGTH_REQUIRED = 411; + int SC_PRECONDITION_FAILED = 412; + int SC_REQUEST_ENTITY_TOO_LARGE = 413; + int SC_REQUEST_URI_TOO_LONG = 414; + int SC_UNSUPPORTED_MEDIA_TYPE = 415; + int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + int SC_EXPECTATION_FAILED = 417; + int SC_INTERNAL_SERVER_ERROR = 500; + int SC_NOT_IMPLEMENTED = 501; + int SC_BAD_GATEWAY = 502; + int SC_SERVICE_UNAVAILABLE = 503; + int SC_GATEWAY_TIMEOUT = 504; + int SC_HTTP_VERSION_NOT_SUPPORTED = 505; + + void addCookie(Cookie var1); + + boolean containsHeader(String var1); + + String encodeURL(String var1); + + String encodeRedirectURL(String var1); + + /** @deprecated */ + @Deprecated + String encodeUrl(String var1); + + /** @deprecated */ + @Deprecated + String encodeRedirectUrl(String var1); + + void sendError(int var1, String var2) throws IOException; + + void sendError(int var1) throws IOException; + + void sendRedirect(String var1) throws IOException; + + void setDateHeader(String var1, long var2); + + void addDateHeader(String var1, long var2); + + void setHeader(String var1, String var2); + + void addHeader(String var1, String var2); + + void setIntHeader(String var1, int var2); + + void addIntHeader(String var1, int var2); + + void setStatus(int var1); + + /** @deprecated */ + @Deprecated + void setStatus(int var1, String var2); + + int getStatus(); + + String getHeader(String var1); + + Collection getHeaders(String var1); + + Collection getHeaderNames(); + + default void setTrailerFields(Supplier> supplier) { + } + + default Supplier> getTrailerFields() { + return null; + } +} From 01c13c470350c2784d427c5a47d68a198a503fd1 Mon Sep 17 00:00:00 2001 From: ihsinme Date: Thu, 4 Mar 2021 16:14:11 +0300 Subject: [PATCH 0029/1429] Add files via upload --- ...ratorPrecedenceLogicErrorWhenUseBoolType.c | 11 +++++ ...rPrecedenceLogicErrorWhenUseBoolType.qhelp | 28 +++++++++++ ...atorPrecedenceLogicErrorWhenUseBoolType.ql | 48 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.c create mode 100644 cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.qhelp create mode 100644 cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql diff --git a/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.c b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.c new file mode 100644 index 00000000000..8458d82f7ad --- /dev/null +++ b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.c @@ -0,0 +1,11 @@ +if(len=funcReadData()==0) return 1; // BAD: variable `len` will not equal the value returned by function `funcReadData()` +... +if((len=funcReadData())==0) return 1; // GOOD: variable `len` equal the value returned by function `funcReadData()` +... +bool a=true; +a++;// BAD: variable `a` does not change its meaning +bool b; +b=-a;// BAD: variable `b` equal `true` +... +a=false;// GOOD: variable `a` equal `false` +b=!a;// GOOD: variable `b` equal `false` diff --git a/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.qhelp b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.qhelp new file mode 100644 index 00000000000..8114da831fe --- /dev/null +++ b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.qhelp @@ -0,0 +1,28 @@ + + + +

    Finding places of confusing use of boolean type. For example, a unary minus does not work before a boolean type and an increment always gives true.

    + + +
    + + +

    we recommend making the code simpler.

    + +
    + +

    The following example demonstrates erroneous and fixed methods for using a boolean data type.

    + + +
    + + +
  • + CERT C Coding Standard: + EXP00-C. Use parentheses for precedence of operation. +
  • + +
    +
    diff --git a/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql new file mode 100644 index 00000000000..1a116a83dbf --- /dev/null +++ b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql @@ -0,0 +1,48 @@ +/** + * @name Operator Precedence Logic Error When Use Bool Type + * @description --Finding places of confusing use of boolean type. + * --For example, a unary minus does not work before a boolean type and an increment always gives true. + * @kind problem + * @id cpp/operator-precedence-logic-error-when-use-bool-type + * @problem.severity warning + * @precision medium + * @tags correctness + * security + * external/cwe/cwe-783 + * external/cwe/cwe-480 + */ + +import cpp +import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis + +/** Holds, if it is an expression, a boolean increment. */ +predicate incrementBoolType(Expr exp) { + exp.(IncrementOperation).getOperand().getType() instanceof BoolType +} + +/** Holds, if this is an expression, applies a minus to a boolean type. */ +predicate revertSignBoolType(Expr exp) { + exp.(AssignExpr).getRValue().(UnaryMinusExpr).getAnOperand().getType() instanceof BoolType and + exp.(AssignExpr).getLValue().getType() instanceof BoolType +} + +/** Holds, if this is an expression, uses comparison and assignment outside of execution precedence. */ +predicate assignBoolType(Expr exp) { + exists(ComparisonOperation co | + exp.(AssignExpr).getRValue() = co and + exp.isCondition() and + not co.isParenthesised() and + not exp.(AssignExpr).getLValue().getType() instanceof BoolType and + co.getLeftOperand() instanceof FunctionCall and + not co.getRightOperand().getType() instanceof BoolType and + not co.getRightOperand().getValue() = "0" and + not co.getRightOperand().getValue() = "1" + ) +} + +from Expr exp +where + incrementBoolType(exp) or + revertSignBoolType(exp) or + assignBoolType(exp) +select exp, "this expression needs attention" From 10cc57428907c41e08786242051028c996935ef3 Mon Sep 17 00:00:00 2001 From: ihsinme Date: Thu, 4 Mar 2021 16:15:26 +0300 Subject: [PATCH 0030/1429] Add files via upload --- ...ecedenceLogicErrorWhenUseBoolType.expected | 5 ++++ ...rPrecedenceLogicErrorWhenUseBoolType.qlref | 1 + .../CWE/CWE-788/semmle/tests/test.cpp | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.expected create mode 100644 cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.qlref create mode 100644 cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/test.cpp diff --git a/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.expected b/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.expected new file mode 100644 index 00000000000..76062fc360a --- /dev/null +++ b/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.expected @@ -0,0 +1,5 @@ +| test.cpp:10:3:10:10 | ... = ... | this expression needs attention | +| test.cpp:12:3:12:6 | ... ++ | this expression needs attention | +| test.cpp:13:3:13:6 | ++ ... | this expression needs attention | +| test.cpp:14:6:14:21 | ... = ... | this expression needs attention | +| test.cpp:16:6:16:21 | ... = ... | this expression needs attention | diff --git a/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.qlref b/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.qlref new file mode 100644 index 00000000000..5189abcce5d --- /dev/null +++ b/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql diff --git a/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/test.cpp b/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/test.cpp new file mode 100644 index 00000000000..f08d2a45757 --- /dev/null +++ b/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/test.cpp @@ -0,0 +1,26 @@ +int tmpFunc() +{ + return 12; +} +void testFunction() +{ + int i1,i2,i3; + bool b1,b2,b3; + char c1,c2,c3; + b1 = -b2; //BAD + b1 = !b2; //GOOD + b1++; //BAD + ++b1; //BAD + if(i1=tmpFunc()!=i2) //BAD + return; + if(i1=tmpFunc()!=11) //BAD + return; + if((i1=tmpFunc())!=i2) //GOOD + return; + if((i1=tmpFunc())!=11) //GOOD + return; + if(i1=tmpFunc()!=1) //GOOD + return; + if(i1=tmpFunc()==b1) //GOOD + return; +} From 919c6b4b0aae6053a1b4b4d1e3657ac50e4a94f5 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 5 Mar 2021 02:50:54 +0000 Subject: [PATCH 0031/1429] Optimize flow steps --- .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 36 ++++++++++--------- .../SensitiveCookieNotHttpOnly.expected | 10 ++++-- .../CWE-1004/SensitiveCookieNotHttpOnly.java | 13 +++++-- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index d9104cbad97..f22e99e567c 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -8,6 +8,7 @@ */ import java +import semmle.code.java.dataflow.FlowSteps import semmle.code.java.frameworks.Servlets import semmle.code.java.dataflow.TaintTracking import DataFlow::PathGraph @@ -41,18 +42,31 @@ class SetCookieMethodAccess extends MethodAccess { } } +/** The cookie class of Java EE. */ +class CookieClass extends RefType { + CookieClass() { + this.getASupertype*() + .hasQualifiedName(["javax.servlet.http", "javax.ws.rs.core", "jakarta.ws.rs.core"], "Cookie") + } +} + +/** The method call `toString` to get a stringified cookie representation. */ +class CookieInstanceExpr extends TaintPreservingCallable { + CookieInstanceExpr() { + this.getDeclaringType() instanceof CookieClass and + this.hasName("toString") + } + + override predicate returnsTaintFrom(int arg) { arg = -1 } +} + /** Sensitive cookie name used in a `Cookie` constructor or a `Set-Cookie` call. */ class SensitiveCookieNameExpr extends Expr { SensitiveCookieNameExpr() { exists( ClassInstanceExpr cie // new Cookie("jwt_token", token) | - ( - cie.getConstructedType().hasQualifiedName("javax.servlet.http", "Cookie") or - cie.getConstructedType() - .getASupertype*() - .hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "Cookie") - ) and + cie.getConstructedType() instanceof CookieClass and this = cie and isSensitiveCookieNameExpr(cie.getArgument(0)) ) @@ -169,16 +183,6 @@ class MissingHttpOnlyConfiguration extends TaintTracking::Configuration { // Test class or method isTestMethod(node) } - - override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { - exists( - MethodAccess ma // `toString` call on a cookie object - | - ma.getQualifier() = pred.asExpr() and - ma.getMethod().hasName("toString") and - ma = succ.asExpr() - ) - } } from DataFlow::PathNode source, DataFlow::PathNode sink, MissingHttpOnlyConfiguration c diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected index e2d05a9b24d..5ccd2bb19f9 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected @@ -1,13 +1,17 @@ edges -| SensitiveCookieNotHttpOnly.java:22:27:22:60 | new Cookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | +| SensitiveCookieNotHttpOnly.java:22:28:22:61 | new Cookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | | SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | +| SensitiveCookieNotHttpOnly.java:60:37:60:115 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:62:42:62:47 | keyStr | nodes -| SensitiveCookieNotHttpOnly.java:22:27:22:60 | new Cookie(...) : Cookie | semmle.label | new Cookie(...) : Cookie | +| SensitiveCookieNotHttpOnly.java:22:28:22:61 | new Cookie(...) : Cookie | semmle.label | new Cookie(...) : Cookie | | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | semmle.label | jwtCookie | | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | semmle.label | ... + ... | | SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) : NewCookie | semmle.label | new NewCookie(...) : NewCookie | | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | semmle.label | toString(...) | +| SensitiveCookieNotHttpOnly.java:60:37:60:115 | new NewCookie(...) : NewCookie | semmle.label | new NewCookie(...) : NewCookie | +| SensitiveCookieNotHttpOnly.java:62:42:62:47 | keyStr | semmle.label | keyStr | #select -| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:22:27:22:60 | new Cookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:22:27:22:60 | new Cookie(...) | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:22:28:22:61 | new Cookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:22:28:22:61 | new Cookie(...) | This sensitive cookie | | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | This sensitive cookie | | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:62:42:62:47 | keyStr | SensitiveCookieNotHttpOnly.java:60:37:60:115 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:62:42:62:47 | keyStr | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:60:37:60:115 | new NewCookie(...) | This sensitive cookie | diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java index 5e4f349f7c8..1d1e6986b44 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java @@ -10,7 +10,7 @@ import javax.ws.rs.core.NewCookie; class SensitiveCookieNotHttpOnly { // GOOD - Tests adding a sensitive cookie with the `HttpOnly` flag set. public void addCookie(String jwt_token, HttpServletRequest request, HttpServletResponse response) { - Cookie jwtCookie =new Cookie("jwt_token", jwt_token); + Cookie jwtCookie = new Cookie("jwt_token", jwt_token); jwtCookie.setPath("/"); jwtCookie.setMaxAge(3600*24*7); jwtCookie.setHttpOnly(true); @@ -19,8 +19,8 @@ class SensitiveCookieNotHttpOnly { // BAD - Tests adding a sensitive cookie without the `HttpOnly` flag set. public void addCookie2(String jwt_token, String userId, HttpServletRequest request, HttpServletResponse response) { - Cookie jwtCookie =new Cookie("jwt_token", jwt_token); - Cookie userIdCookie =new Cookie("user_id", userId.toString()); + Cookie jwtCookie = new Cookie("jwt_token", jwt_token); + Cookie userIdCookie = new Cookie("user_id", userId); jwtCookie.setPath("/"); userIdCookie.setPath("/"); jwtCookie.setMaxAge(3600*24*7); @@ -54,4 +54,11 @@ class SensitiveCookieNotHttpOnly { NewCookie accessKeyCookie = new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true); response.setHeader("Set-Cookie", accessKeyCookie.toString()); } + + // BAD - Tests set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` without the `HttpOnly` flag set. + public void addCookie8(String accessKey, HttpServletRequest request, HttpServletResponse response) { + NewCookie accessKeyCookie = new NewCookie("session-access-key", accessKey, "/", null, 0, null, 86400, true); + String keyStr = accessKeyCookie.toString(); + response.setHeader("Set-Cookie", keyStr); + } } From a93aabab408442a96a9ae8e46718f94291b269b8 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 5 Mar 2021 03:05:49 +0000 Subject: [PATCH 0032/1429] Add the toString() method --- .../jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java | 11 +++++++++++ .../servlet-api-2.4/javax/servlet/http/Cookie.java | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java index 7f2e3ec0535..26279d7fe0a 100644 --- a/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java +++ b/java/ql/test/stubs/jsr311-api-1.1.1/javax/ws/rs/core/NewCookie.java @@ -320,4 +320,15 @@ public class NewCookie extends Cookie { public Cookie toCookie() { return null; } + + /** + * Convert the cookie to a string suitable for use as the value of the + * corresponding HTTP header. + * + * @return a stringified cookie. + */ + @Override + public String toString() { + return null; + } } \ No newline at end of file diff --git a/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/Cookie.java b/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/Cookie.java index a93fec853e0..47b1d883e47 100644 --- a/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/Cookie.java +++ b/java/ql/test/stubs/servlet-api-2.4/javax/servlet/http/Cookie.java @@ -114,4 +114,15 @@ public class Cookie implements Cloneable { public boolean isHttpOnly() { return isHttpOnly; } + + /** + * Convert the cookie to a string suitable for use as the value of the + * corresponding HTTP header. + * + * @return a stringified cookie. + */ + @Override + public String toString() { + return null; + } } From 7d556b354de8cec425caab93fcdd9db0a1ac5ffc Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 5 Mar 2021 09:16:35 +0100 Subject: [PATCH 0033/1429] Python: Update test annotation and expectation --- .../test/query-tests/Security/CWE-327/InsecureProtocol.expected | 2 ++ python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected index afd9cc15d9f..f4202a4634d 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected @@ -38,3 +38,5 @@ | ssl_fluent.py:124:14:124:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:122:5:122:11 | ControlFlowNode for context | context modification | | ssl_fluent.py:173:14:173:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:170:5:170:11 | ControlFlowNode for context | context modification | | ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | ssl_fluent.py:189:5:189:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:188:15:188:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | +| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:188:15:188:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | diff --git a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py index ab65e3bd206..577f342765e 100644 --- a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py +++ b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py @@ -189,5 +189,5 @@ def test_fluent_explicitly_unsafe(): context.options &= ~ssl.OP_NO_SSLv3 with socket.create_connection((hostname, 443)) as sock: - with context.wrap_socket(sock, server_hostname=hostname) as ssock: # SSLv3 not flagged here + with context.wrap_socket(sock, server_hostname=hostname) as ssock: print(ssock.version()) From 31eaa80f5b881531bc66551e568fed1ffc380325 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Sat, 6 Mar 2021 00:56:15 +0000 Subject: [PATCH 0034/1429] Revamp the source --- .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 24 ++++------ .../SensitiveCookieNotHttpOnly.expected | 44 +++++++++++++------ .../CWE-1004/SensitiveCookieNotHttpOnly.java | 9 +++- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index f22e99e567c..229a9d50325 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -60,24 +60,16 @@ class CookieInstanceExpr extends TaintPreservingCallable { override predicate returnsTaintFrom(int arg) { arg = -1 } } +/** The cookie constructor. */ +class CookieTaintPreservingConstructor extends Constructor, TaintPreservingCallable { + CookieTaintPreservingConstructor() { this.getDeclaringType() instanceof CookieClass } + + override predicate returnsTaintFrom(int arg) { arg = 0 } +} + /** Sensitive cookie name used in a `Cookie` constructor or a `Set-Cookie` call. */ class SensitiveCookieNameExpr extends Expr { - SensitiveCookieNameExpr() { - exists( - ClassInstanceExpr cie // new Cookie("jwt_token", token) - | - cie.getConstructedType() instanceof CookieClass and - this = cie and - isSensitiveCookieNameExpr(cie.getArgument(0)) - ) - or - exists( - SetCookieMethodAccess ma // response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure") - | - this = ma.getArgument(1) and - isSensitiveCookieNameExpr(this) - ) - } + SensitiveCookieNameExpr() { isSensitiveCookieNameExpr(this) } } /** Sink of adding a cookie to the HTTP response. */ diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected index 5ccd2bb19f9..c9fe15d4082 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected @@ -1,17 +1,33 @@ edges -| SensitiveCookieNotHttpOnly.java:22:28:22:61 | new Cookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | -| SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | -| SensitiveCookieNotHttpOnly.java:60:37:60:115 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:62:42:62:47 | keyStr | +| SensitiveCookieNotHttpOnly.java:22:33:22:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:29:28:29:36 | jwtCookie | +| SensitiveCookieNotHttpOnly.java:40:42:40:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | +| SensitiveCookieNotHttpOnly.java:40:42:40:57 | ... + ... : String | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | +| SensitiveCookieNotHttpOnly.java:50:56:50:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:50:42:50:124 | toString(...) | +| SensitiveCookieNotHttpOnly.java:61:51:61:70 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:63:42:63:47 | keyStr | +| SensitiveCookieNotHttpOnly.java:68:28:68:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | +| SensitiveCookieNotHttpOnly.java:68:28:68:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | +| SensitiveCookieNotHttpOnly.java:68:28:68:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | nodes -| SensitiveCookieNotHttpOnly.java:22:28:22:61 | new Cookie(...) : Cookie | semmle.label | new Cookie(...) : Cookie | -| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | semmle.label | jwtCookie | -| SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | semmle.label | ... + ... | -| SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) : NewCookie | semmle.label | new NewCookie(...) : NewCookie | -| SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | semmle.label | toString(...) | -| SensitiveCookieNotHttpOnly.java:60:37:60:115 | new NewCookie(...) : NewCookie | semmle.label | new NewCookie(...) : NewCookie | -| SensitiveCookieNotHttpOnly.java:62:42:62:47 | keyStr | semmle.label | keyStr | +| SensitiveCookieNotHttpOnly.java:22:33:22:43 | "jwt_token" : String | semmle.label | "jwt_token" : String | +| SensitiveCookieNotHttpOnly.java:29:28:29:36 | jwtCookie | semmle.label | jwtCookie | +| SensitiveCookieNotHttpOnly.java:40:42:40:49 | "token=" : String | semmle.label | "token=" : String | +| SensitiveCookieNotHttpOnly.java:40:42:40:57 | ... + ... : String | semmle.label | ... + ... : String | +| SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | semmle.label | ... + ... | +| SensitiveCookieNotHttpOnly.java:50:42:50:124 | toString(...) | semmle.label | toString(...) | +| SensitiveCookieNotHttpOnly.java:50:56:50:75 | "session-access-key" : String | semmle.label | "session-access-key" : String | +| SensitiveCookieNotHttpOnly.java:61:51:61:70 | "session-access-key" : String | semmle.label | "session-access-key" : String | +| SensitiveCookieNotHttpOnly.java:63:42:63:47 | keyStr | semmle.label | keyStr | +| SensitiveCookieNotHttpOnly.java:68:28:68:35 | "token=" : String | semmle.label | "token=" : String | +| SensitiveCookieNotHttpOnly.java:68:28:68:43 | ... + ... : String | semmle.label | ... + ... : String | +| SensitiveCookieNotHttpOnly.java:68:28:68:55 | ... + ... : String | semmle.label | ... + ... : String | +| SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | semmle.label | secString | #select -| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:22:28:22:61 | new Cookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:22:28:22:61 | new Cookie(...) | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:49:42:49:113 | new NewCookie(...) | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:62:42:62:47 | keyStr | SensitiveCookieNotHttpOnly.java:60:37:60:115 | new NewCookie(...) : NewCookie | SensitiveCookieNotHttpOnly.java:62:42:62:47 | keyStr | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:60:37:60:115 | new NewCookie(...) | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:29:28:29:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:22:33:22:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:29:28:29:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:22:33:22:43 | "jwt_token" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | SensitiveCookieNotHttpOnly.java:40:42:40:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:40:42:40:49 | "token=" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | SensitiveCookieNotHttpOnly.java:40:42:40:57 | ... + ... : String | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:40:42:40:57 | ... + ... | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:50:42:50:124 | toString(...) | SensitiveCookieNotHttpOnly.java:50:56:50:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:50:42:50:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:50:56:50:75 | "session-access-key" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:63:42:63:47 | keyStr | SensitiveCookieNotHttpOnly.java:61:51:61:70 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:63:42:63:47 | keyStr | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:61:51:61:70 | "session-access-key" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | SensitiveCookieNotHttpOnly.java:68:28:68:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:68:28:68:35 | "token=" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | SensitiveCookieNotHttpOnly.java:68:28:68:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:68:28:68:43 | ... + ... | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | SensitiveCookieNotHttpOnly.java:68:28:68:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:68:28:68:55 | ... + ... | This sensitive cookie | diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java index 1d1e6986b44..6572577a697 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java @@ -19,7 +19,8 @@ class SensitiveCookieNotHttpOnly { // BAD - Tests adding a sensitive cookie without the `HttpOnly` flag set. public void addCookie2(String jwt_token, String userId, HttpServletRequest request, HttpServletResponse response) { - Cookie jwtCookie = new Cookie("jwt_token", jwt_token); + String tokenCookieStr = "jwt_token"; + Cookie jwtCookie = new Cookie(tokenCookieStr, jwt_token); Cookie userIdCookie = new Cookie("user_id", userId); jwtCookie.setPath("/"); userIdCookie.setPath("/"); @@ -61,4 +62,10 @@ class SensitiveCookieNotHttpOnly { String keyStr = accessKeyCookie.toString(); response.setHeader("Set-Cookie", keyStr); } + + // BAD - Tests set a sensitive cookie header using a variable without the `HttpOnly` flag set. + public void addCookie9(String authId, HttpServletRequest request, HttpServletResponse response) { + String secString = "token=" +authId + ";Secure"; + response.addHeader("Set-Cookie", secString); + } } From 48975fa7d220b7debe5d0c8a0b84c8ec64351435 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Wed, 10 Mar 2021 00:17:26 +0000 Subject: [PATCH 0035/1429] Replace sanitizers --- .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 105 +++++++----------- 1 file changed, 41 insertions(+), 64 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index 229a9d50325..716975d0486 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -50,23 +50,6 @@ class CookieClass extends RefType { } } -/** The method call `toString` to get a stringified cookie representation. */ -class CookieInstanceExpr extends TaintPreservingCallable { - CookieInstanceExpr() { - this.getDeclaringType() instanceof CookieClass and - this.hasName("toString") - } - - override predicate returnsTaintFrom(int arg) { arg = -1 } -} - -/** The cookie constructor. */ -class CookieTaintPreservingConstructor extends Constructor, TaintPreservingCallable { - CookieTaintPreservingConstructor() { this.getDeclaringType() instanceof CookieClass } - - override predicate returnsTaintFrom(int arg) { arg = 0 } -} - /** Sensitive cookie name used in a `Cookie` constructor or a `Set-Cookie` call. */ class SensitiveCookieNameExpr extends Expr { SensitiveCookieNameExpr() { isSensitiveCookieNameExpr(this) } @@ -78,55 +61,58 @@ class CookieResponseSink extends DataFlow::ExprNode { exists(MethodAccess ma | ( ma.getMethod() instanceof ResponseAddCookieMethod and - this.getExpr() = ma.getArgument(0) + this.getExpr() = ma.getArgument(0) and + not exists( + MethodAccess ma2 // cookie.setHttpOnly(true) + | + ma2.getMethod().getName() = "setHttpOnly" and + ma2.getArgument(0).(BooleanLiteral).getBooleanValue() = true and + DataFlow::localExprFlow(ma2.getQualifier(), this.getExpr()) + ) or ma instanceof SetCookieMethodAccess and - this.getExpr() = ma.getArgument(1) + this.getExpr() = ma.getArgument(1) and + not hasHttpOnlyExpr(this.getExpr()) // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure") ) ) } } -/** - * Holds if `node` is an access to a variable which has `setHttpOnly(true)` called on it and is also - * the first argument to a call to the method `addCookie` of `javax.servlet.http.HttpServletResponse`. - */ -predicate setHttpOnlyMethodAccess(DataFlow::Node node) { - exists( - MethodAccess addCookie, Variable cookie, MethodAccess m // jwtCookie.setHttpOnly(true) - | - addCookie.getMethod() instanceof ResponseAddCookieMethod and - addCookie.getArgument(0) = cookie.getAnAccess() and - m.getMethod().getName() = "setHttpOnly" and - m.getArgument(0).(BooleanLiteral).getBooleanValue() = true and - m.getQualifier() = cookie.getAnAccess() and - node.asExpr() = cookie.getAnAccess() - ) +/** A JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */ +class HttpOnlyNewCookie extends ClassInstanceExpr { + HttpOnlyNewCookie() { + this.getConstructedType() + .hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and + ( + this.getNumArgument() = 6 and this.getArgument(5).(BooleanLiteral).getBooleanValue() = true // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) + or + this.getNumArgument() = 8 and + this.getArgument(6).getType() instanceof BooleanType and + this.getArgument(7).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) + or + this.getNumArgument() = 10 and this.getArgument(9).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) + ) + } } -/** - * Holds if `node` is a string that contains `httponly` and which flows to the second argument - * of a method to set a cookie. - */ -predicate setHttpOnlyInSetCookie(DataFlow::Node node) { - exists(SetCookieMethodAccess sa | - hasHttpOnlyExpr(node.asExpr()) and - DataFlow::localExprFlow(node.asExpr(), sa.getArgument(1)) - ) +/** The cookie constructor. */ +class CookieTaintPreservingConstructor extends Constructor, TaintPreservingCallable { + CookieTaintPreservingConstructor() { + this.getDeclaringType() instanceof CookieClass and + not exists(HttpOnlyNewCookie hie | hie.getConstructor() = this) + } + + override predicate returnsTaintFrom(int arg) { arg = 0 } } -/** Holds if `cie` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */ -predicate setHttpOnlyInNewCookie(ClassInstanceExpr cie) { - cie.getConstructedType().hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and - ( - cie.getNumArgument() = 6 and cie.getArgument(5).(BooleanLiteral).getBooleanValue() = true // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) - or - cie.getNumArgument() = 8 and - cie.getArgument(6).getType() instanceof BooleanType and - cie.getArgument(7).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) - or - cie.getNumArgument() = 10 and cie.getArgument(9).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) - ) +/** The method call `toString` to get a stringified cookie representation. */ +class CookieInstanceExpr extends TaintPreservingCallable { + CookieInstanceExpr() { + this.getDeclaringType() instanceof CookieClass and + this.hasName("toString") + } + + override predicate returnsTaintFrom(int arg) { arg = -1 } } /** @@ -163,15 +149,6 @@ class MissingHttpOnlyConfiguration extends TaintTracking::Configuration { override predicate isSink(DataFlow::Node sink) { sink instanceof CookieResponseSink } override predicate isSanitizer(DataFlow::Node node) { - // cookie.setHttpOnly(true) - setHttpOnlyMethodAccess(node) - or - // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure") - setHttpOnlyInSetCookie(node) - or - // new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true) - setHttpOnlyInNewCookie(node.asExpr()) - or // Test class or method isTestMethod(node) } From 72f28513eb6478024cb184f9affe71490c8217d3 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Wed, 10 Mar 2021 12:12:27 +0000 Subject: [PATCH 0036/1429] Move test check to the sink --- .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index 716975d0486..eee4d8c89d1 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -73,34 +73,29 @@ class CookieResponseSink extends DataFlow::ExprNode { ma instanceof SetCookieMethodAccess and this.getExpr() = ma.getArgument(1) and not hasHttpOnlyExpr(this.getExpr()) // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure") - ) + ) and + not isTestMethod(ma) // Test class or method ) } } -/** A JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */ -class HttpOnlyNewCookie extends ClassInstanceExpr { - HttpOnlyNewCookie() { - this.getConstructedType() - .hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and - ( - this.getNumArgument() = 6 and this.getArgument(5).(BooleanLiteral).getBooleanValue() = true // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) - or - this.getNumArgument() = 8 and - this.getArgument(6).getType() instanceof BooleanType and - this.getArgument(7).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) - or - this.getNumArgument() = 10 and this.getArgument(9).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) - ) - } +/** Holds if `cie` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */ +predicate setHttpOnlyInNewCookie(ClassInstanceExpr cie) { + cie.getConstructedType().hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and + ( + cie.getNumArgument() = 6 and cie.getArgument(5).(BooleanLiteral).getBooleanValue() = true // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) + or + cie.getNumArgument() = 8 and + cie.getArgument(6).getType() instanceof BooleanType and + cie.getArgument(7).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) + or + cie.getNumArgument() = 10 and cie.getArgument(9).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) + ) } /** The cookie constructor. */ class CookieTaintPreservingConstructor extends Constructor, TaintPreservingCallable { - CookieTaintPreservingConstructor() { - this.getDeclaringType() instanceof CookieClass and - not exists(HttpOnlyNewCookie hie | hie.getConstructor() = this) - } + CookieTaintPreservingConstructor() { this.getDeclaringType() instanceof CookieClass } override predicate returnsTaintFrom(int arg) { arg = 0 } } @@ -122,9 +117,8 @@ class CookieInstanceExpr extends TaintPreservingCallable { * c) in a test class whose name has the word `test` * d) in a test class implementing a test framework such as JUnit or TestNG */ -predicate isTestMethod(DataFlow::Node node) { - exists(MethodAccess ma, Method m | - node.asExpr() = ma.getAnArgument() and +predicate isTestMethod(MethodAccess ma) { + exists(Method m | m = ma.getEnclosingCallable() and ( m.getDeclaringType().getName().toLowerCase().matches("%test%") or // Simple check to exclude test classes to reduce FPs @@ -149,8 +143,8 @@ class MissingHttpOnlyConfiguration extends TaintTracking::Configuration { override predicate isSink(DataFlow::Node sink) { sink instanceof CookieResponseSink } override predicate isSanitizer(DataFlow::Node node) { - // Test class or method - isTestMethod(node) + // new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true) + setHttpOnlyInNewCookie(node.asExpr()) } } From f0ddfc9283607f01603a93e6a05357aa27a579ce Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Wed, 10 Mar 2021 12:18:55 +0000 Subject: [PATCH 0037/1429] Minor qldoc changes --- .../Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index eee4d8c89d1..c3f9349dbc8 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -79,7 +79,10 @@ class CookieResponseSink extends DataFlow::ExprNode { } } -/** Holds if `cie` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */ +/** + * Holds if `ClassInstanceExpr` cie is an invocation of a JAX-RS `NewCookie` constructor + * that sets `HttpOnly` to true. + */ predicate setHttpOnlyInNewCookie(ClassInstanceExpr cie) { cie.getConstructedType().hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and ( @@ -111,7 +114,7 @@ class CookieInstanceExpr extends TaintPreservingCallable { } /** - * Holds if the node is a test method indicated by: + * Holds if the MethodAccess `ma` is a test method call indicated by: * a) in a test directory such as `src/test/java` * b) in a test package whose name has the word `test` * c) in a test class whose name has the word `test` From a0a1ddee86382c183b774e9cb88509a1b4aac1d9 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Wed, 10 Mar 2021 17:07:31 +0000 Subject: [PATCH 0038/1429] Update class name --- .../Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index c3f9349dbc8..985c546b555 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -104,8 +104,8 @@ class CookieTaintPreservingConstructor extends Constructor, TaintPreservingCalla } /** The method call `toString` to get a stringified cookie representation. */ -class CookieInstanceExpr extends TaintPreservingCallable { - CookieInstanceExpr() { +class CookieToString extends TaintPreservingCallable { + CookieToString() { this.getDeclaringType() instanceof CookieClass and this.hasName("toString") } From eeac7e322ad0d450f8dac1f0dad9195e4d3b8c33 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Thu, 11 Mar 2021 13:46:32 +0000 Subject: [PATCH 0039/1429] Query to detect insecure configuration of Spring Boot Actuator --- .../InsecureSpringActuatorConfig.qhelp | 47 ++++++++ .../CWE-016/InsecureSpringActuatorConfig.ql | 112 ++++++++++++++++++ .../CWE/CWE-016/application.properties | 22 ++++ .../Security/CWE/CWE-016/pom_bad.xml | 50 ++++++++ .../Security/CWE/CWE-016/pom_good.xml | 50 ++++++++ .../InsecureSpringActuatorConfig.expected | 1 + .../InsecureSpringActuatorConfig.qlref | 1 + .../security/CWE-016/SensitiveInfo.java | 13 ++ .../security/CWE-016/application.properties | 14 +++ .../query-tests/security/CWE-016/pom.xml | 47 ++++++++ 10 files changed, 357 insertions(+) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/application.properties create mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/pom_bad.xml create mode 100644 java/ql/src/experimental/Security/CWE/CWE-016/pom_good.xml create mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.qlref create mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/SensitiveInfo.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/application.properties create mode 100644 java/ql/test/experimental/query-tests/security/CWE-016/pom.xml diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.qhelp b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.qhelp new file mode 100644 index 00000000000..e201156728a --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.qhelp @@ -0,0 +1,47 @@ + + + +

    Spring Boot is a popular framework that facilitates the development of stand-alone applications +and micro services. Spring Boot Actuator helps to expose production-ready support features against +Spring Boot applications.

    + +

    Endpoints of Spring Boot Actuator allow to monitor and interact with a Spring Boot application. +Exposing unprotected actuator endpoints through configuration files can lead to information disclosure +or even remote code execution vulnerability.

    + +

    Rather than programmatically permitting endpoint requests or enforcing access control, frequently +developers simply leave management endpoints publicly accessible in the application configuration file +application.properties without enforcing access control through Spring Security.

    +
    + + +

    Declare the Spring Boot Starter Security module in XML configuration or programmatically enforce +security checks on management endpoints using Spring Security. Otherwise accessing management endpoints +on a different HTTP port other than the port that the web application is listening on also helps to +improve the security.

    +
    + + +

    The following examples show both 'BAD' and 'GOOD' configurations. In the 'BAD' configuration, +no security module is declared and sensitive management endpoints are exposed. In the 'GOOD' configuration, +security is enforced and only endpoints requiring exposure are exposed.

    + + + +
    + + +
  • + Spring Boot documentation: + Spring Boot Actuator: Production-ready Features +
  • +
  • + VERACODE Blog: + Exploiting Spring Boot Actuators +
  • +
  • + HackerOne Report: + Spring Actuator endpoints publicly available, leading to account takeover +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql new file mode 100644 index 00000000000..2dc11e8e38e --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql @@ -0,0 +1,112 @@ +/** + * @name Insecure Spring Boot Actuator Configuration + * @description Exposed Spring Boot Actuator through configuration files without declarative or procedural security enforcement leads to information leak or even remote code execution. + * @kind problem + * @id java/insecure-spring-actuator-config + * @tags security + * external/cwe-016 + */ + +import java +import semmle.code.configfiles.ConfigFiles +import semmle.code.java.security.SensitiveActions +import semmle.code.xml.MavenPom + +/** The parent node of the `org.springframework.boot` group. */ +class SpringBootParent extends Parent { + SpringBootParent() { this.getGroup().getValue() = "org.springframework.boot" } +} + +/** Class of Spring Boot dependencies. */ +class SpringBootPom extends Pom { + SpringBootPom() { this.getParentElement() instanceof SpringBootParent } + + /** Holds if the Spring Boot Actuator module `spring-boot-starter-actuator` is used in the project. */ + predicate isSpringBootActuatorUsed() { + this.getADependency().getArtifact().getValue() = "spring-boot-starter-actuator" + } + + /** Holds if the Spring Boot Security module is used in the project, which brings in other security related libraries. */ + predicate isSpringBootSecurityUsed() { + this.getADependency().getArtifact().getValue() = "spring-boot-starter-security" + } +} + +/** The properties file `application.properties`. */ +class ApplicationProperties extends ConfigPair { + ApplicationProperties() { this.getFile().getBaseName() = "application.properties" } +} + +/** The configuration property `management.security.enabled`. */ +class ManagementSecurityEnabled extends ApplicationProperties { + ManagementSecurityEnabled() { this.getNameElement().getName() = "management.security.enabled" } + + string getManagementSecurityEnabled() { result = this.getValueElement().getValue() } + + predicate hasSecurityDisabled() { getManagementSecurityEnabled() = "false" } + + predicate hasSecurityEnabled() { getManagementSecurityEnabled() = "true" } +} + +/** The configuration property `management.endpoints.web.exposure.include`. */ +class ManagementEndPointInclude extends ApplicationProperties { + ManagementEndPointInclude() { + this.getNameElement().getName() = "management.endpoints.web.exposure.include" + } + + string getManagementEndPointInclude() { result = this.getValueElement().getValue().trim() } +} + +/** The configuration property `management.endpoints.web.exposure.exclude`. */ +class ManagementEndPointExclude extends ApplicationProperties { + ManagementEndPointExclude() { + this.getNameElement().getName() = "management.endpoints.web.exposure.exclude" + } + + string getManagementEndPointExclude() { result = this.getValueElement().getValue().trim() } +} + +/** Holds if an application handles sensitive information judging by its variable names. */ +predicate isProtectedApp() { + exists(VarAccess va | va.getVariable().getName().regexpMatch(getCommonSensitiveInfoRegex())) +} + +from SpringBootPom pom, ApplicationProperties ap, Dependency d +where + isProtectedApp() and + pom.isSpringBootActuatorUsed() and + not pom.isSpringBootSecurityUsed() and + ap.getFile() + .getParentContainer() + .getAbsolutePath() + .matches(pom.getFile().getParentContainer().getAbsolutePath() + "%") and // in the same sub-directory + exists(string s | s = pom.getParentElement().getVersionString() | + s.regexpMatch("1\\.[0|1|2|3|4].*") and + not exists(ManagementSecurityEnabled me | + me.hasSecurityEnabled() and me.getFile() = ap.getFile() + ) + or + s.regexpMatch("1\\.5.*") and + exists(ManagementSecurityEnabled me | me.hasSecurityDisabled() and me.getFile() = ap.getFile()) + or + s.regexpMatch("2.*") and + exists(ManagementEndPointInclude mi | + mi.getFile() = ap.getFile() and + ( + mi.getManagementEndPointInclude() = "*" // all endpoints are enabled + or + mi.getManagementEndPointInclude() + .matches([ + "%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%", "%env%", + "%beans%", "%sessions%" + ]) // all endpoints apart from '/health' and '/info' are considered sensitive + ) and + not exists(ManagementEndPointExclude mx | + mx.getFile() = ap.getFile() and + mx.getManagementEndPointExclude() = mi.getManagementEndPointInclude() + ) + ) + ) and + d = pom.getADependency() and + d.getArtifact().getValue() = "spring-boot-starter-actuator" +select d, "Insecure configuration of Spring Boot Actuator exposes sensitive endpoints." diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/application.properties b/java/ql/src/experimental/Security/CWE/CWE-016/application.properties new file mode 100644 index 00000000000..aa489435a12 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-016/application.properties @@ -0,0 +1,22 @@ +#management.endpoints.web.base-path=/admin + + +#### BAD: All management endpoints are accessible #### +# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default + +# vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators +management.security.enabled=false + +# vulnerable configuration (spring boot 2+): exposes health and info only by default +management.endpoints.web.exposure.include=* + + +#### GOOD: All management endpoints have access control #### +# safe configuration (spring boot 1.0 - 1.4): exposes actuators by default +management.security.enabled=true + +# safe configuration (spring boot 1.5+): requires value false to expose sensitive actuators +management.security.enabled=true + +# safe configuration (spring boot 2+): exposes health and info only by default +management.endpoints.web.exposure.include=beans,info,health diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/pom_bad.xml b/java/ql/src/experimental/Security/CWE/CWE-016/pom_bad.xml new file mode 100644 index 00000000000..9dd5c9c188b --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-016/pom_bad.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + spring-boot-actuator-app + spring-boot-actuator-app + 1.0-SNAPSHOT + + + UTF-8 + 1.8 + 1.8 + + + + org.springframework.boot + spring-boot-starter-parent + 2.3.8.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-devtools + + + + + + + org.springframework.boot + spring-boot-test + + + + \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/pom_good.xml b/java/ql/src/experimental/Security/CWE/CWE-016/pom_good.xml new file mode 100644 index 00000000000..89f577f21e5 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-016/pom_good.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + spring-boot-actuator-app + spring-boot-actuator-app + 1.0-SNAPSHOT + + + UTF-8 + 1.8 + 1.8 + + + + org.springframework.boot + spring-boot-starter-parent + 2.3.8.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-test + + + + \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.expected b/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.expected new file mode 100644 index 00000000000..48630293985 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.expected @@ -0,0 +1 @@ +| pom.xml:29:9:32:22 | dependency | Insecure configuration of Spring Boot Actuator exposes sensitive endpoints. | diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.qlref b/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.qlref new file mode 100644 index 00000000000..9cd12d5e4fb --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-016/InsecureSpringActuatorConfig.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/SensitiveInfo.java b/java/ql/test/experimental/query-tests/security/CWE-016/SensitiveInfo.java new file mode 100644 index 00000000000..a3ff69c1b81 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-016/SensitiveInfo.java @@ -0,0 +1,13 @@ +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class SensitiveInfo { + @RequestMapping + public void handleLogin(@RequestParam String username, @RequestParam String password) throws Exception { + if (!username.equals("") && password.equals("")) { + //Blank processing + } + } +} \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/application.properties b/java/ql/test/experimental/query-tests/security/CWE-016/application.properties new file mode 100644 index 00000000000..95e704f3a1a --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-016/application.properties @@ -0,0 +1,14 @@ +#management.endpoints.web.base-path=/admin + +# vulnerable configuration (spring boot 1.0 - 1.4): exposes actuators by default + +# vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators +management.security.enabled=false + +# vulnerable configuration (spring boot 2+): exposes health and info only by default +management.endpoints.web.exposure.include=* +management.endpoints.web.exposure.exclude=beans + +management.endpoint.shutdown.enabled=true + +management.endpoint.health.show-details=when_authorized \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/pom.xml b/java/ql/test/experimental/query-tests/security/CWE-016/pom.xml new file mode 100644 index 00000000000..a9d5fa920c8 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-016/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + spring-boot-actuator-app + spring-boot-actuator-app + 1.0-SNAPSHOT + + + UTF-8 + 1.8 + 1.8 + + + + org.springframework.boot + spring-boot-starter-parent + 2.3.8.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-devtools + + + + org.springframework.boot + spring-boot-test + + + + \ No newline at end of file From 0a35feef766249b88abfc78f8a9dd552664ab083 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Thu, 11 Mar 2021 17:28:07 +0000 Subject: [PATCH 0040/1429] Exclude CSRF cookies to reduce FPs --- .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 16 +++-- .../SensitiveCookieNotHttpOnly.expected | 60 +++++++++---------- .../CWE-1004/SensitiveCookieNotHttpOnly.java | 22 ++++++- .../query-tests/security/CWE-1004/options | 2 +- .../security/web/csrf/CsrfToken.java | 50 ++++++++++++++++ 5 files changed, 113 insertions(+), 37 deletions(-) create mode 100644 java/ql/test/stubs/springframework-5.2.3/org/springframework/security/web/csrf/CsrfToken.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index 985c546b555..693dad68082 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -16,12 +16,18 @@ import DataFlow::PathGraph /** Gets a regular expression for matching common names of sensitive cookies. */ string getSensitiveCookieNameRegex() { result = "(?i).*(auth|session|token|key|credential).*" } -/** Holds if a string is concatenated with the name of a sensitive cookie. */ +/** Gets a regular expression for matching CSRF cookies. */ +string getCsrfCookieNameRegex() { result = "(?i).*(csrf).*" } + +/** + * Holds if a string is concatenated with the name of a sensitive cookie. Excludes CSRF cookies since + * they are special cookies implementing the Synchronizer Token Pattern that can be used in JavaScript. + */ predicate isSensitiveCookieNameExpr(Expr expr) { - expr.(CompileTimeConstantExpr) - .getStringValue() - .toLowerCase() - .regexpMatch(getSensitiveCookieNameRegex()) or + exists(string s | s = expr.(CompileTimeConstantExpr).getStringValue().toLowerCase() | + s.regexpMatch(getSensitiveCookieNameRegex()) and not s.regexpMatch(getCsrfCookieNameRegex()) + ) + or isSensitiveCookieNameExpr(expr.(AddExpr).getAnOperand()) } diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected index c9fe15d4082..8fa688bef2a 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected @@ -1,33 +1,33 @@ edges -| SensitiveCookieNotHttpOnly.java:22:33:22:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:29:28:29:36 | jwtCookie | -| SensitiveCookieNotHttpOnly.java:40:42:40:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | -| SensitiveCookieNotHttpOnly.java:40:42:40:57 | ... + ... : String | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | -| SensitiveCookieNotHttpOnly.java:50:56:50:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:50:42:50:124 | toString(...) | -| SensitiveCookieNotHttpOnly.java:61:51:61:70 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:63:42:63:47 | keyStr | -| SensitiveCookieNotHttpOnly.java:68:28:68:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | -| SensitiveCookieNotHttpOnly.java:68:28:68:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | -| SensitiveCookieNotHttpOnly.java:68:28:68:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | +| SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | +| SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | +| SensitiveCookieNotHttpOnly.java:42:42:42:57 | ... + ... : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | +| SensitiveCookieNotHttpOnly.java:52:56:52:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:52:42:52:124 | toString(...) | +| SensitiveCookieNotHttpOnly.java:63:51:63:70 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:65:42:65:47 | keyStr | +| SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | +| SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | +| SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | nodes -| SensitiveCookieNotHttpOnly.java:22:33:22:43 | "jwt_token" : String | semmle.label | "jwt_token" : String | -| SensitiveCookieNotHttpOnly.java:29:28:29:36 | jwtCookie | semmle.label | jwtCookie | -| SensitiveCookieNotHttpOnly.java:40:42:40:49 | "token=" : String | semmle.label | "token=" : String | -| SensitiveCookieNotHttpOnly.java:40:42:40:57 | ... + ... : String | semmle.label | ... + ... : String | -| SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | semmle.label | ... + ... | -| SensitiveCookieNotHttpOnly.java:50:42:50:124 | toString(...) | semmle.label | toString(...) | -| SensitiveCookieNotHttpOnly.java:50:56:50:75 | "session-access-key" : String | semmle.label | "session-access-key" : String | -| SensitiveCookieNotHttpOnly.java:61:51:61:70 | "session-access-key" : String | semmle.label | "session-access-key" : String | -| SensitiveCookieNotHttpOnly.java:63:42:63:47 | keyStr | semmle.label | keyStr | -| SensitiveCookieNotHttpOnly.java:68:28:68:35 | "token=" : String | semmle.label | "token=" : String | -| SensitiveCookieNotHttpOnly.java:68:28:68:43 | ... + ... : String | semmle.label | ... + ... : String | -| SensitiveCookieNotHttpOnly.java:68:28:68:55 | ... + ... : String | semmle.label | ... + ... : String | -| SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | semmle.label | secString | +| SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | semmle.label | "jwt_token" : String | +| SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | semmle.label | jwtCookie | +| SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" : String | semmle.label | "token=" : String | +| SensitiveCookieNotHttpOnly.java:42:42:42:57 | ... + ... : String | semmle.label | ... + ... : String | +| SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | semmle.label | ... + ... | +| SensitiveCookieNotHttpOnly.java:52:42:52:124 | toString(...) | semmle.label | toString(...) | +| SensitiveCookieNotHttpOnly.java:52:56:52:75 | "session-access-key" : String | semmle.label | "session-access-key" : String | +| SensitiveCookieNotHttpOnly.java:63:51:63:70 | "session-access-key" : String | semmle.label | "session-access-key" : String | +| SensitiveCookieNotHttpOnly.java:65:42:65:47 | keyStr | semmle.label | keyStr | +| SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" : String | semmle.label | "token=" : String | +| SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | semmle.label | ... + ... : String | +| SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | semmle.label | ... + ... : String | +| SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | semmle.label | secString | #select -| SensitiveCookieNotHttpOnly.java:29:28:29:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:22:33:22:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:29:28:29:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:22:33:22:43 | "jwt_token" | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | SensitiveCookieNotHttpOnly.java:40:42:40:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:40:42:40:49 | "token=" | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | SensitiveCookieNotHttpOnly.java:40:42:40:57 | ... + ... : String | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:40:42:40:57 | ... + ... | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:40:42:40:69 | ... + ... | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:50:42:50:124 | toString(...) | SensitiveCookieNotHttpOnly.java:50:56:50:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:50:42:50:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:50:56:50:75 | "session-access-key" | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:63:42:63:47 | keyStr | SensitiveCookieNotHttpOnly.java:61:51:61:70 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:63:42:63:47 | keyStr | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:61:51:61:70 | "session-access-key" | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | SensitiveCookieNotHttpOnly.java:68:28:68:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:68:28:68:35 | "token=" | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | SensitiveCookieNotHttpOnly.java:68:28:68:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:68:28:68:43 | ... + ... | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | SensitiveCookieNotHttpOnly.java:68:28:68:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:69:42:69:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:68:28:68:55 | ... + ... | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:57 | ... + ... : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:42:42:42:57 | ... + ... | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:52:42:52:124 | toString(...) | SensitiveCookieNotHttpOnly.java:52:56:52:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:52:42:52:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:52:56:52:75 | "session-access-key" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:65:42:65:47 | keyStr | SensitiveCookieNotHttpOnly.java:63:51:63:70 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:65:42:65:47 | keyStr | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:63:51:63:70 | "session-access-key" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... | This sensitive cookie | diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java index 6572577a697..337a99cc096 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java @@ -7,6 +7,8 @@ import javax.servlet.ServletException; import javax.ws.rs.core.NewCookie; +import org.springframework.security.web.csrf.CsrfToken; + class SensitiveCookieNotHttpOnly { // GOOD - Tests adding a sensitive cookie with the `HttpOnly` flag set. public void addCookie(String jwt_token, HttpServletRequest request, HttpServletResponse response) { @@ -67,5 +69,23 @@ class SensitiveCookieNotHttpOnly { public void addCookie9(String authId, HttpServletRequest request, HttpServletResponse response) { String secString = "token=" +authId + ";Secure"; response.addHeader("Set-Cookie", secString); - } + } + + // GOOD - CSRF token doesn't need to have the `HttpOnly` flag set. + public void addCsrfCookie(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + // Spring put the CSRF token in session attribute "_csrf" + CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf"); + + // Send the cookie only if the token has changed + String actualToken = request.getHeader("X-CSRF-TOKEN"); + if (actualToken == null || !actualToken.equals(csrfToken.getToken())) { + // Session cookie that can be used by AngularJS + String pCookieName = "CSRF-TOKEN"; + Cookie cookie = new Cookie(pCookieName, csrfToken.getToken()); + cookie.setMaxAge(-1); + cookie.setHttpOnly(false); + cookie.setPath("/"); + response.addCookie(cookie); + } + } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/options b/java/ql/test/experimental/query-tests/security/CWE-1004/options index 7f2b253fb20..d61a358d97f 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/options +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/options @@ -1 +1 @@ -// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jsr311-api-1.1.1 \ No newline at end of file +// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jsr311-api-1.1.1:${testdir}/../../../../stubs/springframework-5.2.3 \ No newline at end of file diff --git a/java/ql/test/stubs/springframework-5.2.3/org/springframework/security/web/csrf/CsrfToken.java b/java/ql/test/stubs/springframework-5.2.3/org/springframework/security/web/csrf/CsrfToken.java new file mode 100644 index 00000000000..bc59a2e496a --- /dev/null +++ b/java/ql/test/stubs/springframework-5.2.3/org/springframework/security/web/csrf/CsrfToken.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.csrf; + +import java.io.Serializable; + +/** + * Provides the information about an expected CSRF token. + * + * @author Rob Winch + * @since 3.2 + * @see DefaultCsrfToken + */ +public interface CsrfToken extends Serializable { + + /** + * Gets the HTTP header that the CSRF is populated on the response and can be placed + * on requests instead of the parameter. Cannot be null. + * @return the HTTP header that the CSRF is populated on the response and can be + * placed on requests instead of the parameter + */ + String getHeaderName(); + + /** + * Gets the HTTP parameter name that should contain the token. Cannot be null. + * @return the HTTP parameter name that should contain the token. + */ + String getParameterName(); + + /** + * Gets the token value. Cannot be null. + * @return the token value + */ + String getToken(); + +} From c8b1bc3a89b6f0c6b006449f38f4966899f0bc3d Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Thu, 11 Mar 2021 21:41:34 +0000 Subject: [PATCH 0041/1429] Enhance the query --- .../CWE-016/InsecureSpringActuatorConfig.ql | 58 +++++++------------ .../CWE/CWE-016/application.properties | 4 +- .../security/CWE-016/application.properties | 2 +- 3 files changed, 24 insertions(+), 40 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql index 2dc11e8e38e..06ba0d8a288 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql @@ -1,6 +1,7 @@ /** * @name Insecure Spring Boot Actuator Configuration - * @description Exposed Spring Boot Actuator through configuration files without declarative or procedural security enforcement leads to information leak or even remote code execution. + * @description Exposed Spring Boot Actuator through configuration files without declarative or procedural + * security enforcement leads to information leak or even remote code execution. * @kind problem * @id java/insecure-spring-actuator-config * @tags security @@ -9,7 +10,6 @@ import java import semmle.code.configfiles.ConfigFiles -import semmle.code.java.security.SensitiveActions import semmle.code.xml.MavenPom /** The parent node of the `org.springframework.boot` group. */ @@ -26,7 +26,10 @@ class SpringBootPom extends Pom { this.getADependency().getArtifact().getValue() = "spring-boot-starter-actuator" } - /** Holds if the Spring Boot Security module is used in the project, which brings in other security related libraries. */ + /** + * Holds if the Spring Boot Security module is used in the project, which brings in other security + * related libraries. + */ predicate isSpringBootSecurityUsed() { this.getADependency().getArtifact().getValue() = "spring-boot-starter-security" } @@ -38,14 +41,14 @@ class ApplicationProperties extends ConfigPair { } /** The configuration property `management.security.enabled`. */ -class ManagementSecurityEnabled extends ApplicationProperties { - ManagementSecurityEnabled() { this.getNameElement().getName() = "management.security.enabled" } +class ManagementSecurityConfig extends ApplicationProperties { + ManagementSecurityConfig() { this.getNameElement().getName() = "management.security.enabled" } - string getManagementSecurityEnabled() { result = this.getValueElement().getValue() } + string getValue() { result = this.getValueElement().getValue().trim() } - predicate hasSecurityDisabled() { getManagementSecurityEnabled() = "false" } + predicate hasSecurityDisabled() { getValue() = "false" } - predicate hasSecurityEnabled() { getManagementSecurityEnabled() = "true" } + predicate hasSecurityEnabled() { getValue() = "true" } } /** The configuration property `management.endpoints.web.exposure.include`. */ @@ -54,56 +57,37 @@ class ManagementEndPointInclude extends ApplicationProperties { this.getNameElement().getName() = "management.endpoints.web.exposure.include" } - string getManagementEndPointInclude() { result = this.getValueElement().getValue().trim() } -} - -/** The configuration property `management.endpoints.web.exposure.exclude`. */ -class ManagementEndPointExclude extends ApplicationProperties { - ManagementEndPointExclude() { - this.getNameElement().getName() = "management.endpoints.web.exposure.exclude" - } - - string getManagementEndPointExclude() { result = this.getValueElement().getValue().trim() } -} - -/** Holds if an application handles sensitive information judging by its variable names. */ -predicate isProtectedApp() { - exists(VarAccess va | va.getVariable().getName().regexpMatch(getCommonSensitiveInfoRegex())) + string getValue() { result = this.getValueElement().getValue().trim() } } from SpringBootPom pom, ApplicationProperties ap, Dependency d where - isProtectedApp() and pom.isSpringBootActuatorUsed() and not pom.isSpringBootSecurityUsed() and ap.getFile() .getParentContainer() .getAbsolutePath() .matches(pom.getFile().getParentContainer().getAbsolutePath() + "%") and // in the same sub-directory - exists(string s | s = pom.getParentElement().getVersionString() | - s.regexpMatch("1\\.[0|1|2|3|4].*") and - not exists(ManagementSecurityEnabled me | + exists(string springBootVersion | springBootVersion = pom.getParentElement().getVersionString() | + springBootVersion.regexpMatch("1\\.[0-4].*") and // version 1.0, 1.1, ..., 1.4 + not exists(ManagementSecurityConfig me | me.hasSecurityEnabled() and me.getFile() = ap.getFile() ) or - s.regexpMatch("1\\.5.*") and - exists(ManagementSecurityEnabled me | me.hasSecurityDisabled() and me.getFile() = ap.getFile()) + springBootVersion.matches("1.5%") and // version 1.5 + exists(ManagementSecurityConfig me | me.hasSecurityDisabled() and me.getFile() = ap.getFile()) or - s.regexpMatch("2.*") and + springBootVersion.matches("2.%") and //version 2.x exists(ManagementEndPointInclude mi | mi.getFile() = ap.getFile() and ( - mi.getManagementEndPointInclude() = "*" // all endpoints are enabled + mi.getValue() = "*" // all endpoints are enabled or - mi.getManagementEndPointInclude() + mi.getValue() .matches([ "%dump%", "%trace%", "%logfile%", "%shutdown%", "%startup%", "%mappings%", "%env%", "%beans%", "%sessions%" - ]) // all endpoints apart from '/health' and '/info' are considered sensitive - ) and - not exists(ManagementEndPointExclude mx | - mx.getFile() = ap.getFile() and - mx.getManagementEndPointExclude() = mi.getManagementEndPointInclude() + ]) // confidential endpoints to check although all endpoints apart from '/health' and '/info' are considered sensitive by Spring ) ) ) and diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/application.properties b/java/ql/src/experimental/Security/CWE/CWE-016/application.properties index aa489435a12..4f5defdd948 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-016/application.properties +++ b/java/ql/src/experimental/Security/CWE/CWE-016/application.properties @@ -7,7 +7,7 @@ # vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators management.security.enabled=false -# vulnerable configuration (spring boot 2+): exposes health and info only by default +# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything management.endpoints.web.exposure.include=* @@ -18,5 +18,5 @@ management.security.enabled=true # safe configuration (spring boot 1.5+): requires value false to expose sensitive actuators management.security.enabled=true -# safe configuration (spring boot 2+): exposes health and info only by default +# safe configuration (spring boot 2+): exposes health and info only by default, here overridden to expose one additional endpoint which we assume is intentional and safe. management.endpoints.web.exposure.include=beans,info,health diff --git a/java/ql/test/experimental/query-tests/security/CWE-016/application.properties b/java/ql/test/experimental/query-tests/security/CWE-016/application.properties index 95e704f3a1a..797906a3ca3 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-016/application.properties +++ b/java/ql/test/experimental/query-tests/security/CWE-016/application.properties @@ -5,7 +5,7 @@ # vulnerable configuration (spring boot 1.5+): requires value false to expose sensitive actuators management.security.enabled=false -# vulnerable configuration (spring boot 2+): exposes health and info only by default +# vulnerable configuration (spring boot 2+): exposes health and info only by default, here overridden to expose everything management.endpoints.web.exposure.include=* management.endpoints.web.exposure.exclude=beans From 1a2e341b7c767fc4b6e21e9dc38d25110aa8b50e Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 12 Mar 2021 12:19:37 +0000 Subject: [PATCH 0042/1429] Refactor the business logic of the query into a separate predicate --- .../CWE/CWE-016/InsecureSpringActuatorConfig.ql | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql index 06ba0d8a288..3acd22e767a 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql @@ -60,8 +60,11 @@ class ManagementEndPointInclude extends ApplicationProperties { string getValue() { result = this.getValueElement().getValue().trim() } } -from SpringBootPom pom, ApplicationProperties ap, Dependency d -where +/** + * Holds if `ApplicationProperties` ap of a repository managed by `SpringBootPom` pom + * has a vulnerable configuration of Spring Boot Actuator management endpoints. + */ +predicate hasConfidentialEndPointExposed(SpringBootPom pom, ApplicationProperties ap) { pom.isSpringBootActuatorUsed() and not pom.isSpringBootSecurityUsed() and ap.getFile() @@ -90,7 +93,12 @@ where ]) // confidential endpoints to check although all endpoints apart from '/health' and '/info' are considered sensitive by Spring ) ) - ) and + ) +} + +from SpringBootPom pom, ApplicationProperties ap, Dependency d +where + hasConfidentialEndPointExposed(pom, ap) and d = pom.getADependency() and d.getArtifact().getValue() = "spring-boot-starter-actuator" select d, "Insecure configuration of Spring Boot Actuator exposes sensitive endpoints." From 9b8056371fe5c17fdcf70c43a815ee6941cb4fa9 Mon Sep 17 00:00:00 2001 From: Taus Brock-Nannestad Date: Fri, 12 Mar 2021 13:51:24 +0100 Subject: [PATCH 0043/1429] Python: Make the type tracking implementation shareable --- .../experimental/typetracking/TypeTracker.qll | 393 ++++++++++++++++++ .../typetracking/TypeTrackerPrivate.qll | 95 +++++ 2 files changed, 488 insertions(+) create mode 100644 python/ql/src/experimental/typetracking/TypeTracker.qll create mode 100644 python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll diff --git a/python/ql/src/experimental/typetracking/TypeTracker.qll b/python/ql/src/experimental/typetracking/TypeTracker.qll new file mode 100644 index 00000000000..9ef29f46205 --- /dev/null +++ b/python/ql/src/experimental/typetracking/TypeTracker.qll @@ -0,0 +1,393 @@ +/** Step Summaries and Type Tracking */ + +private import TypeTrackerPrivate + +/** Any string that may appear as the name of a piece of content. */ +class ContentName extends string { + ContentName() { this = getPossibleContentName() } +} + +/** Either a content name, or the empty string (representing no content). */ +class OptionalContentName extends string { + OptionalContentName() { this instanceof ContentName or this = "" } +} + +/** + * A description of a step on an inter-procedural data flow path. + */ +private newtype TStepSummary = + LevelStep() or + CallStep() or + ReturnStep() or + StoreStep(ContentName content) or + LoadStep(ContentName content) + +/** + * INTERNAL: Use `TypeTracker` or `TypeBackTracker` instead. + * + * A description of a step on an inter-procedural data flow path. + */ +class StepSummary extends TStepSummary { + /** Gets a textual representation of this step summary. */ + string toString() { + this instanceof LevelStep and result = "level" + or + this instanceof CallStep and result = "call" + or + this instanceof ReturnStep and result = "return" + or + exists(string content | this = StoreStep(content) | result = "store " + content) + or + exists(string content | this = LoadStep(content) | result = "load " + content) + } +} + +/** Provides predicates for updating step summaries (`StepSummary`s). */ +module StepSummary { + /** + * Gets the summary that corresponds to having taken a forwards + * heap and/or inter-procedural step from `nodeFrom` to `nodeTo`. + */ + cached + predicate step(LocalSourceNode nodeFrom, LocalSourceNode nodeTo, StepSummary summary) { + exists(Node mid | nodeFrom.flowsTo(mid) and smallstep(mid, nodeTo, summary)) + } + + /** + * Gets the summary that corresponds to having taken a forwards + * local, heap and/or inter-procedural step from `nodeFrom` to `nodeTo`. + * + * Unlike `StepSummary::step`, this predicate does not compress + * type-preserving steps. + */ + predicate smallstep(Node nodeFrom, Node nodeTo, StepSummary summary) { + typePreservingStep(nodeFrom, nodeTo) and + summary = LevelStep() + or + callStep(nodeFrom, nodeTo) and summary = CallStep() + or + returnStep(nodeFrom, nodeTo) and + summary = ReturnStep() + or + exists(string content | + basicStoreStep(nodeFrom, nodeTo, content) and + summary = StoreStep(content) + or + basicLoadStep(nodeFrom, nodeTo, content) and summary = LoadStep(content) + ) + } +} + +/** Holds if it's reasonable to expect the data flow step from `nodeFrom` to `nodeTo` to preserve types. */ +private predicate typePreservingStep(Node nodeFrom, Node nodeTo) { + simpleLocalFlowStep(nodeFrom, nodeTo) or + jumpStep(nodeFrom, nodeTo) +} + +/** + * A utility class that is equivalent to `boolean` but does not require type joining. + */ +private class Boolean extends boolean { + Boolean() { this = true or this = false } +} + +private newtype TTypeTracker = MkTypeTracker(Boolean hasCall, OptionalContentName content) + +/** + * Summary of the steps needed to track a value to a given dataflow node. + * + * This can be used to track objects that implement a certain API in order to + * recognize calls to that API. Note that type-tracking does not by itself provide a + * source/sink relation, that is, it may determine that a node has a given type, + * but it won't determine where that type came from. + * + * It is recommended that all uses of this type are written in the following form, + * for tracking some type `myType`: + * ``` + * DataFlow::LocalSourceNode myType(DataFlow::TypeTracker t) { + * t.start() and + * result = < source of myType > + * or + * exists (DataFlow::TypeTracker t2 | + * result = myType(t2).track(t2, t) + * ) + * } + * + * DataFlow::Node myType() { myType(DataFlow::TypeTracker::end()).flowsTo(result) } + * ``` + * + * Instead of `result = myType(t2).track(t2, t)`, you can also use the equivalent + * `t = t2.step(myType(t2), result)`. If you additionally want to track individual + * intra-procedural steps, use `t = t2.smallstep(myCallback(t2), result)`. + */ +class TypeTracker extends TTypeTracker { + Boolean hasCall; + OptionalContentName content; + + TypeTracker() { this = MkTypeTracker(hasCall, content) } + + /** Gets the summary resulting from appending `step` to this type-tracking summary. */ + cached + TypeTracker append(StepSummary step) { + step = LevelStep() and result = this + or + step = CallStep() and result = MkTypeTracker(true, content) + or + step = ReturnStep() and hasCall = false and result = this + or + step = LoadStep(content) and result = MkTypeTracker(hasCall, "") + or + exists(string p | step = StoreStep(p) and content = "" and result = MkTypeTracker(hasCall, p)) + } + + /** Gets a textual representation of this summary. */ + string toString() { + exists(string withCall, string withContent | + (if hasCall = true then withCall = "with" else withCall = "without") and + (if content != "" then withContent = " with content " + content else withContent = "") and + result = "type tracker " + withCall + " call steps" + withContent + ) + } + + /** + * Holds if this is the starting point of type tracking. + */ + predicate start() { hasCall = false and content = "" } + + /** + * Holds if this is the starting point of type tracking, and the value starts in the content named `contentName`. + * The type tracking only ends after the content has been loaded. + */ + predicate startInContent(ContentName contentName) { hasCall = false and content = contentName } + + /** + * Holds if this is the starting point of type tracking + * when tracking a parameter into a call, but not out of it. + */ + predicate call() { hasCall = true and content = "" } + + /** + * Holds if this is the end point of type tracking. + */ + predicate end() { content = "" } + + /** + * INTERNAL. DO NOT USE. + * + * Holds if this type has been tracked into a call. + */ + boolean hasCall() { result = hasCall } + + /** + * INTERNAL. DO NOT USE. + * + * Gets the content associated with this type tracker. + */ + string getContent() { result = content } + + /** + * Gets a type tracker that starts where this one has left off to allow continued + * tracking. + * + * This predicate is only defined if the type is not associated to a piece of content. + */ + TypeTracker continue() { content = "" and result = this } + + /** + * Gets the summary that corresponds to having taken a forwards + * heap and/or inter-procedural step from `nodeFrom` to `nodeTo`. + */ + pragma[inline] + TypeTracker step(LocalSourceNode nodeFrom, Node nodeTo) { + exists(StepSummary summary | + StepSummary::step(nodeFrom, nodeTo, summary) and + result = this.append(summary) + ) + } + + /** + * Gets the summary that corresponds to having taken a forwards + * local, heap and/or inter-procedural step from `nodeFrom` to `nodeTo`. + * + * Unlike `TypeTracker::step`, this predicate exposes all edges + * in the flow graph, and not just the edges between `Node`s. + * It may therefore be less performant. + * + * Type tracking predicates using small steps typically take the following form: + * ```ql + * DataFlow::Node myType(DataFlow::TypeTracker t) { + * t.start() and + * result = < source of myType > + * or + * exists (DataFlow::TypeTracker t2 | + * t = t2.smallstep(myType(t2), result) + * ) + * } + * + * DataFlow::Node myType() { + * result = myType(DataFlow::TypeTracker::end()) + * } + * ``` + */ + pragma[inline] + TypeTracker smallstep(Node nodeFrom, Node nodeTo) { + exists(StepSummary summary | + StepSummary::smallstep(nodeFrom, nodeTo, summary) and + result = this.append(summary) + ) + or + typePreservingStep(nodeFrom, nodeTo) and + result = this + } +} + +/** Provides predicates for implementing custom `TypeTracker`s. */ +module TypeTracker { + /** + * Gets a valid end point of type tracking. + */ + TypeTracker end() { result.end() } +} + +private newtype TTypeBackTracker = MkTypeBackTracker(Boolean hasReturn, OptionalContentName content) + +/** + * Summary of the steps needed to back-track a use of a value to a given dataflow node. + * + * This can for example be used to track callbacks that are passed to a certain API, + * so we can model specific parameters of that callback as having a certain type. + * + * Note that type back-tracking does not provide a source/sink relation, that is, + * it may determine that a node will be used in an API call somewhere, but it won't + * determine exactly where that use was, or the path that led to the use. + * + * It is recommended that all uses of this type are written in the following form, + * for back-tracking some callback type `myCallback`: + * + * ``` + * DataFlow::LocalSourceNode myCallback(DataFlow::TypeBackTracker t) { + * t.start() and + * result = (< some API call >).getArgument(< n >).getALocalSource() + * or + * exists (DataFlow::TypeBackTracker t2 | + * result = myCallback(t2).backtrack(t2, t) + * ) + * } + * + * DataFlow::LocalSourceNode myCallback() { result = myCallback(DataFlow::TypeBackTracker::end()) } + * ``` + * + * Instead of `result = myCallback(t2).backtrack(t2, t)`, you can also use the equivalent + * `t2 = t.step(result, myCallback(t2))`. If you additionally want to track individual + * intra-procedural steps, use `t2 = t.smallstep(result, myCallback(t2))`. + */ +class TypeBackTracker extends TTypeBackTracker { + Boolean hasReturn; + string content; + + TypeBackTracker() { this = MkTypeBackTracker(hasReturn, content) } + + /** Gets the summary resulting from prepending `step` to this type-tracking summary. */ + TypeBackTracker prepend(StepSummary step) { + step = LevelStep() and result = this + or + step = CallStep() and hasReturn = false and result = this + or + step = ReturnStep() and result = MkTypeBackTracker(true, content) + or + exists(string p | + step = LoadStep(p) and content = "" and result = MkTypeBackTracker(hasReturn, p) + ) + or + step = StoreStep(content) and result = MkTypeBackTracker(hasReturn, "") + } + + /** Gets a textual representation of this summary. */ + string toString() { + exists(string withReturn, string withContent | + (if hasReturn = true then withReturn = "with" else withReturn = "without") and + (if content != "" then withContent = " with content " + content else withContent = "") and + result = "type back-tracker " + withReturn + " return steps" + withContent + ) + } + + /** + * Holds if this is the starting point of type tracking. + */ + predicate start() { hasReturn = false and content = "" } + + /** + * Holds if this is the end point of type tracking. + */ + predicate end() { content = "" } + + /** + * INTERNAL. DO NOT USE. + * + * Holds if this type has been back-tracked into a call through return edge. + */ + boolean hasReturn() { result = hasReturn } + + /** + * Gets a type tracker that starts where this one has left off to allow continued + * tracking. + * + * This predicate is only defined if the type has not been tracked into a piece of content. + */ + TypeBackTracker continue() { content = "" and result = this } + + /** + * Gets the summary that corresponds to having taken a backwards + * heap and/or inter-procedural step from `nodeTo` to `nodeFrom`. + */ + pragma[inline] + TypeBackTracker step(LocalSourceNode nodeFrom, LocalSourceNode nodeTo) { + exists(StepSummary summary | + StepSummary::step(nodeFrom, nodeTo, summary) and + this = result.prepend(summary) + ) + } + + /** + * Gets the summary that corresponds to having taken a backwards + * local, heap and/or inter-procedural step from `nodeTo` to `nodeFrom`. + * + * Unlike `TypeBackTracker::step`, this predicate exposes all edges + * in the flowgraph, and not just the edges between + * `LocalSourceNode`s. It may therefore be less performant. + * + * Type tracking predicates using small steps typically take the following form: + * ```ql + * DataFlow::Node myType(DataFlow::TypeBackTracker t) { + * t.start() and + * result = < some API call >.getArgument(< n >) + * or + * exists (DataFlow::TypeBackTracker t2 | + * t = t2.smallstep(result, myType(t2)) + * ) + * } + * + * DataFlow::Node myType() { + * result = myType(DataFlow::TypeBackTracker::end()) + * } + * ``` + */ + pragma[inline] + TypeBackTracker smallstep(Node nodeFrom, Node nodeTo) { + exists(StepSummary summary | + StepSummary::smallstep(nodeFrom, nodeTo, summary) and + this = result.prepend(summary) + ) + or + typePreservingStep(nodeFrom, nodeTo) and + this = result + } +} + +/** Provides predicates for implementing custom `TypeBackTracker`s. */ +module TypeBackTracker { + /** + * Gets a valid end point of type back-tracking. + */ + TypeBackTracker end() { result.end() } +} diff --git a/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll b/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll new file mode 100644 index 00000000000..f8f915f175d --- /dev/null +++ b/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll @@ -0,0 +1,95 @@ +private import python +private import semmle.python.dataflow.new.internal.DataFlowPublic as DataFlowPublic +private import semmle.python.dataflow.new.internal.DataFlowPrivate as DataFlowPrivate + +class Node = DataFlowPublic::Node; + +class LocalSourceNode = DataFlowPublic::LocalSourceNode; + +predicate jumpStep = DataFlowPrivate::jumpStep/2; + +predicate simpleLocalFlowStep = DataFlowPrivate::simpleLocalFlowStep/2; + +/** + * Gets the name of a possible piece of content. This will usually include things like + * + * - Attribute names (in Python) + * - Property names (in JavaScript) + */ +string getPossibleContentName() { result = any(DataFlowPublic::AttrRef a).getAttributeName() } + +/** + * Gets a callable for the call where `nodeFrom` is used as the `i`'th argument. + * + * Helper predicate to avoid bad join order experienced in `callStep`. + * This happened when `isParameterOf` was joined _before_ `getCallable`. + */ +pragma[nomagic] +private DataFlowPrivate::DataFlowCallable getCallableForArgument( + DataFlowPublic::ArgumentNode nodeFrom, int i +) { + exists(DataFlowPrivate::DataFlowCall call | + nodeFrom.argumentOf(call, i) and + result = call.getCallable() + ) +} + +/** Holds if `nodeFrom` steps to `nodeTo` by being passed as a parameter in a call. */ +predicate callStep(DataFlowPublic::ArgumentNode nodeFrom, DataFlowPublic::ParameterNode nodeTo) { + // TODO: Support special methods? + exists(DataFlowPrivate::DataFlowCallable callable, int i | + callable = getCallableForArgument(nodeFrom, i) and + nodeTo.isParameterOf(callable, i) + ) +} + +/** Holds if `nodeFrom` steps to `nodeTo` by being returned from a call. */ +predicate returnStep(DataFlowPrivate::ReturnNode nodeFrom, Node nodeTo) { + exists(DataFlowPrivate::DataFlowCall call | + nodeFrom.getEnclosingCallable() = call.getCallable() and nodeTo.asCfgNode() = call.getNode() + ) +} + +/** + * Holds if `nodeFrom` is being written to the `content` content of the object in `nodeTo`. + * + * Note that the choice of `nodeTo` does not have to make sense "chronologically". + * All we care about is whether the `content` content of `nodeTo` can have a specific type, + * and the assumption is that if a specific type appears here, then any access of that + * particular content can yield something of that particular type. + * + * Thus, in an example such as + * + * ```python + * def foo(y): + * x = Foo() + * bar(x) + * x.content = y + * baz(x) + * + * def bar(x): + * z = x.content + * ``` + * for the content write `x.content = y`, we will have `content` being the literal string `"content"`, + * `nodeFrom` will be `y`, and `nodeTo` will be the object `Foo()` created on the first line of the + * function. This means we will track the fact that `x.content` can have the type of `y` into the + * assignment to `z` inside `bar`, even though this content write happens _after_ `bar` is called. + */ +predicate basicStoreStep(Node nodeFrom, LocalSourceNode nodeTo, string content) { + exists(DataFlowPublic::AttrWrite a | + a.mayHaveAttributeName(content) and + nodeFrom = a.getValue() and + nodeTo.flowsTo(a.getObject()) + ) +} + +/** + * Holds if `nodeTo` is the result of accessing the `content` content of `nodeFrom`. + */ +predicate basicLoadStep(Node nodeFrom, Node nodeTo, string content) { + exists(DataFlowPublic::AttrRead a | + a.mayHaveAttributeName(content) and + nodeFrom = a.getObject() and + nodeTo = a + ) +} From f05313435d68b06b96e3172d823024f9beb1df49 Mon Sep 17 00:00:00 2001 From: Taus Brock-Nannestad Date: Fri, 12 Mar 2021 14:06:39 +0100 Subject: [PATCH 0044/1429] Python: Move `typePreservingStep` into `Private` --- python/ql/src/experimental/typetracking/TypeTracker.qll | 6 ------ .../src/experimental/typetracking/TypeTrackerPrivate.qll | 8 +++++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/python/ql/src/experimental/typetracking/TypeTracker.qll b/python/ql/src/experimental/typetracking/TypeTracker.qll index 9ef29f46205..f21d3a2a91b 100644 --- a/python/ql/src/experimental/typetracking/TypeTracker.qll +++ b/python/ql/src/experimental/typetracking/TypeTracker.qll @@ -78,12 +78,6 @@ module StepSummary { } } -/** Holds if it's reasonable to expect the data flow step from `nodeFrom` to `nodeTo` to preserve types. */ -private predicate typePreservingStep(Node nodeFrom, Node nodeTo) { - simpleLocalFlowStep(nodeFrom, nodeTo) or - jumpStep(nodeFrom, nodeTo) -} - /** * A utility class that is equivalent to `boolean` but does not require type joining. */ diff --git a/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll b/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll index f8f915f175d..d26a76b3355 100644 --- a/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll +++ b/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll @@ -6,9 +6,11 @@ class Node = DataFlowPublic::Node; class LocalSourceNode = DataFlowPublic::LocalSourceNode; -predicate jumpStep = DataFlowPrivate::jumpStep/2; - -predicate simpleLocalFlowStep = DataFlowPrivate::simpleLocalFlowStep/2; +/** Holds if it's reasonable to expect the data flow step from `nodeFrom` to `nodeTo` to preserve types. */ +predicate typePreservingStep(Node nodeFrom, Node nodeTo) { + DataFlowPrivate::simpleLocalFlowStep(nodeFrom, nodeTo) or + DataFlowPrivate::jumpStep(nodeFrom, nodeTo) +} /** * Gets the name of a possible piece of content. This will usually include things like From 41c9394b4bd12063e133222310a9facac689b330 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Sun, 14 Mar 2021 09:22:47 +0100 Subject: [PATCH 0045/1429] Python: update qhelp and example --- .../Security/CWE-327/InsecureProtocol.qhelp | 21 +++++++++++++++++++ .../examples/secure_default_protocol.py | 16 ++++++-------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp b/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp index 4a7364ca14e..cfcebd0930d 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp @@ -38,12 +38,33 @@ ssl.SSLContext, which is supported in Python 2.7.9 and 3.2 and later versions.

    +

    + Note that ssl.wrap_socket has been deprecated in + Python 3.7. The recommended alternatives are: +

    +
      +
    • ssl.SSLContext - supported in Python 2.7.9, + 3.2, and later versions
    • +
    • ssl.create_default_context - a convenience function, + supported in Python 3.4 and later versions.
    • +
    +

    + Even when you use these alternatives, you should + ensure that a safe protocol is used. The following code illustrates + how to use flags (available since Python 3.2) or the `minimum_version` + field (favored since Python 3.7) to restrict the protocols accepted when + creating a connection. +

    + +
  • Wikipedia: Transport Layer Security.
  • Python 3 documentation: class ssl.SSLContext.
  • Python 3 documentation: ssl.wrap_socket.
  • +
  • Python 3 documentation: notes on context creation.
  • +
  • Python 3 documentation: notes on security considerations.
  • pyOpenSSL documentation: An interface to the SSL-specific parts of OpenSSL.
  • diff --git a/python/ql/src/Security/CWE-327/examples/secure_default_protocol.py b/python/ql/src/Security/CWE-327/examples/secure_default_protocol.py index 83c6dbbba0e..535a97f0b93 100644 --- a/python/ql/src/Security/CWE-327/examples/secure_default_protocol.py +++ b/python/ql/src/Security/CWE-327/examples/secure_default_protocol.py @@ -1,13 +1,9 @@ -# taken from https://docs.python.org/3/library/ssl.html?highlight=ssl#ssl.SSLContext - -import socket import ssl -hostname = 'www.python.org' -context = ssl.create_default_context() -context.options |= ssl.OP_NO_TLSv1 # This added by me -context.options |= ssl.OP_NO_TLSv1_1 # This added by me +# Using flags to restrict the protocol +context = ssl.SSLContext() +context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 -with socket.create_connection((hostname, 443)) as sock: - with context.wrap_socket(sock, server_hostname=hostname) as ssock: - print(ssock.version()) +# Declaring a minimum version to restrict the protocol +context = ssl.create_default_context() +context.minimum_version = ssl.TLSVersion.TLSv1_2 From 4094b184074ca1c4b20e4ad599bef6d536f518c3 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Mon, 15 Mar 2021 16:28:08 +0100 Subject: [PATCH 0046/1429] Python: Clean up tests --- .../CWE-327/InsecureProtocol.expected | 10 ++--- .../Security/CWE-327/InsecureProtocol.py | 39 ++++++------------- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected index f4202a4634d..db30e41b480 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected @@ -5,11 +5,11 @@ | InsecureProtocol.py:11:1:11:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv3 specified by $@ | InsecureProtocol.py:11:1:11:39 | ControlFlowNode for SSLContext() | call to SSLContext | | InsecureProtocol.py:12:1:12:39 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version TLSv1 specified by $@ | InsecureProtocol.py:12:1:12:39 | ControlFlowNode for SSLContext() | call to SSLContext | | InsecureProtocol.py:14:1:14:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:14:1:14:29 | ControlFlowNode for Attribute() | call to SSL.Context | -| InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv3 specified by $@ | InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | call to SSL.Context | -| InsecureProtocol.py:17:1:17:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version TLSv1 specified by $@ | InsecureProtocol.py:17:1:17:29 | ControlFlowNode for Attribute() | call to SSL.Context | -| InsecureProtocol.py:32:1:32:19 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:32:1:32:19 | ControlFlowNode for Attribute() | call to SSL.Context | -| InsecureProtocol.py:48:1:48:43 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:48:1:48:43 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | -| InsecureProtocol.py:49:1:49:35 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:49:1:49:35 | ControlFlowNode for SSLContext() | call to SSLContext | +| InsecureProtocol.py:15:1:15:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv3 specified by $@ | InsecureProtocol.py:15:1:15:29 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version TLSv1 specified by $@ | InsecureProtocol.py:16:1:16:29 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:19:1:19:19 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:19:1:19:19 | ControlFlowNode for Attribute() | call to SSL.Context | +| InsecureProtocol.py:23:1:23:43 | ControlFlowNode for Attribute() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:23:1:23:43 | ControlFlowNode for Attribute() | call to ssl.wrap_socket | +| InsecureProtocol.py:24:1:24:35 | ControlFlowNode for SSLContext() | Insecure SSL/TLS protocol version SSLv2 specified by $@ | InsecureProtocol.py:24:1:24:35 | ControlFlowNode for SSLContext() | call to SSLContext | | pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv2 allowed by $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | | pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | | pyOpenSSL_fluent.py:8:27:8:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | pyOpenSSL_fluent.py:6:15:6:44 | ControlFlowNode for Attribute() | call to SSL.Context | diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py index cb21a6623c9..3ff1207b527 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py @@ -2,7 +2,7 @@ import ssl from OpenSSL import SSL from ssl import SSLContext -# true positives +# insecure versions specified ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) @@ -12,41 +12,26 @@ SSLContext(protocol=ssl.PROTOCOL_SSLv3) SSLContext(protocol=ssl.PROTOCOL_TLSv1) SSL.Context(SSL.SSLv2_METHOD) -SSL.Context(SSL.SSLv23_METHOD) SSL.Context(SSL.SSLv3_METHOD) SSL.Context(SSL.TLSv1_METHOD) -# not relevant -wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) -wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) -wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) - -Context(SSL.SSLv3_METHOD) -Context(SSL.TLSv1_METHOD) -Context(SSL.SSLv2_METHOD) -Context(SSL.SSLv23_METHOD) - -# true positive using flow - METHOD = SSL.SSLv2_METHOD SSL.Context(METHOD) -# secure versions +# importing the protocol constant directly +from ssl import PROTOCOL_SSLv2 +ssl.wrap_socket(ssl_version=PROTOCOL_SSLv2) +SSLContext(protocol=PROTOCOL_SSLv2) +# secure versions specified ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) SSLContext(protocol=ssl.PROTOCOL_TLSv1_2) SSL.Context(SSL.TLSv1_2_METHOD) -# possibly insecure default -ssl.wrap_socket() -context = SSLContext() +# possibly secure versions specified +SSLContext(protocol=ssl.PROTOCOL_SSLv23) +SSLContext(protocol=ssl.PROTOCOL_TLS) +SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) +SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER) -# importing the protocol constant directly - -from ssl import PROTOCOL_SSLv2 - -ssl.wrap_socket(ssl_version=PROTOCOL_SSLv2) -SSLContext(protocol=PROTOCOL_SSLv2) - -# FP for insecure default -ssl.SSLContext(ssl.SSLv23_METHOD) +SSL.Context(SSL.SSLv23_METHOD) From 731f4559b415dce07d8118707a42081bcba42c08 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Mon, 15 Mar 2021 17:23:58 +0100 Subject: [PATCH 0047/1429] Python: update test expectations --- .../Security/CWE-327/InsecureDefaultProtocol.expected | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureDefaultProtocol.expected b/python/ql/test/query-tests/Security/CWE-327/InsecureDefaultProtocol.expected index 1e389aefdc1..e69de29bb2d 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureDefaultProtocol.expected +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureDefaultProtocol.expected @@ -1,2 +0,0 @@ -| InsecureProtocol.py:41:1:41:17 | ControlFlowNode for Attribute() | Call to deprecated method ssl.wrap_socket does not specify a protocol, which may result in an insecure default being used. | -| InsecureProtocol.py:42:11:42:22 | ControlFlowNode for SSLContext() | Call to ssl.SSLContext does not specify a protocol, which may result in an insecure default being used. | From 87f3ba26843b8f861df47b9ca03226926aa58b5b Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Mon, 15 Mar 2021 17:24:39 +0100 Subject: [PATCH 0048/1429] Python: add tests for `ssl.PROTOCOL_TLS_SERVER` and `ssl.PROTOCOL_TLS_CLIENT` --- .../CWE-327/InsecureProtocol.expected | 36 +++++++++---------- .../Security/CWE-327/ssl_fluent.py | 18 ++++++++++ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected index db30e41b480..3f383ea5297 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected @@ -22,21 +22,21 @@ | ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:19:14:19:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:15:15:15:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:34:15:34:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:47:14:47:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:43:15:43:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:75:14:75:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:71:15:71:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:75:14:75:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:71:15:71:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:128:15:128:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:128:15:128:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:144:5:144:11 | ControlFlowNode for context | context modification | -| ssl_fluent.py:98:14:98:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:162:5:162:11 | ControlFlowNode for context | context modification | -| ssl_fluent.py:104:14:104:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:104:14:104:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:89:12:89:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | -| ssl_fluent.py:124:14:124:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:122:5:122:11 | ControlFlowNode for context | context modification | -| ssl_fluent.py:173:14:173:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:170:5:170:11 | ControlFlowNode for context | context modification | -| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | ssl_fluent.py:189:5:189:11 | ControlFlowNode for context | context modification | -| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:188:15:188:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | -| ssl_fluent.py:192:14:192:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:188:15:188:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | +| ssl_fluent.py:55:14:55:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:52:15:52:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:55:14:55:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:52:15:52:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:65:14:65:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:61:15:61:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:93:14:93:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:89:15:89:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:93:14:93:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:89:15:89:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:116:14:116:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:107:12:107:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:116:14:116:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:146:15:146:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:116:14:116:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:107:12:107:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:116:14:116:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:146:15:146:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:116:14:116:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:162:5:162:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:116:14:116:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:180:5:180:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:122:14:122:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:107:12:107:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:122:14:122:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:107:12:107:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:142:14:142:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:140:5:140:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:191:14:191:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:188:5:188:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:210:14:210:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | ssl_fluent.py:207:5:207:11 | ControlFlowNode for context | context modification | +| ssl_fluent.py:210:14:210:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:206:15:206:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | +| ssl_fluent.py:210:14:210:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:206:15:206:65 | ControlFlowNode for Attribute() | call to ssl.create_default_context | diff --git a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py index 577f342765e..ceddaa32f09 100644 --- a/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py +++ b/python/ql/test/query-tests/Security/CWE-327/ssl_fluent.py @@ -19,6 +19,24 @@ def test_fluent_tls_no_TLSv1(): with context.wrap_socket(sock, server_hostname=hostname) as ssock: print(ssock.version()) +def test_fluent_tls_client_no_TLSv1(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.options |= ssl.OP_NO_TLSv1 + + with socket.create_connection((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + +def test_fluent_tls_server_no_TLSv1(): + hostname = 'www.python.org' + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.options |= ssl.OP_NO_TLSv1 + + with socket.create_server((hostname, 443)) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as ssock: + print(ssock.version()) + def test_fluent_tls_safe(): hostname = 'www.python.org' context = ssl.SSLContext(ssl.PROTOCOL_TLS) From 514a69c47ad79318883483f159662503bc7358b0 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Mon, 15 Mar 2021 17:30:01 +0100 Subject: [PATCH 0049/1429] Python: Support `ssl.PROTOCOL_TLS_SERVER` and `ssl.PROTOCOL_TLS_CLIENT` --- python/ql/src/Security/CWE-327/Ssl.qll | 6 +++++- .../query-tests/Security/CWE-327/InsecureProtocol.expected | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index d4886219d08..be16138b961 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -146,7 +146,11 @@ class Ssl extends TlsLibrary { override string specific_version_name(ProtocolVersion version) { result = "PROTOCOL_" + version } - override string unspecific_version_name(ProtocolFamily family) { result = "PROTOCOL_" + family } + override string unspecific_version_name(ProtocolFamily family) { + family = "SSLv23" and result = "PROTOCOL_" + family + or + family = "TLS" and result = "PROTOCOL_" + family + ["", "_CLIENT", "_SERVER"] + } override API::Node version_constants() { result = API::moduleImport("ssl") } diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected index 3f383ea5297..e578a335d84 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected @@ -22,6 +22,8 @@ | ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:19:14:19:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:15:15:15:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:28:14:28:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:24:15:24:53 | ControlFlowNode for Attribute() | call to ssl.SSLContext | +| ssl_fluent.py:37:14:37:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:33:15:33:53 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:55:14:55:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:52:15:52:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:55:14:55:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:52:15:52:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:65:14:65:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:61:15:61:49 | ControlFlowNode for Attribute() | call to ssl.SSLContext | From 9a962305236c194d18d80d6321bf11ddc6eabe8b Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Mon, 15 Mar 2021 17:35:30 +0100 Subject: [PATCH 0050/1429] Python: Add changenote --- python/change-notes/2021-03-15-port-insecure-protocol.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 python/change-notes/2021-03-15-port-insecure-protocol.md diff --git a/python/change-notes/2021-03-15-port-insecure-protocol.md b/python/change-notes/2021-03-15-port-insecure-protocol.md new file mode 100644 index 00000000000..c92f387b29a --- /dev/null +++ b/python/change-notes/2021-03-15-port-insecure-protocol.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Ported use of insecure SSL/TLS version (`py/insecure-protocol`) query to use new data-flow library. This might result in different results, but overall a more robust and accurate analysis. From 98204a15a6ad8c8dd5dfb42269bfdd153cfc1827 Mon Sep 17 00:00:00 2001 From: haby0 Date: Wed, 17 Mar 2021 15:28:04 +0800 Subject: [PATCH 0051/1429] Fix the problem --- .../Security/CWE/CWE-352/JsonStringLib.qll | 2 +- .../Security/CWE/CWE-352/JsonpInjection.java | 117 ++++++---- .../Security/CWE/CWE-352/JsonpInjection.qhelp | 11 +- .../Security/CWE/CWE-352/JsonpInjection.ql | 24 ++- .../CWE/CWE-352/JsonpInjectionLib.qll | 66 +++--- .../Security/CWE/CWE-598/SensitiveGetQuery.ql | 10 - .../semmle/code/java/frameworks/Servlets.qll | 11 + .../security/CWE-352/JsonpInjection.qlref | 1 - .../JsonpController.java | 105 +++++++-- .../JsonpInjection.expected | 87 ++++++++ .../JsonpInjection.qlref | 1 + .../JsonpInjectionServlet1.java | 0 .../JsonpInjectionServlet2.java | 0 .../RefererFilter.java | 0 .../CWE-352/JsonpInjectionWithFilter/options | 1 + .../JsonpController.java | 107 +++++++-- .../JsonpInjection.expected | 76 +++++++ .../JsonpInjection.qlref | 1 + .../options | 1 + .../JsonpController.java | 203 ++++++++++++++++++ .../JsonpInjection.expected | 92 ++++++++ .../JsonpInjection.qlref | 1 + .../JsonpInjectionServlet1.java | 0 .../JsonpInjectionServlet2.java | 0 .../options | 1 + .../CWE-352/JsonpInjection_1.expected | 60 ------ .../CWE-352/JsonpInjection_2.expected | 78 ------- .../CWE-352/JsonpInjection_3.expected | 66 ------ .../query-tests/security/CWE-352/Readme | 3 - .../security/CWE-352/RefererFilter.java | 43 ---- .../query-tests/security/CWE-352/options | 1 - .../core/annotation/AliasFor.java | 13 +- .../core/io/InputStreamSource.java | 8 + .../org/springframework/core/io/Resource.java | 46 ++++ .../org/springframework/lang/Nullable.java | 13 ++ .../springframework/util/FileCopyUtils.java | 53 +++++ .../org/springframework/util/StringUtils.java | 2 +- .../web/bind/annotation/GetMapping.java | 36 +++- .../web/bind/annotation/Mapping.java | 4 + .../web/bind/annotation/RequestMapping.java | 19 +- .../web/bind/annotation/RequestParam.java | 23 ++ .../web/bind/annotation/ResponseBody.java | 9 + .../web/multipart/MultipartFile.java | 38 ++++ .../javax/servlet/annotation/WebServlet.java | 30 +++ 44 files changed, 1075 insertions(+), 388 deletions(-) delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.qlref rename java/ql/test/experimental/query-tests/security/CWE-352/{ => JsonpInjectionWithFilter}/JsonpController.java (54%) create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.qlref rename java/ql/{src/experimental/Security/CWE/CWE-352 => test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter}/JsonpInjectionServlet1.java (100%) rename java/ql/{src/experimental/Security/CWE/CWE-352 => test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter}/JsonpInjectionServlet2.java (100%) rename java/ql/{src/experimental/Security/CWE/CWE-352 => test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter}/RefererFilter.java (100%) create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/options rename java/ql/{src/experimental/Security/CWE/CWE-352 => test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController}/JsonpController.java (54%) create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.qlref create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/options create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpController.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.qlref rename java/ql/test/experimental/query-tests/security/CWE-352/{ => JsonpInjectionWithSpringControllerAndServlet}/JsonpInjectionServlet1.java (100%) rename java/ql/test/experimental/query-tests/security/CWE-352/{ => JsonpInjectionWithSpringControllerAndServlet}/JsonpInjectionServlet2.java (100%) create mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/options delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_1.expected delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_2.expected delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_3.expected delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/Readme delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/RefererFilter.java delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-352/options create mode 100644 java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/io/InputStreamSource.java create mode 100644 java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/io/Resource.java create mode 100644 java/ql/test/stubs/spring-core-5.3.2/org/springframework/lang/Nullable.java create mode 100644 java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/FileCopyUtils.java create mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/Mapping.java create mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestParam.java create mode 100644 java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/multipart/MultipartFile.java create mode 100644 java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/annotation/WebServlet.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonStringLib.qll b/java/ql/src/experimental/Security/CWE/CWE-352/JsonStringLib.qll index 0da8bc860d1..5cc52e97e33 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonStringLib.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonStringLib.qll @@ -3,7 +3,7 @@ import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.FlowSources import DataFlow::PathGraph -/** Json string type data */ +/** Json string type data. */ abstract class JsonpStringSource extends DataFlow::Node { } /** Convert to String using Gson library. */ diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java index 8b4e7cc005e..7f479a8c023 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java @@ -1,31 +1,39 @@ import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.HashMap; -import java.util.Random; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; @Controller public class JsonpInjection { -private static HashMap hashMap = new HashMap(); + + private static HashMap hashMap = new HashMap(); static { hashMap.put("username","admin"); hashMap.put("password","123456"); } + private String name = null; + @GetMapping(value = "jsonp1") @ResponseBody public String bad1(HttpServletRequest request) { String resultStr = null; String jsonpCallback = request.getParameter("jsonpCallback"); - Gson gson = new Gson(); String result = gson.toJson(hashMap); resultStr = jsonpCallback + "(" + result + ")"; @@ -37,9 +45,7 @@ private static HashMap hashMap = new HashMap(); public String bad2(HttpServletRequest request) { String resultStr = null; String jsonpCallback = request.getParameter("jsonpCallback"); - resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; - return resultStr; } @@ -67,7 +73,6 @@ private static HashMap hashMap = new HashMap(); @ResponseBody public void bad5(HttpServletRequest request, HttpServletResponse response) throws Exception { - response.setContentType("application/json"); String jsonpCallback = request.getParameter("jsonpCallback"); PrintWriter pw = null; Gson gson = new Gson(); @@ -83,7 +88,6 @@ private static HashMap hashMap = new HashMap(); @ResponseBody public void bad6(HttpServletRequest request, HttpServletResponse response) throws Exception { - response.setContentType("application/json"); String jsonpCallback = request.getParameter("jsonpCallback"); PrintWriter pw = null; ObjectMapper mapper = new ObjectMapper(); @@ -94,60 +98,96 @@ private static HashMap hashMap = new HashMap(); pw.println(resultStr); } - @GetMapping(value = "jsonp7") + @RequestMapping(value = "jsonp7", method = RequestMethod.GET) @ResponseBody - public String good(HttpServletRequest request) { + public String bad7(HttpServletRequest request) { String resultStr = null; String jsonpCallback = request.getParameter("jsonpCallback"); - - String val = ""; - Random random = new Random(); - for (int i = 0; i < 10; i++) { - val += String.valueOf(random.nextInt(10)); - } - // good - jsonpCallback = jsonpCallback + "_" + val; - String jsonStr = getJsonStr(hashMap); - resultStr = jsonpCallback + "(" + jsonStr + ")"; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; return resultStr; } + @GetMapping(value = "jsonp8") @ResponseBody public String good1(HttpServletRequest request) { String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - String token = request.getParameter("token"); - - // good if (verifToken(token)){ - System.out.println(token); + String jsonpCallback = request.getParameter("jsonpCallback"); String jsonStr = getJsonStr(hashMap); resultStr = jsonpCallback + "(" + jsonStr + ")"; return resultStr; } - return "error"; } + @GetMapping(value = "jsonp9") @ResponseBody public String good2(HttpServletRequest request) { String resultStr = null; - String jsonpCallback = request.getParameter("jsonpCallback"); - - String referer = request.getHeader("Referer"); - - boolean result = verifReferer(referer); - // good + String token = request.getParameter("token"); + boolean result = verifToken(token); if (result){ - String jsonStr = getJsonStr(hashMap); - resultStr = jsonpCallback + "(" + jsonStr + ")"; - return resultStr; + return ""; } + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } - return "error"; + @RequestMapping(value = "jsonp10") + @ResponseBody + public String good3(HttpServletRequest request) { + JSONObject parameterObj = readToJSONObect(request); + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @RequestMapping(value = "jsonp11") + @ResponseBody + public String good4(@RequestParam("file") MultipartFile file,HttpServletRequest request) { + if(null == file){ + return "upload file error"; + } + String fileName = file.getOriginalFilename(); + System.out.println("file operations"); + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + public static JSONObject readToJSONObect(HttpServletRequest request){ + String jsonText = readPostContent(request); + JSONObject jsonObj = JSONObject.parseObject(jsonText, JSONObject.class); + return jsonObj; + } + + public static String readPostContent(HttpServletRequest request){ + BufferedReader in= null; + String content = null; + String line = null; + try { + in = new BufferedReader(new InputStreamReader(request.getInputStream(),"UTF-8")); + StringBuilder buf = new StringBuilder(); + while ((line = in.readLine()) != null) { + buf.append(line); + } + content = buf.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + String uri = request.getRequestURI(); + return content; } public static String getJsonStr(Object result) { @@ -160,11 +200,4 @@ private static HashMap hashMap = new HashMap(); } return true; } - - public static boolean verifReferer(String referer){ - if (!referer.startsWith("http://test.com/")){ - return false; - } - return true; - } } \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp index bb5d628ac0b..93c167d6c2c 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp @@ -3,18 +3,21 @@ "qhelp.dtd"> -

    The software uses external input as the function name to wrap JSON data and return it to the client as a request response. When there is a cross-domain problem, -there is a problem of sensitive information leakage.

    +

    The software uses external input as the function name to wrap JSON data and returns it to the client as a request response. +When there is a cross-domain problem, the problem of sensitive information leakage may occur.

    -

    Adding `Referer` or random `token` verification processing can effectively prevent the leakage of sensitive information.

    +

    Adding Referer/Origin or random token verification processing can effectively prevent the leakage of sensitive information.

    -

    The following example shows the case of no verification processing and verification processing for the external input function name.

    +

    The following examples show the bad case and the good case respectively. Bad case, such as bad1 to bad7, +will cause information leakage problems when there are cross-domain problems. In a good case, for example, in the good1 +method and the good2 method, use the verifToken method to do the random token Verification can +solve the problem of information leakage caused by cross-domain.

    diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql index f3ae25daa03..068469328ea 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql @@ -14,25 +14,25 @@ import java import JsonpInjectionLib import semmle.code.java.dataflow.FlowSources import semmle.code.java.deadcode.WebEntryPoints -import semmle.code.java.security.XSS import DataFlow::PathGraph /** Determine whether there is a verification method for the remote streaming source data flow path method. */ predicate existsFilterVerificationMethod() { - exists(MethodAccess ma,Node existsNode, Method m| + exists(MethodAccess ma, Node existsNode, Method m | ma.getMethod() instanceof VerificationMethodClass and existsNode.asExpr() = ma and - m = getAnMethod(existsNode.getEnclosingCallable()) and + m = getACallingCallableOrSelf(existsNode.getEnclosingCallable()) and isDoFilterMethod(m) ) } /** Determine whether there is a verification method for the remote streaming source data flow path method. */ predicate existsServletVerificationMethod(Node checkNode) { - exists(MethodAccess ma,Node existsNode| + exists(MethodAccess ma, Node existsNode | ma.getMethod() instanceof VerificationMethodClass and existsNode.asExpr() = ma and - getAnMethod(existsNode.getEnclosingCallable()) = getAnMethod(checkNode.getEnclosingCallable()) + getACallingCallableOrSelf(existsNode.getEnclosingCallable()) = + getACallingCallableOrSelf(checkNode.getEnclosingCallable()) ) } @@ -40,13 +40,15 @@ predicate existsServletVerificationMethod(Node checkNode) { class RequestResponseFlowConfig extends TaintTracking::Configuration { RequestResponseFlowConfig() { this = "RequestResponseFlowConfig" } - override predicate isSource(DataFlow::Node source) { - source instanceof RemoteFlowSource and - getAnMethod(source.getEnclosingCallable()) instanceof RequestGetMethod - } + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { sink instanceof XssSink } + /** Eliminate the method of calling the node is not the get method. */ + override predicate isSanitizer(DataFlow::Node node) { + not getACallingCallableOrSelf(node.getEnclosingCallable()) instanceof RequestGetMethod + } + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { exists(MethodAccess ma | isRequestGetParamMethod(ma) and pred.asExpr() = ma.getQualifier() and succ.asExpr() = ma @@ -60,5 +62,5 @@ where not existsFilterVerificationMethod() and conf.hasFlowPath(source, sink) and exists(JsonpInjectionFlowConfig jhfc | jhfc.hasFlowTo(sink.getNode())) -select sink.getNode(), source, sink, "Jsonp Injection query might include code from $@.", - source.getNode(), "this user input" \ No newline at end of file +select sink.getNode(), source, sink, "Jsonp response might include code from $@.", source.getNode(), + "this user input" diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll index b8964524a9f..d0e00bcb634 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll @@ -6,28 +6,25 @@ import semmle.code.java.dataflow.DataFlow import semmle.code.java.dataflow.FlowSources import semmle.code.java.frameworks.spring.SpringController -/** Taint-tracking configuration tracing flow from user-controllable function name jsonp data to output jsonp data. */ +/** Taint-tracking configuration tracing flow from untrusted inputs to verification of remote user input. */ class VerificationMethodFlowConfig extends TaintTracking::Configuration { VerificationMethodFlowConfig() { this = "VerificationMethodFlowConfig" } override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { - exists(MethodAccess ma, BarrierGuard bg | + exists(MethodAccess ma | ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and - bg = ma and - sink.asExpr() = ma.getAnArgument() + ma.getAnArgument() = sink.asExpr() ) } } -/** The parameter name of the method is `token`, `auth`, `referer`, `origin`. */ +/** The parameter names of this method are token/auth/referer/origin. */ class VerificationMethodClass extends Method { VerificationMethodClass() { - exists(MethodAccess ma, BarrierGuard bg, VerificationMethodFlowConfig vmfc, Node node | + exists(MethodAccess ma, VerificationMethodFlowConfig vmfc, Node node | this = ma.getMethod() and - this.getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and - bg = ma and node.asExpr() = ma.getAnArgument() and vmfc.hasFlowTo(node) ) @@ -35,38 +32,43 @@ class VerificationMethodClass extends Method { } /** Get Callable by recursive method. */ -Callable getAnMethod(Callable call) { +Callable getACallingCallableOrSelf(Callable call) { result = call or - result = getAnMethod(call.getAReference().getEnclosingCallable()) + result = getACallingCallableOrSelf(call.getAReference().getEnclosingCallable()) } abstract class RequestGetMethod extends Method { } -/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ +/** Override method of `doGet` of `Servlet` subclass. */ private class ServletGetMethod extends RequestGetMethod { - ServletGetMethod() { - exists(Method m | - m = this and - isServletRequestMethod(m) and - m.getName() = "doGet" - ) + ServletGetMethod() { this instanceof DoGetServletMethod } +} + +/** The method of SpringController class processing `get` request. */ +abstract class SpringControllerGetMethod extends RequestGetMethod { } + +/** Method using `GetMapping` annotation in SpringController class. */ +class SpringControllerGetMappingGetMethod extends SpringControllerGetMethod { + SpringControllerGetMappingGetMethod() { + this.getAnAnnotation() + .getType() + .hasQualifiedName("org.springframework.web.bind.annotation", "GetMapping") } } -/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ -private class SpringControllerGetMethod extends RequestGetMethod { - SpringControllerGetMethod() { - exists(Annotation a | - a = this.getAnAnnotation() and - a.getType().hasQualifiedName("org.springframework.web.bind.annotation", "GetMapping") - ) - or - exists(Annotation a | - a = this.getAnAnnotation() and - a.getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestMapping") and - a.getValue("method").toString().regexpMatch("RequestMethod.GET|\\{...\\}") - ) +/** The method that uses the `RequestMapping` annotation in the SpringController class and only handles the get request. */ +class SpringControllerRequestMappingGetMethod extends SpringControllerGetMethod { + SpringControllerRequestMappingGetMethod() { + this.getAnAnnotation() + .getType() + .hasQualifiedName("org.springframework.web.bind.annotation", "RequestMapping") and + this.getAnAnnotation().getValue("method").toString().regexpMatch("RequestMethod.GET|\\{...\\}") and + not exists(MethodAccess ma | + ma.getMethod() instanceof ServletRequestGetBodyMethod and + this = getACallingCallableOrSelf(ma.getEnclosingCallable()) + ) and + not this.getAParamType().getName() = "MultipartFile" } } @@ -83,12 +85,12 @@ class JsonpInjectionExpr extends AddExpr { .regexpMatch("\"\\(\"") } - /** Get the jsonp function name of this expression */ + /** Get the jsonp function name of this expression. */ Expr getFunctionName() { result = getLeftOperand().(AddExpr).getLeftOperand().(AddExpr).getLeftOperand() } - /** Get the json data of this expression */ + /** Get the json data of this expression. */ Expr getJsonExpr() { result = getLeftOperand().(AddExpr).getRightOperand() } } diff --git a/java/ql/src/experimental/Security/CWE/CWE-598/SensitiveGetQuery.ql b/java/ql/src/experimental/Security/CWE/CWE-598/SensitiveGetQuery.ql index bc9850cfddb..c381595af14 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-598/SensitiveGetQuery.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-598/SensitiveGetQuery.ql @@ -23,16 +23,6 @@ class SensitiveInfoExpr extends Expr { } } -/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ -private predicate isGetServletMethod(Method m) { - isServletRequestMethod(m) and m.getName() = "doGet" -} - -/** The `doGet` method of `HttpServlet`. */ -class DoGetServletMethod extends Method { - DoGetServletMethod() { isGetServletMethod(this) } -} - /** Holds if `ma` is (perhaps indirectly) called from the `doGet` method of `HttpServlet`. */ predicate isReachableFromServletDoGet(MethodAccess ma) { ma.getEnclosingCallable() instanceof DoGetServletMethod diff --git a/java/ql/src/semmle/code/java/frameworks/Servlets.qll b/java/ql/src/semmle/code/java/frameworks/Servlets.qll index 5cccf62122f..7ac452affa9 100644 --- a/java/ql/src/semmle/code/java/frameworks/Servlets.qll +++ b/java/ql/src/semmle/code/java/frameworks/Servlets.qll @@ -354,9 +354,20 @@ class FilterChain extends Interface { /** Holds if `m` is a filter handler method (for example `doFilter`). */ predicate isDoFilterMethod(Method m) { + m.getName().matches("doFilter") and m.getDeclaringType() instanceof FilterClass and m.getNumberOfParameters() = 3 and m.getParameter(0).getType() instanceof ServletRequest and m.getParameter(1).getType() instanceof ServletResponse and m.getParameter(2).getType() instanceof FilterChain } + +/** Holds if `m` is a method of some override of `HttpServlet.doGet`. */ +predicate isGetServletMethod(Method m) { + isServletRequestMethod(m) and m.getName() = "doGet" +} + +/** The `doGet` method of `HttpServlet`. */ +class DoGetServletMethod extends Method { + DoGetServletMethod() { isGetServletMethod(this) } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.qlref deleted file mode 100644 index 6ad4b8acda7..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection.qlref +++ /dev/null @@ -1 +0,0 @@ -Security/CWE/CWE-352/JsonpInjection.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpController.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpController.java similarity index 54% rename from java/ql/test/experimental/query-tests/security/CWE-352/JsonpController.java rename to java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpController.java index cf860c75640..e5b5e70a38d 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpController.java +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpController.java @@ -1,16 +1,24 @@ import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.HashMap; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; @Controller public class JsonpController { + private static HashMap hashMap = new HashMap(); static { @@ -18,13 +26,14 @@ public class JsonpController { hashMap.put("password","123456"); } + private String name = null; - @GetMapping(value = "jsonp1", produces="text/javascript") + + @GetMapping(value = "jsonp1") @ResponseBody public String bad1(HttpServletRequest request) { String resultStr = null; String jsonpCallback = request.getParameter("jsonpCallback"); - Gson gson = new Gson(); String result = gson.toJson(hashMap); resultStr = jsonpCallback + "(" + result + ")"; @@ -36,9 +45,7 @@ public class JsonpController { public String bad2(HttpServletRequest request) { String resultStr = null; String jsonpCallback = request.getParameter("jsonpCallback"); - resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; - return resultStr; } @@ -91,23 +98,98 @@ public class JsonpController { pw.println(resultStr); } - @GetMapping(value = "jsonp7") + @RequestMapping(value = "jsonp7", method = RequestMethod.GET) + @ResponseBody + public String bad7(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; + return resultStr; + } + + + @GetMapping(value = "jsonp8") @ResponseBody public String good1(HttpServletRequest request) { String resultStr = null; - String token = request.getParameter("token"); - if (verifToken(token)){ String jsonpCallback = request.getParameter("jsonpCallback"); String jsonStr = getJsonStr(hashMap); resultStr = jsonpCallback + "(" + jsonStr + ")"; return resultStr; } - return "error"; } + + @GetMapping(value = "jsonp9") + @ResponseBody + public String good2(HttpServletRequest request) { + String resultStr = null; + String token = request.getParameter("token"); + boolean result = verifToken(token); + if (result){ + return ""; + } + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @RequestMapping(value = "jsonp10") + @ResponseBody + public String good3(HttpServletRequest request) { + JSONObject parameterObj = readToJSONObect(request); + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @RequestMapping(value = "jsonp11") + @ResponseBody + public String good4(@RequestParam("file") MultipartFile file,HttpServletRequest request) { + if(null == file){ + return "upload file error"; + } + String fileName = file.getOriginalFilename(); + System.out.println("file operations"); + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + public static JSONObject readToJSONObect(HttpServletRequest request){ + String jsonText = readPostContent(request); + JSONObject jsonObj = JSONObject.parseObject(jsonText, JSONObject.class); + return jsonObj; + } + + public static String readPostContent(HttpServletRequest request){ + BufferedReader in= null; + String content = null; + String line = null; + try { + in = new BufferedReader(new InputStreamReader(request.getInputStream(),"UTF-8")); + StringBuilder buf = new StringBuilder(); + while ((line = in.readLine()) != null) { + buf.append(line); + } + content = buf.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + String uri = request.getRequestURI(); + return content; + } + public static String getJsonStr(Object result) { return JSONObject.toJSONString(result); } @@ -118,11 +200,4 @@ public class JsonpController { } return true; } - - public static boolean verifReferer(String referer){ - if (!referer.startsWith("http://test.com/")){ - return false; - } - return true; - } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.expected new file mode 100644 index 00000000000..501565f2b4e --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.expected @@ -0,0 +1,87 @@ +edges +| JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | +| JsonpController.java:39:21:39:54 | ... + ... : String | JsonpController.java:40:16:40:24 | resultStr | +| JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | +| JsonpController.java:48:21:48:80 | ... + ... : String | JsonpController.java:49:16:49:24 | resultStr | +| JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | +| JsonpController.java:58:21:58:55 | ... + ... : String | JsonpController.java:59:16:59:24 | resultStr | +| JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | +| JsonpController.java:68:21:68:54 | ... + ... : String | JsonpController.java:69:16:69:24 | resultStr | +| JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | +| JsonpController.java:83:21:83:54 | ... + ... : String | JsonpController.java:84:20:84:28 | resultStr | +| JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | +| JsonpController.java:97:21:97:54 | ... + ... : String | JsonpController.java:98:20:98:28 | resultStr | +| JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | +| JsonpController.java:108:21:108:54 | ... + ... : String | JsonpController.java:109:16:109:24 | resultStr | +| JsonpController.java:117:24:117:52 | getParameter(...) : String | JsonpController.java:118:24:118:28 | token | +| JsonpController.java:119:36:119:72 | getParameter(...) : String | JsonpController.java:122:20:122:28 | resultStr | +| JsonpController.java:121:25:121:59 | ... + ... : String | JsonpController.java:122:20:122:28 | resultStr | +| JsonpController.java:132:24:132:52 | getParameter(...) : String | JsonpController.java:133:37:133:41 | token | +| JsonpController.java:137:32:137:68 | getParameter(...) : String | JsonpController.java:140:16:140:24 | resultStr | +| JsonpController.java:139:21:139:55 | ... + ... : String | JsonpController.java:140:16:140:24 | resultStr | +| JsonpController.java:150:21:150:54 | ... + ... : String | JsonpController.java:151:16:151:24 | resultStr | +| JsonpController.java:165:21:165:54 | ... + ... : String | JsonpController.java:166:16:166:24 | resultStr | +| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | +| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | JsonpInjectionServlet1.java:38:39:38:45 | referer | +| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | +| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | +| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | +| RefererFilter.java:22:26:22:53 | getHeader(...) : String | RefererFilter.java:23:39:23:45 | refefer | +nodes +| JsonpController.java:36:32:36:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:39:21:39:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:47:32:47:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:48:21:48:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:56:32:56:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:58:21:58:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:66:32:66:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:68:21:68:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:76:32:76:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:83:21:83:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:91:32:91:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:97:21:97:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:105:32:105:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:108:21:108:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:117:24:117:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:118:24:118:28 | token | semmle.label | token | +| JsonpController.java:119:36:119:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:121:25:121:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:132:24:132:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:133:37:133:41 | token | semmle.label | token | +| JsonpController.java:137:32:137:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:139:21:139:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:150:21:150:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:151:16:151:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:165:21:165:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:166:16:166:24 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | semmle.label | getHeader(...) : String | +| JsonpInjectionServlet1.java:38:39:38:45 | referer | semmle.label | referer | +| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | +| RefererFilter.java:22:26:22:53 | getHeader(...) : String | semmle.label | getHeader(...) : String | +| RefererFilter.java:23:39:23:45 | refefer | semmle.label | refefer | +#select diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.qlref new file mode 100644 index 00000000000..3f5fc450669 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-352/JsonpInjection.ql diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionServlet1.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjectionServlet1.java similarity index 100% rename from java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionServlet1.java rename to java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjectionServlet1.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionServlet2.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjectionServlet2.java similarity index 100% rename from java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionServlet2.java rename to java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjectionServlet2.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/RefererFilter.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/RefererFilter.java similarity index 100% rename from java/ql/src/experimental/Security/CWE/CWE-352/RefererFilter.java rename to java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/RefererFilter.java diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/options b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/options new file mode 100644 index 00000000000..c53e31e467f --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/options @@ -0,0 +1 @@ +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../stubs/apache-http-4.4.13/:${testdir}/../../../../../stubs/servlet-api-2.4:${testdir}/../../../../../stubs/fastjson-1.2.74/:${testdir}/../../../../../stubs/gson-2.8.6/:${testdir}/../../../../../stubs/jackson-databind-2.10/:${testdir}/../../../../../stubs/spring-context-5.3.2/:${testdir}/../../../../../stubs/spring-web-5.3.2/:${testdir}/../../../../../stubs/spring-core-5.3.2/:${testdir}/../../../../../stubs/tomcat-embed-core-9.0.41/ diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpController.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpController.java similarity index 54% rename from java/ql/src/experimental/Security/CWE/CWE-352/JsonpController.java rename to java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpController.java index 84a172a7aeb..e5b5e70a38d 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpController.java +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpController.java @@ -1,16 +1,24 @@ import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.HashMap; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; @Controller public class JsonpController { + private static HashMap hashMap = new HashMap(); static { @@ -18,13 +26,14 @@ public class JsonpController { hashMap.put("password","123456"); } + private String name = null; - @GetMapping(value = "jsonp1", produces="text/javascript") + + @GetMapping(value = "jsonp1") @ResponseBody public String bad1(HttpServletRequest request) { String resultStr = null; String jsonpCallback = request.getParameter("jsonpCallback"); - Gson gson = new Gson(); String result = gson.toJson(hashMap); resultStr = jsonpCallback + "(" + result + ")"; @@ -36,9 +45,7 @@ public class JsonpController { public String bad2(HttpServletRequest request) { String resultStr = null; String jsonpCallback = request.getParameter("jsonpCallback"); - resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; - return resultStr; } @@ -91,23 +98,98 @@ public class JsonpController { pw.println(resultStr); } - @GetMapping(value = "jsonp7") + @RequestMapping(value = "jsonp7", method = RequestMethod.GET) + @ResponseBody + public String bad7(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; + return resultStr; + } + + + @GetMapping(value = "jsonp8") @ResponseBody public String good1(HttpServletRequest request) { String resultStr = null; - String token = request.getParameter("token"); - if (verifToken(token)){ String jsonpCallback = request.getParameter("jsonpCallback"); String jsonStr = getJsonStr(hashMap); resultStr = jsonpCallback + "(" + jsonStr + ")"; return resultStr; } - return "error"; } + + @GetMapping(value = "jsonp9") + @ResponseBody + public String good2(HttpServletRequest request) { + String resultStr = null; + String token = request.getParameter("token"); + boolean result = verifToken(token); + if (result){ + return ""; + } + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @RequestMapping(value = "jsonp10") + @ResponseBody + public String good3(HttpServletRequest request) { + JSONObject parameterObj = readToJSONObect(request); + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @RequestMapping(value = "jsonp11") + @ResponseBody + public String good4(@RequestParam("file") MultipartFile file,HttpServletRequest request) { + if(null == file){ + return "upload file error"; + } + String fileName = file.getOriginalFilename(); + System.out.println("file operations"); + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + public static JSONObject readToJSONObect(HttpServletRequest request){ + String jsonText = readPostContent(request); + JSONObject jsonObj = JSONObject.parseObject(jsonText, JSONObject.class); + return jsonObj; + } + + public static String readPostContent(HttpServletRequest request){ + BufferedReader in= null; + String content = null; + String line = null; + try { + in = new BufferedReader(new InputStreamReader(request.getInputStream(),"UTF-8")); + StringBuilder buf = new StringBuilder(); + while ((line = in.readLine()) != null) { + buf.append(line); + } + content = buf.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + String uri = request.getRequestURI(); + return content; + } + public static String getJsonStr(Object result) { return JSONObject.toJSONString(result); } @@ -118,11 +200,4 @@ public class JsonpController { } return true; } - - public static boolean verifReferer(String referer){ - if (!referer.startsWith("http://test.com/")){ - return false; - } - return true; - } -} \ No newline at end of file +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.expected new file mode 100644 index 00000000000..91d23cebbda --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.expected @@ -0,0 +1,76 @@ +edges +| JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | +| JsonpController.java:39:21:39:54 | ... + ... : String | JsonpController.java:40:16:40:24 | resultStr | +| JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | +| JsonpController.java:48:21:48:80 | ... + ... : String | JsonpController.java:49:16:49:24 | resultStr | +| JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | +| JsonpController.java:58:21:58:55 | ... + ... : String | JsonpController.java:59:16:59:24 | resultStr | +| JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | +| JsonpController.java:68:21:68:54 | ... + ... : String | JsonpController.java:69:16:69:24 | resultStr | +| JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | +| JsonpController.java:83:21:83:54 | ... + ... : String | JsonpController.java:84:20:84:28 | resultStr | +| JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | +| JsonpController.java:97:21:97:54 | ... + ... : String | JsonpController.java:98:20:98:28 | resultStr | +| JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | +| JsonpController.java:108:21:108:54 | ... + ... : String | JsonpController.java:109:16:109:24 | resultStr | +| JsonpController.java:117:24:117:52 | getParameter(...) : String | JsonpController.java:118:24:118:28 | token | +| JsonpController.java:119:36:119:72 | getParameter(...) : String | JsonpController.java:122:20:122:28 | resultStr | +| JsonpController.java:121:25:121:59 | ... + ... : String | JsonpController.java:122:20:122:28 | resultStr | +| JsonpController.java:132:24:132:52 | getParameter(...) : String | JsonpController.java:133:37:133:41 | token | +| JsonpController.java:137:32:137:68 | getParameter(...) : String | JsonpController.java:140:16:140:24 | resultStr | +| JsonpController.java:139:21:139:55 | ... + ... : String | JsonpController.java:140:16:140:24 | resultStr | +| JsonpController.java:150:21:150:54 | ... + ... : String | JsonpController.java:151:16:151:24 | resultStr | +| JsonpController.java:165:21:165:54 | ... + ... : String | JsonpController.java:166:16:166:24 | resultStr | +nodes +| JsonpController.java:36:32:36:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:39:21:39:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:47:32:47:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:48:21:48:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:56:32:56:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:58:21:58:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:66:32:66:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:68:21:68:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:76:32:76:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:83:21:83:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:91:32:91:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:97:21:97:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:105:32:105:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:108:21:108:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:117:24:117:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:118:24:118:28 | token | semmle.label | token | +| JsonpController.java:119:36:119:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:121:25:121:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:132:24:132:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:133:37:133:41 | token | semmle.label | token | +| JsonpController.java:137:32:137:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:139:21:139:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:150:21:150:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:151:16:151:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:165:21:165:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:166:16:166:24 | resultStr | semmle.label | resultStr | +#select +| JsonpController.java:40:16:40:24 | resultStr | JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:36:32:36:68 | getParameter(...) | this user input | +| JsonpController.java:49:16:49:24 | resultStr | JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:47:32:47:68 | getParameter(...) | this user input | +| JsonpController.java:59:16:59:24 | resultStr | JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:56:32:56:68 | getParameter(...) | this user input | +| JsonpController.java:69:16:69:24 | resultStr | JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:66:32:66:68 | getParameter(...) | this user input | +| JsonpController.java:84:20:84:28 | resultStr | JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:76:32:76:68 | getParameter(...) | this user input | +| JsonpController.java:98:20:98:28 | resultStr | JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:91:32:91:68 | getParameter(...) | this user input | +| JsonpController.java:109:16:109:24 | resultStr | JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:105:32:105:68 | getParameter(...) | this user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.qlref new file mode 100644 index 00000000000..3f5fc450669 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-352/JsonpInjection.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/options b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/options new file mode 100644 index 00000000000..c53e31e467f --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/options @@ -0,0 +1 @@ +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../stubs/apache-http-4.4.13/:${testdir}/../../../../../stubs/servlet-api-2.4:${testdir}/../../../../../stubs/fastjson-1.2.74/:${testdir}/../../../../../stubs/gson-2.8.6/:${testdir}/../../../../../stubs/jackson-databind-2.10/:${testdir}/../../../../../stubs/spring-context-5.3.2/:${testdir}/../../../../../stubs/spring-web-5.3.2/:${testdir}/../../../../../stubs/spring-core-5.3.2/:${testdir}/../../../../../stubs/tomcat-embed-core-9.0.41/ diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpController.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpController.java new file mode 100644 index 00000000000..e5b5e70a38d --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpController.java @@ -0,0 +1,203 @@ +import com.alibaba.fastjson.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.util.HashMap; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; + +@Controller +public class JsonpController { + + private static HashMap hashMap = new HashMap(); + + static { + hashMap.put("username","admin"); + hashMap.put("password","123456"); + } + + private String name = null; + + + @GetMapping(value = "jsonp1") + @ResponseBody + public String bad1(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp2") + @ResponseBody + public String bad2(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + resultStr = jsonpCallback + "(" + JSONObject.toJSONString(hashMap) + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp3") + @ResponseBody + public String bad3(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @GetMapping(value = "jsonp4") + @ResponseBody + public String bad4(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @GetMapping(value = "jsonp5") + @ResponseBody + public void bad5(HttpServletRequest request, + HttpServletResponse response) throws Exception { + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @GetMapping(value = "jsonp6") + @ResponseBody + public void bad6(HttpServletRequest request, + HttpServletResponse response) throws Exception { + String jsonpCallback = request.getParameter("jsonpCallback"); + PrintWriter pw = null; + ObjectMapper mapper = new ObjectMapper(); + String result = mapper.writeValueAsString(hashMap); + String resultStr = null; + pw = response.getWriter(); + resultStr = jsonpCallback + "(" + result + ")"; + pw.println(resultStr); + } + + @RequestMapping(value = "jsonp7", method = RequestMethod.GET) + @ResponseBody + public String bad7(HttpServletRequest request) { + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + Gson gson = new Gson(); + String result = gson.toJson(hashMap); + resultStr = jsonpCallback + "(" + result + ")"; + return resultStr; + } + + + @GetMapping(value = "jsonp8") + @ResponseBody + public String good1(HttpServletRequest request) { + String resultStr = null; + String token = request.getParameter("token"); + if (verifToken(token)){ + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + return "error"; + } + + + @GetMapping(value = "jsonp9") + @ResponseBody + public String good2(HttpServletRequest request) { + String resultStr = null; + String token = request.getParameter("token"); + boolean result = verifToken(token); + if (result){ + return ""; + } + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + @RequestMapping(value = "jsonp10") + @ResponseBody + public String good3(HttpServletRequest request) { + JSONObject parameterObj = readToJSONObect(request); + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + @RequestMapping(value = "jsonp11") + @ResponseBody + public String good4(@RequestParam("file") MultipartFile file,HttpServletRequest request) { + if(null == file){ + return "upload file error"; + } + String fileName = file.getOriginalFilename(); + System.out.println("file operations"); + String resultStr = null; + String jsonpCallback = request.getParameter("jsonpCallback"); + String restr = JSONObject.toJSONString(hashMap); + resultStr = jsonpCallback + "(" + restr + ");"; + return resultStr; + } + + public static JSONObject readToJSONObect(HttpServletRequest request){ + String jsonText = readPostContent(request); + JSONObject jsonObj = JSONObject.parseObject(jsonText, JSONObject.class); + return jsonObj; + } + + public static String readPostContent(HttpServletRequest request){ + BufferedReader in= null; + String content = null; + String line = null; + try { + in = new BufferedReader(new InputStreamReader(request.getInputStream(),"UTF-8")); + StringBuilder buf = new StringBuilder(); + while ((line = in.readLine()) != null) { + buf.append(line); + } + content = buf.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + String uri = request.getRequestURI(); + return content; + } + + public static String getJsonStr(Object result) { + return JSONObject.toJSONString(result); + } + + public static boolean verifToken(String token){ + if (token != "xxxx"){ + return false; + } + return true; + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.expected new file mode 100644 index 00000000000..c2bcab77d4d --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.expected @@ -0,0 +1,92 @@ +edges +| JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | +| JsonpController.java:39:21:39:54 | ... + ... : String | JsonpController.java:40:16:40:24 | resultStr | +| JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | +| JsonpController.java:48:21:48:80 | ... + ... : String | JsonpController.java:49:16:49:24 | resultStr | +| JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | +| JsonpController.java:58:21:58:55 | ... + ... : String | JsonpController.java:59:16:59:24 | resultStr | +| JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | +| JsonpController.java:68:21:68:54 | ... + ... : String | JsonpController.java:69:16:69:24 | resultStr | +| JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | +| JsonpController.java:83:21:83:54 | ... + ... : String | JsonpController.java:84:20:84:28 | resultStr | +| JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | +| JsonpController.java:97:21:97:54 | ... + ... : String | JsonpController.java:98:20:98:28 | resultStr | +| JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | +| JsonpController.java:108:21:108:54 | ... + ... : String | JsonpController.java:109:16:109:24 | resultStr | +| JsonpController.java:117:24:117:52 | getParameter(...) : String | JsonpController.java:118:24:118:28 | token | +| JsonpController.java:119:36:119:72 | getParameter(...) : String | JsonpController.java:122:20:122:28 | resultStr | +| JsonpController.java:121:25:121:59 | ... + ... : String | JsonpController.java:122:20:122:28 | resultStr | +| JsonpController.java:132:24:132:52 | getParameter(...) : String | JsonpController.java:133:37:133:41 | token | +| JsonpController.java:137:32:137:68 | getParameter(...) : String | JsonpController.java:140:16:140:24 | resultStr | +| JsonpController.java:139:21:139:55 | ... + ... : String | JsonpController.java:140:16:140:24 | resultStr | +| JsonpController.java:150:21:150:54 | ... + ... : String | JsonpController.java:151:16:151:24 | resultStr | +| JsonpController.java:165:21:165:54 | ... + ... : String | JsonpController.java:166:16:166:24 | resultStr | +| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | +| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | JsonpInjectionServlet1.java:38:39:38:45 | referer | +| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | +| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | +| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | +nodes +| JsonpController.java:36:32:36:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:39:21:39:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:47:32:47:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:48:21:48:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:56:32:56:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:58:21:58:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:66:32:66:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:68:21:68:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:76:32:76:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:83:21:83:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:91:32:91:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:97:21:97:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:105:32:105:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:108:21:108:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:117:24:117:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:118:24:118:28 | token | semmle.label | token | +| JsonpController.java:119:36:119:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:121:25:121:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:132:24:132:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:133:37:133:41 | token | semmle.label | token | +| JsonpController.java:137:32:137:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:139:21:139:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:150:21:150:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:151:16:151:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:165:21:165:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:166:16:166:24 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | semmle.label | getHeader(...) : String | +| JsonpInjectionServlet1.java:38:39:38:45 | referer | semmle.label | referer | +| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | +#select +| JsonpController.java:40:16:40:24 | resultStr | JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:36:32:36:68 | getParameter(...) | this user input | +| JsonpController.java:49:16:49:24 | resultStr | JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:47:32:47:68 | getParameter(...) | this user input | +| JsonpController.java:59:16:59:24 | resultStr | JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:56:32:56:68 | getParameter(...) | this user input | +| JsonpController.java:69:16:69:24 | resultStr | JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:66:32:66:68 | getParameter(...) | this user input | +| JsonpController.java:84:20:84:28 | resultStr | JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:76:32:76:68 | getParameter(...) | this user input | +| JsonpController.java:98:20:98:28 | resultStr | JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:91:32:91:68 | getParameter(...) | this user input | +| JsonpController.java:109:16:109:24 | resultStr | JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:105:32:105:68 | getParameter(...) | this user input | +| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | Jsonp response might include code from $@. | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) | this user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.qlref new file mode 100644 index 00000000000..3f5fc450669 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-352/JsonpInjection.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet1.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjectionServlet1.java similarity index 100% rename from java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet1.java rename to java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjectionServlet1.java diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet2.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjectionServlet2.java similarity index 100% rename from java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionServlet2.java rename to java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjectionServlet2.java diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/options b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/options new file mode 100644 index 00000000000..c53e31e467f --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/options @@ -0,0 +1 @@ +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../../stubs/apache-http-4.4.13/:${testdir}/../../../../../stubs/servlet-api-2.4:${testdir}/../../../../../stubs/fastjson-1.2.74/:${testdir}/../../../../../stubs/gson-2.8.6/:${testdir}/../../../../../stubs/jackson-databind-2.10/:${testdir}/../../../../../stubs/spring-context-5.3.2/:${testdir}/../../../../../stubs/spring-web-5.3.2/:${testdir}/../../../../../stubs/spring-core-5.3.2/:${testdir}/../../../../../stubs/tomcat-embed-core-9.0.41/ diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_1.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_1.expected deleted file mode 100644 index a89d03b67a7..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_1.expected +++ /dev/null @@ -1,60 +0,0 @@ -edges -| JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | resultStr | -| JsonpController.java:30:21:30:54 | ... + ... : String | JsonpController.java:31:16:31:24 | resultStr | -| JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | resultStr | -| JsonpController.java:40:21:40:80 | ... + ... : String | JsonpController.java:42:16:42:24 | resultStr | -| JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | resultStr | -| JsonpController.java:51:21:51:55 | ... + ... : String | JsonpController.java:52:16:52:24 | resultStr | -| JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | resultStr | -| JsonpController.java:61:21:61:54 | ... + ... : String | JsonpController.java:62:16:62:24 | resultStr | -| JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | resultStr | -| JsonpController.java:76:21:76:54 | ... + ... : String | JsonpController.java:77:20:77:28 | resultStr | -| JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | resultStr | -| JsonpController.java:90:21:90:54 | ... + ... : String | JsonpController.java:91:20:91:28 | resultStr | -| JsonpController.java:99:24:99:52 | getParameter(...) : String | JsonpController.java:101:24:101:28 | token | -| JsonpController.java:102:36:102:72 | getParameter(...) : String | JsonpController.java:105:20:105:28 | resultStr | -| JsonpController.java:104:25:104:59 | ... + ... : String | JsonpController.java:105:20:105:28 | resultStr | -nodes -| JsonpController.java:26:32:26:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:30:21:30:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:38:32:38:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:40:21:40:80 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:49:32:49:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:51:21:51:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:59:32:59:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:61:21:61:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:69:32:69:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:76:21:76:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:84:32:84:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:90:21:90:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:99:24:99:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:101:24:101:28 | token | semmle.label | token | -| JsonpController.java:102:36:102:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:104:25:104:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | -#select -| JsonpController.java:31:16:31:24 | resultStr | JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:26:32:26:68 | getParameter(...) | this user input | -| JsonpController.java:42:16:42:24 | resultStr | JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:38:32:38:68 | getParameter(...) | this user input | -| JsonpController.java:52:16:52:24 | resultStr | JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:49:32:49:68 | getParameter(...) | this user input | -| JsonpController.java:62:16:62:24 | resultStr | JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:59:32:59:68 | getParameter(...) | this user input | -| JsonpController.java:77:20:77:28 | resultStr | JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:69:32:69:68 | getParameter(...) | this user input | -| JsonpController.java:91:20:91:28 | resultStr | JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:84:32:84:68 | getParameter(...) | this user input | \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_2.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_2.expected deleted file mode 100644 index 4b12308a212..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_2.expected +++ /dev/null @@ -1,78 +0,0 @@ -edges -| JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | resultStr | -| JsonpController.java:30:21:30:54 | ... + ... : String | JsonpController.java:31:16:31:24 | resultStr | -| JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | resultStr | -| JsonpController.java:40:21:40:80 | ... + ... : String | JsonpController.java:42:16:42:24 | resultStr | -| JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | resultStr | -| JsonpController.java:51:21:51:55 | ... + ... : String | JsonpController.java:52:16:52:24 | resultStr | -| JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | resultStr | -| JsonpController.java:61:21:61:54 | ... + ... : String | JsonpController.java:62:16:62:24 | resultStr | -| JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | resultStr | -| JsonpController.java:76:21:76:54 | ... + ... : String | JsonpController.java:77:20:77:28 | resultStr | -| JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | resultStr | -| JsonpController.java:90:21:90:54 | ... + ... : String | JsonpController.java:91:20:91:28 | resultStr | -| JsonpController.java:99:24:99:52 | getParameter(...) : String | JsonpController.java:101:24:101:28 | token | -| JsonpController.java:102:36:102:72 | getParameter(...) : String | JsonpController.java:105:20:105:28 | resultStr | -| JsonpController.java:104:25:104:59 | ... + ... : String | JsonpController.java:105:20:105:28 | resultStr | -| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | -| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | JsonpInjectionServlet1.java:38:39:38:45 | referer | -| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | -| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | -| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | -nodes -| JsonpController.java:26:32:26:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:30:21:30:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:38:32:38:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:40:21:40:80 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:49:32:49:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:51:21:51:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:59:32:59:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:61:21:61:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:69:32:69:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:76:21:76:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:84:32:84:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:90:21:90:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:99:24:99:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:101:24:101:28 | token | semmle.label | token | -| JsonpController.java:102:36:102:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:104:25:104:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | -| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | semmle.label | getHeader(...) : String | -| JsonpInjectionServlet1.java:38:39:38:45 | referer | semmle.label | referer | -| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | -| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | -| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | -| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | -#select -| JsonpController.java:31:16:31:24 | resultStr | JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:26:32:26:68 | getParameter(...) | this user input | -| JsonpController.java:42:16:42:24 | resultStr | JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:38:32:38:68 | getParameter(...) | this user input | -| JsonpController.java:52:16:52:24 | resultStr | JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:49:32:49:68 | getParameter(...) | this user input | -| JsonpController.java:62:16:62:24 | resultStr | JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:59:32:59:68 | getParameter(...) | this user input | -| JsonpController.java:77:20:77:28 | resultStr | JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:69:32:69:68 | getParameter(...) | this user input | -| JsonpController.java:91:20:91:28 | resultStr | JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | - resultStr | Jsonp Injection query might include code from $@. | JsonpController.java:84:32:84:68 | getParameter(...) | this user input | -| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServle -t2.java:39:20:39:28 | resultStr | Jsonp Injection query might include code from $@. | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) | - this user input | \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_3.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_3.expected deleted file mode 100644 index 8e33ca6984c..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjection_3.expected +++ /dev/null @@ -1,66 +0,0 @@ -edges -| JsonpController.java:26:32:26:68 | getParameter(...) : String | JsonpController.java:31:16:31:24 | resultStr | -| JsonpController.java:30:21:30:54 | ... + ... : String | JsonpController.java:31:16:31:24 | resultStr | -| JsonpController.java:38:32:38:68 | getParameter(...) : String | JsonpController.java:42:16:42:24 | resultStr | -| JsonpController.java:40:21:40:80 | ... + ... : String | JsonpController.java:42:16:42:24 | resultStr | -| JsonpController.java:49:32:49:68 | getParameter(...) : String | JsonpController.java:52:16:52:24 | resultStr | -| JsonpController.java:51:21:51:55 | ... + ... : String | JsonpController.java:52:16:52:24 | resultStr | -| JsonpController.java:59:32:59:68 | getParameter(...) : String | JsonpController.java:62:16:62:24 | resultStr | -| JsonpController.java:61:21:61:54 | ... + ... : String | JsonpController.java:62:16:62:24 | resultStr | -| JsonpController.java:69:32:69:68 | getParameter(...) : String | JsonpController.java:77:20:77:28 | resultStr | -| JsonpController.java:76:21:76:54 | ... + ... : String | JsonpController.java:77:20:77:28 | resultStr | -| JsonpController.java:84:32:84:68 | getParameter(...) : String | JsonpController.java:91:20:91:28 | resultStr | -| JsonpController.java:90:21:90:54 | ... + ... : String | JsonpController.java:91:20:91:28 | resultStr | -| JsonpController.java:99:24:99:52 | getParameter(...) : String | JsonpController.java:101:24:101:28 | token | -| JsonpController.java:102:36:102:72 | getParameter(...) : String | JsonpController.java:105:20:105:28 | resultStr | -| JsonpController.java:104:25:104:59 | ... + ... : String | JsonpController.java:105:20:105:28 | resultStr | -| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | -| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | JsonpInjectionServlet1.java:38:39:38:45 | referer | -| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | -| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | -| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | -| RefererFilter.java:22:26:22:53 | getHeader(...) : String | RefererFilter.java:23:39:23:45 | refefer | -nodes -| JsonpController.java:26:32:26:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:30:21:30:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:31:16:31:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:38:32:38:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:40:21:40:80 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:42:16:42:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:49:32:49:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:51:21:51:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:52:16:52:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:59:32:59:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:61:21:61:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:62:16:62:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:69:32:69:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:76:21:76:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:77:20:77:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:84:32:84:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:90:21:90:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:91:20:91:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:99:24:99:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:101:24:101:28 | token | semmle.label | token | -| JsonpController.java:102:36:102:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:104:25:104:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:105:20:105:28 | resultStr | semmle.label | resultStr | -| JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | semmle.label | getHeader(...) : String | -| JsonpInjectionServlet1.java:38:39:38:45 | referer | semmle.label | referer | -| JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | -| JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | -| JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | -| JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | -| RefererFilter.java:22:26:22:53 | getHeader(...) : String | semmle.label | getHeader(...) : String | -| RefererFilter.java:23:39:23:45 | refefer | semmle.label | refefer | -#select \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/Readme b/java/ql/test/experimental/query-tests/security/CWE-352/Readme deleted file mode 100644 index 15715d6187c..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/Readme +++ /dev/null @@ -1,3 +0,0 @@ -1. The JsonpInjection_1.expected result is obtained through the test of `JsonpController.java`. -2. The JsonpInjection_2.expected result is obtained through the test of `JsonpController.java`, `JsonpInjectionServlet1.java`, `JsonpInjectionServlet2.java`. -3. The JsonpInjection_3.expected result is obtained through the test of `JsonpController.java`, `JsonpInjectionServlet1.java`, `JsonpInjectionServlet2.java`, `RefererFilter.java`. \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/RefererFilter.java b/java/ql/test/experimental/query-tests/security/CWE-352/RefererFilter.java deleted file mode 100644 index 97444932ae1..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/RefererFilter.java +++ /dev/null @@ -1,43 +0,0 @@ -import java.io.IOException; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.springframework.util.StringUtils; - -public class RefererFilter implements Filter { - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - } - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) servletRequest; - HttpServletResponse response = (HttpServletResponse) servletResponse; - String refefer = request.getHeader("Referer"); - boolean result = verifReferer(refefer); - if (result){ - filterChain.doFilter(servletRequest, servletResponse); - } - response.sendError(444, "Referer xxx."); - } - - @Override - public void destroy() { - } - - public static boolean verifReferer(String referer){ - if (StringUtils.isEmpty(referer)){ - return false; - } - if (referer.startsWith("http://www.baidu.com/")){ - return true; - } - return false; - } -} diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/options b/java/ql/test/experimental/query-tests/security/CWE-352/options deleted file mode 100644 index 3676b8e38b6..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-352/options +++ /dev/null @@ -1 +0,0 @@ -//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/apache-http-4.4.13/:${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/fastjson-1.2.74/:${testdir}/../../../../stubs/gson-2.8.6/:${testdir}/../../../../stubs/jackson-databind-2.10/:${testdir}/../../../../stubs/springframework-5.2.3/:${testdir}/../../../../stubs/spring-context-5.3.2/:${testdir}/../../../../stubs/spring-web-5.3.2/:${testdir}/../../../../stubs/spring-core-5.3.2/ diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.java b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.java index 3a823fade5b..edfe917400b 100644 --- a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.java +++ b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/annotation/AliasFor.java @@ -1,10 +1,21 @@ package org.springframework.core.annotation; +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Documented public @interface AliasFor { @AliasFor("attribute") String value() default ""; @AliasFor("value") String attribute() default ""; - + + Class annotation() default Annotation.class; } diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/io/InputStreamSource.java b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/io/InputStreamSource.java new file mode 100644 index 00000000000..372d06cc738 --- /dev/null +++ b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/io/InputStreamSource.java @@ -0,0 +1,8 @@ +package org.springframework.core.io; + +import java.io.IOException; +import java.io.InputStream; + +public interface InputStreamSource { + InputStream getInputStream() throws IOException; +} diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/io/Resource.java b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/io/Resource.java new file mode 100644 index 00000000000..6bd357f2228 --- /dev/null +++ b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/core/io/Resource.java @@ -0,0 +1,46 @@ +package org.springframework.core.io; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import org.springframework.lang.Nullable; + +public interface Resource extends InputStreamSource { + boolean exists(); + + default boolean isReadable() { + return this.exists(); + } + + default boolean isOpen() { + return false; + } + + default boolean isFile() { + return false; + } + + URL getURL() throws IOException; + + URI getURI() throws IOException; + + File getFile() throws IOException; + + default ReadableByteChannel readableChannel() throws IOException { + return null; + } + + long contentLength() throws IOException; + + long lastModified() throws IOException; + + Resource createRelative(String var1) throws IOException; + + @Nullable + String getFilename(); + + String getDescription(); +} diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/lang/Nullable.java b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/lang/Nullable.java new file mode 100644 index 00000000000..44bdae10fda --- /dev/null +++ b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/lang/Nullable.java @@ -0,0 +1,13 @@ +package org.springframework.lang; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Nullable { +} diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/FileCopyUtils.java b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/FileCopyUtils.java new file mode 100644 index 00000000000..78d384d7266 --- /dev/null +++ b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/FileCopyUtils.java @@ -0,0 +1,53 @@ +package org.springframework.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.file.Files; +import org.springframework.lang.Nullable; + +public abstract class FileCopyUtils { + public static final int BUFFER_SIZE = 4096; + + public FileCopyUtils() { + } + + public static int copy(File in, File out) throws IOException { + return 1; + } + + public static void copy(byte[] in, File out) throws IOException {} + + public static byte[] copyToByteArray(File in) throws IOException { + return null; + } + + public static int copy(InputStream in, OutputStream out) throws IOException { + return 1; + } + + public static void copy(byte[] in, OutputStream out) throws IOException {} + + public static byte[] copyToByteArray(@Nullable InputStream in) throws IOException { + return null; + } + + public static int copy(Reader in, Writer out) throws IOException { + return 1; + } + + public static void copy(String in, Writer out) throws IOException {} + + public static String copyToString(@Nullable Reader in) throws IOException { + return null; + } + + private static void close(Closeable closeable) {} +} diff --git a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/StringUtils.java b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/StringUtils.java index 6ee07f84593..6ea27bbffa2 100644 --- a/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/StringUtils.java +++ b/java/ql/test/stubs/spring-core-5.3.2/org/springframework/util/StringUtils.java @@ -5,4 +5,4 @@ public abstract class StringUtils { public static boolean isEmpty(Object str) { return str == null || "".equals(str); } -} \ No newline at end of file +} diff --git a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.java b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.java index 1190be7b879..541c7cd4e21 100644 --- a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.java +++ b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/GetMapping.java @@ -1,19 +1,51 @@ package org.springframework.web.bind.annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; -@RequestMapping +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping( + method = {RequestMethod.GET} +) public @interface GetMapping { - + @AliasFor( + annotation = RequestMapping.class + ) String name() default ""; + @AliasFor( + annotation = RequestMapping.class + ) String[] value() default {}; + @AliasFor( + annotation = RequestMapping.class + ) String[] path() default {}; + @AliasFor( + annotation = RequestMapping.class + ) String[] params() default {}; + @AliasFor( + annotation = RequestMapping.class + ) + String[] headers() default {}; + + @AliasFor( + annotation = RequestMapping.class + ) String[] consumes() default {}; + @AliasFor( + annotation = RequestMapping.class + ) String[] produces() default {}; } diff --git a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/Mapping.java b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/Mapping.java new file mode 100644 index 00000000000..5f269bbcbb8 --- /dev/null +++ b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/Mapping.java @@ -0,0 +1,4 @@ +package org.springframework.web.bind.annotation; + +public @interface Mapping { +} diff --git a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestMapping.java b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestMapping.java index ddb36bce4c0..ed692a03063 100644 --- a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestMapping.java +++ b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestMapping.java @@ -1,15 +1,32 @@ package org.springframework.web.bind.annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Mapping public @interface RequestMapping { String name() default ""; @AliasFor("path") String[] value() default {}; - + @AliasFor("value") String[] path() default {}; RequestMethod[] method() default {}; + + String[] params() default {}; + + String[] headers() default {}; + + String[] consumes() default {}; + + String[] produces() default {}; } diff --git a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestParam.java b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestParam.java new file mode 100644 index 00000000000..56094811c37 --- /dev/null +++ b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/RequestParam.java @@ -0,0 +1,23 @@ +package org.springframework.web.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RequestParam { + @AliasFor("name") + String value() default ""; + + @AliasFor("value") + String name() default ""; + + boolean required() default true; + + String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n"; +} diff --git a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.java b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.java index b2134009968..4f21b6daaaf 100644 --- a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.java +++ b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/bind/annotation/ResponseBody.java @@ -1,4 +1,13 @@ package org.springframework.web.bind.annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented public @interface ResponseBody { } diff --git a/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/multipart/MultipartFile.java b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/multipart/MultipartFile.java new file mode 100644 index 00000000000..93ea3439fed --- /dev/null +++ b/java/ql/test/stubs/spring-web-5.3.2/org/springframework/web/multipart/MultipartFile.java @@ -0,0 +1,38 @@ +package org.springframework.web.multipart; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import org.springframework.core.io.InputStreamSource; +import org.springframework.core.io.Resource; +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; + +public interface MultipartFile extends InputStreamSource { + String getName(); + + @Nullable + String getOriginalFilename(); + + @Nullable + String getContentType(); + + boolean isEmpty(); + + long getSize(); + + byte[] getBytes() throws IOException; + + InputStream getInputStream() throws IOException; + + default Resource getResource() { + return null; + } + + void transferTo(File var1) throws IOException, IllegalStateException; + + default void transferTo(Path dest) throws IOException, IllegalStateException { + } +} diff --git a/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/annotation/WebServlet.java b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/annotation/WebServlet.java new file mode 100644 index 00000000000..cc255efc00f --- /dev/null +++ b/java/ql/test/stubs/tomcat-embed-core-9.0.41/javax/servlet/annotation/WebServlet.java @@ -0,0 +1,30 @@ +package javax.servlet.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WebServlet { + String name() default ""; + + String[] value() default {}; + + String[] urlPatterns() default {}; + + int loadOnStartup() default -1; + + boolean asyncSupported() default false; + + String smallIcon() default ""; + + String largeIcon() default ""; + + String description() default ""; + + String displayName() default ""; +} From 15206fd2ce3d53168caa2d0250c2b9036dccfca9 Mon Sep 17 00:00:00 2001 From: haby0 Date: Wed, 17 Mar 2021 15:52:05 +0800 Subject: [PATCH 0052/1429] JsonpInjection.ql autoformatted --- java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql index 068469328ea..eb4fe4e5b66 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql @@ -46,7 +46,7 @@ class RequestResponseFlowConfig extends TaintTracking::Configuration { /** Eliminate the method of calling the node is not the get method. */ override predicate isSanitizer(DataFlow::Node node) { - not getACallingCallableOrSelf(node.getEnclosingCallable()) instanceof RequestGetMethod + not getACallingCallableOrSelf(node.getEnclosingCallable()) instanceof RequestGetMethod } override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { From 79d6731ed8e1725c1064214b7a201c4ad0794009 Mon Sep 17 00:00:00 2001 From: Tamas Vajk Date: Fri, 19 Mar 2021 11:01:28 +0100 Subject: [PATCH 0053/1429] C#: Adjust make_stubs.py to use codeql instead of odasa --- csharp/ql/src/Stubs/make_stubs.py | 50 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/csharp/ql/src/Stubs/make_stubs.py b/csharp/ql/src/Stubs/make_stubs.py index e94cf0c7261..19a917cd8ab 100644 --- a/csharp/ql/src/Stubs/make_stubs.py +++ b/csharp/ql/src/Stubs/make_stubs.py @@ -43,12 +43,6 @@ if not foundCS: print("Test directory does not contain .cs files. Please specify a working qltest directory.") exit(1) -cmd = ['odasa', 'selfTest'] -print('Running ' + ' '.join(cmd)) -if subprocess.check_call(cmd): - print("odasa selfTest failed. Ensure odasa is on your current path.") - exit(1) - csharpQueries = os.path.abspath(os.path.dirname(sys.argv[0])) outputFile = os.path.join(testDir, 'stubs.cs') @@ -58,35 +52,36 @@ if os.path.isfile(outputFile): os.remove(outputFile) # It would interfere with the test. print("Removed previous", outputFile) -cmd = ['odasa', 'qltest', '--optimize', '--leave-temp-files', testDir] +cmd = ['codeql', 'test', 'run', '--keep-databases', testDir] print('Running ' + ' '.join(cmd)) if subprocess.check_call(cmd): - print("qltest failed. Please fix up the test before proceeding.") + print("codeql test failed. Please fix up the test before proceeding.") exit(1) -dbDir = os.path.join(testDir, os.path.basename(testDir) + ".testproj", "db-csharp") +dbDir = os.path.join(testDir, os.path.basename(testDir) + ".testproj") if not os.path.isdir(dbDir): - print("Expected database directory " + dbDir + " not found. Please contact Semmle.") + print("Expected database directory " + dbDir + " not found.") exit(1) -cmd = ['odasa', 'runQuery', '--query', os.path.join(csharpQueries, 'MinimalStubsFromSource.ql'), '--db', dbDir, '--output-file', outputFile] +cmd = ['codeql', 'query', 'run', os.path.join( + csharpQueries, 'MinimalStubsFromSource.ql'), '--database', dbDir, '--output', outputFile] print('Running ' + ' '.join(cmd)) if subprocess.check_call(cmd): - print('Failed to run the query to generate output file. Please contact Semmle.') + print('Failed to run the query to generate output file.') exit(1) # Remove the leading " and trailing " bytes from the file len = os.stat(outputFile).st_size f = open(outputFile, "rb") try: - quote = f.read(1) - if quote != b'"': - print("Unexpected character in file. Please contact Semmle.") - contents = f.read(len-3) - quote = f.read(1) - if quote != b'"': - print("Unexpected end character. Please contact Semmle.", quote) + quote = f.read(6) + if quote != b"\x02\x01\x86'\x85'": + print("Unexpected start characters in file.", quote) + contents = f.read(len-21) + quote = f.read(15) + if quote != b'\x0e\x01\x08#select\x01\x01\x00s\x00': + print("Unexpected end character in file.", quote) finally: f.close() @@ -94,16 +89,17 @@ f = open(outputFile, "wb") f.write(contents) f.close() -cmd = ['odasa', 'qltest', '--optimize', testDir] +cmd = ['codeql', 'test', 'run', testDir] print('Running ' + ' '.join(cmd)) if subprocess.check_call(cmd): - print('\nTest failed. You may need to fix up', outputFile) - print('It may help to view', outputFile, ' in Visual Studio') - print("Next steps:") - print('1. Look at the compilation errors, and fix up', outputFile, 'so that the test compiles') - print('2. Re-run odasa qltest --optimize "' + testDir + '"') - print('3. git add "' + outputFile + '"') - exit(1) + print('\nTest failed. You may need to fix up', outputFile) + print('It may help to view', outputFile, ' in Visual Studio') + print("Next steps:") + print('1. Look at the compilation errors, and fix up', + outputFile, 'so that the test compiles') + print('2. Re-run codeql test run "' + testDir + '"') + print('3. git add "' + outputFile + '"') + exit(1) print("\nStub generation successful! Next steps:") print('1. Edit "semmle-extractor-options" in the .cs files to remove unused references') From 164b383fda25be40906e216a53581c5fec4da4df Mon Sep 17 00:00:00 2001 From: yoff Date: Fri, 19 Mar 2021 19:12:13 +0100 Subject: [PATCH 0054/1429] Update python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py Co-authored-by: Rasmus Wriedt Larsen --- .../test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py b/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py index 028c318be66..e4205c49824 100644 --- a/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py +++ b/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py @@ -29,3 +29,11 @@ def test_fluent_safe(): conn = SSL.Connection(context, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) r = conn.connect((hostname, 443)) print(r, conn.get_protocol_version_name()) + +def test_safe_by_construction(): + hostname = 'www.python.org' + context = SSL.Context(SSL.TLSv1_2_METHOD) + + conn = SSL.Connection(context, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) + r = conn.connect((hostname, 443)) + print(conn.get_protocol_version_name()) From 1385b2264234bc6fa3deaaeb7537ee47d89c47fe Mon Sep 17 00:00:00 2001 From: Dilan Date: Fri, 19 Mar 2021 16:43:29 -0700 Subject: [PATCH 0055/1429] pr fixes, typo in qhelp file and helper method for queries --- .../Classes/NamingConventionsClasses.qhelp | 2 +- .../Classes/NamingConventionsClasses.ql | 17 +++++++----- .../NamingConventionsFunctions.qhelp | 2 +- .../Functions/NamingConventionsFunctions.ql | 27 +++++++++++-------- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp b/python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp index 1a7f4bc45a4..a928668dc6f 100644 --- a/python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp +++ b/python/ql/src/experimental/Classes/NamingConventionsClasses.qhelp @@ -21,7 +21,7 @@ Write the class name beginning with an uppercase letter. For example, clas
  • - Guido van Rossum, Barry Warsaw, Nick Coghlan PEP 8 -- Style Guide for Python Code + Guido van Rossum, Barry Warsaw, Nick Coghlan PEP 8 -- Style Guide for Python Code Python Class Names
  • diff --git a/python/ql/src/experimental/Classes/NamingConventionsClasses.ql b/python/ql/src/experimental/Classes/NamingConventionsClasses.ql index d4919c3ece8..f16d242eadf 100644 --- a/python/ql/src/experimental/Classes/NamingConventionsClasses.ql +++ b/python/ql/src/experimental/Classes/NamingConventionsClasses.ql @@ -9,15 +9,20 @@ import python -from Class c, string first_char +predicate lower_case_class(Class c) { + exists(string first_char | + first_char = c.getName().prefix(1) and + not first_char = first_char.toUpperCase() + ) +} + +from Class c where c.inSource() and - first_char = c.getName().prefix(1) and - not first_char = first_char.toUpperCase() and - not exists(Class c1, string first_char1 | + lower_case_class(c) and + not exists(Class c1 | c1 != c and c1.getLocation().getFile() = c.getLocation().getFile() and - first_char1 = c1.getName().prefix(1) and - not first_char1 = first_char1.toUpperCase() + lower_case_class(c1) ) select c, "Class names should start in uppercase." diff --git a/python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp b/python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp index 46d948592ff..b4ff738782c 100644 --- a/python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp +++ b/python/ql/src/experimental/Functions/NamingConventionsFunctions.qhelp @@ -21,7 +21,7 @@ Write the function name beginning with an lowercase letter. For example, j
  • - Guido van Rossum, Barry Warsaw, Nick Coghlan PEP 8 -- Style Guide for Python Code + Guido van Rossum, Barry Warsaw, Nick Coghlan PEP 8 -- Style Guide for Python Code Python Function and Variable Names
  • diff --git a/python/ql/src/experimental/Functions/NamingConventionsFunctions.ql b/python/ql/src/experimental/Functions/NamingConventionsFunctions.ql index 80dc0e99cd8..d2868af22c9 100644 --- a/python/ql/src/experimental/Functions/NamingConventionsFunctions.ql +++ b/python/ql/src/experimental/Functions/NamingConventionsFunctions.ql @@ -9,15 +9,20 @@ import python -from Function f, string first_char -where - f.inSource() and - first_char = f.getName().prefix(1) and - not first_char = first_char.toLowerCase() and - not exists(Function f1, string first_char1 | - f1 != f and - f1.getLocation().getFile() = f.getLocation().getFile() and - first_char1 = f1.getName().prefix(1) and - not first_char1 = first_char1.toLowerCase() +predicate upper_case_function(Function func) { + exists(string first_char | + first_char = func.getName().prefix(1) and + not first_char = first_char.toLowerCase() ) -select f, "Function names should start in lowercase." +} + +from Function func +where + func.inSource() and + upper_case_function(func) and + not exists(Function func1 | + func1 != func and + func1.getLocation().getFile() = func.getLocation().getFile() and + upper_case_function(func1) + ) +select func, "Function names should start in lowercase." From 26bac9f425c2ecab54759797da19bcc1586ed95d Mon Sep 17 00:00:00 2001 From: ihsinme Date: Sun, 21 Mar 2021 15:25:29 +0300 Subject: [PATCH 0056/1429] Apply suggestions from code review Co-authored-by: Robert Marsh --- .../CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql index 1a116a83dbf..034df703bc3 100644 --- a/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql +++ b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql @@ -15,12 +15,12 @@ import cpp import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis -/** Holds, if it is an expression, a boolean increment. */ +/** Holds if `exp` increments a boolean value. */ predicate incrementBoolType(Expr exp) { exp.(IncrementOperation).getOperand().getType() instanceof BoolType } -/** Holds, if this is an expression, applies a minus to a boolean type. */ +/** Holds if `exp` applies the unary minus operator to a boolean type. */ predicate revertSignBoolType(Expr exp) { exp.(AssignExpr).getRValue().(UnaryMinusExpr).getAnOperand().getType() instanceof BoolType and exp.(AssignExpr).getLValue().getType() instanceof BoolType From 0200aedc2efc939b96170951d848d440fcbb8e4a Mon Sep 17 00:00:00 2001 From: yo-h <55373593+yo-h@users.noreply.github.com> Date: Sun, 21 Mar 2021 12:55:25 -0400 Subject: [PATCH 0057/1429] Java 16: adjust test `options` --- java/ql/test/library-tests/dataflow/records/options | 2 +- java/ql/test/library-tests/dataflow/taint-format/options | 1 - java/ql/test/library-tests/printAst/options | 2 +- java/ql/test/library-tests/ssa/options | 2 +- java/ql/test/query-tests/StringFormat/options | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 java/ql/test/library-tests/dataflow/taint-format/options delete mode 100644 java/ql/test/query-tests/StringFormat/options diff --git a/java/ql/test/library-tests/dataflow/records/options b/java/ql/test/library-tests/dataflow/records/options index 81817b37785..fc57fe025b9 100644 --- a/java/ql/test/library-tests/dataflow/records/options +++ b/java/ql/test/library-tests/dataflow/records/options @@ -1 +1 @@ -//semmle-extractor-options: --javac-args --enable-preview -source 15 -target 15 +//semmle-extractor-options: --javac-args -source 16 -target 16 diff --git a/java/ql/test/library-tests/dataflow/taint-format/options b/java/ql/test/library-tests/dataflow/taint-format/options deleted file mode 100644 index 81817b37785..00000000000 --- a/java/ql/test/library-tests/dataflow/taint-format/options +++ /dev/null @@ -1 +0,0 @@ -//semmle-extractor-options: --javac-args --enable-preview -source 15 -target 15 diff --git a/java/ql/test/library-tests/printAst/options b/java/ql/test/library-tests/printAst/options index 81817b37785..e41003a6e3c 100644 --- a/java/ql/test/library-tests/printAst/options +++ b/java/ql/test/library-tests/printAst/options @@ -1 +1 @@ -//semmle-extractor-options: --javac-args --enable-preview -source 15 -target 15 +//semmle-extractor-options: --javac-args --enable-preview -source 16 -target 16 diff --git a/java/ql/test/library-tests/ssa/options b/java/ql/test/library-tests/ssa/options index 81817b37785..e41003a6e3c 100644 --- a/java/ql/test/library-tests/ssa/options +++ b/java/ql/test/library-tests/ssa/options @@ -1 +1 @@ -//semmle-extractor-options: --javac-args --enable-preview -source 15 -target 15 +//semmle-extractor-options: --javac-args --enable-preview -source 16 -target 16 diff --git a/java/ql/test/query-tests/StringFormat/options b/java/ql/test/query-tests/StringFormat/options deleted file mode 100644 index 81817b37785..00000000000 --- a/java/ql/test/query-tests/StringFormat/options +++ /dev/null @@ -1 +0,0 @@ -//semmle-extractor-options: --javac-args --enable-preview -source 15 -target 15 From 73e940de74bb326664db451b97fa58e67e683400 Mon Sep 17 00:00:00 2001 From: Artem Smotrakov Date: Tue, 2 Feb 2021 17:37:14 +0100 Subject: [PATCH 0058/1429] Added query for Jakarta EL injections - Added JakartaExpressionInjection.ql - Added a qhelp file with examples --- .../Security/CWE/CWE-094/InjectionLib.qll | 14 +++ .../CWE-094/JakartaExpressionInjection.qhelp | 65 ++++++++++++ .../CWE/CWE-094/JakartaExpressionInjection.ql | 20 ++++ .../CWE-094/JakartaExpressionInjectionLib.qll | 98 +++++++++++++++++++ .../Security/CWE/CWE-094/JexlInjectionLib.qll | 13 +-- .../SaferExpressionEvaluationWithJuel.java | 10 ++ .../UnsafeExpressionEvaluationWithJuel.java | 5 + 7 files changed, 213 insertions(+), 12 deletions(-) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/InjectionLib.qll create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/SaferExpressionEvaluationWithJuel.java create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/UnsafeExpressionEvaluationWithJuel.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/InjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-094/InjectionLib.qll new file mode 100644 index 00000000000..adab79d6f5c --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/InjectionLib.qll @@ -0,0 +1,14 @@ +import java +import semmle.code.java.dataflow.FlowSources + +/** + * Holds if `fromNode` to `toNode` is a dataflow step that returns data from + * a bean by calling one of its getters. + */ +predicate returnsDataFromBean(DataFlow::Node fromNode, DataFlow::Node toNode) { + exists(MethodAccess ma, Method m | ma.getMethod() = m | + m instanceof GetterMethod and + ma.getQualifier() = fromNode.asExpr() and + ma = toNode.asExpr() + ) +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.qhelp new file mode 100644 index 00000000000..6e6b5f63394 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.qhelp @@ -0,0 +1,65 @@ + + + + +

    +Jakarta Expression Language (EL) is an expression language for Java applications. +There are a single language specification and multiple implementations +such as Glassfish, Juel, Apache Commons EL, etc. +The language allows invocation of methods available in the JVM. +If an expression is built using attacker-controlled data, +and then evaluated, then it may allow the attacker to run arbitrary code. +

    +
    + + +

    +It is generally recommended to avoid using untrusted data in an EL expression. +Before using untrusted data to build an EL expressoin, the data should be validated +to ensure it is not evaluated as expression language. If the EL implementaion offers +configuring a sandbox for EL expression, they should be run in a restircitive sandbox +that allows accessing only explicitly allowed classes. If the EL implementation +does not allow sandboxing, consider using other expressiong language implementations +with sandboxing capabilities such as Apache Commons JEXL or the Spring Expression Language. +

    +
    + + +

    +The following example shows how untrusted data is used to build and run an expression +using the JUEL interpreter: +

    + + +

    +JUEL does not allow to run expression in a sandbox. To prevent running arbitrary code, +incoming data has to be checked before including to an expression. The next example +uses a Regex pattern to check whether a user tries to run an allowed exression or not: +

    + + +
    + + +
  • + Eclipse Foundation: + Jakarta Expression Language. +
  • +
  • + Jakarta EE documentation: + Jakarta Expression Language API +
  • +
  • + OWASP: + Expression Language Injection. +
  • +
  • + JUEL: + Home page +
  • +
  • + Apache Foundation: + Apache Commons EL +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql new file mode 100644 index 00000000000..b94c98201de --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql @@ -0,0 +1,20 @@ +/** + * @name Java EE Expression Language injection + * @description Evaluation of a user-controlled Jave EE expression + * may lead to arbitrary code execution. + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/javaee-expression-injection + * @tags security + * external/cwe/cwe-094 + */ + +import java +import JavaEEExpressionInjectionLib +import DataFlow::PathGraph + +from DataFlow::PathNode source, DataFlow::PathNode sink, JavaEEExpressionInjectionConfig conf +where conf.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "Java EE Expression Language injection from $@.", + source.getNode(), "this user input" diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll new file mode 100644 index 00000000000..d43b31c9888 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll @@ -0,0 +1,98 @@ +import java +import InjectionLib +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking + +/** + * A taint-tracking configuration for unsafe user input + * that is used to construct and evaluate a Java EE expression. + */ +class JavaEEExpressionInjectionConfig extends TaintTracking::Configuration { + JavaEEExpressionInjectionConfig() { this = "JavaEEExpressionInjectionConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof ExpressionEvaluationSink } + + override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) { + any(TaintPropagatingCall c).taintFlow(fromNode, toNode) or + returnsDataFromBean(fromNode, toNode) + } +} + +/** + * A sink for Expresssion Language injection vulnerabilities, + * i.e. method calls that run evaluation of a Java EE expression. + */ +private class ExpressionEvaluationSink extends DataFlow::ExprNode { + ExpressionEvaluationSink() { + exists(MethodAccess ma, Method m, Expr taintFrom | + ma.getMethod() = m and taintFrom = this.asExpr() + | + m.getDeclaringType() instanceof ValueExpression and + m.hasName(["getValue", "setValue"]) and + ma.getQualifier() = taintFrom + or + m.getDeclaringType() instanceof MethodExpression and + m.hasName("invoke") and + ma.getQualifier() = taintFrom + or + m.getDeclaringType() instanceof LambdaExpression and + m.hasName("invoke") and + ma.getQualifier() = taintFrom + or + m.getDeclaringType() instanceof ELProcessor and + m.hasName(["eval", "getValue", "setValue"]) and + ma.getArgument(0) = taintFrom + ) + } +} + +/** + * Defines method calls that propagate tainted expressions. + */ +private class TaintPropagatingCall extends Call { + Expr taintFromExpr; + + TaintPropagatingCall() { + taintFromExpr = this.getArgument(1) and + exists(Method m | this.(MethodAccess).getMethod() = m | + m.getDeclaringType() instanceof ExpressionFactory and + m.hasName(["createValueExpression", "createMethodExpression"]) and + taintFromExpr.getType() instanceof TypeString + ) + or + exists(Constructor c | this.(ConstructorCall).getConstructor() = c | + c.getDeclaringType() instanceof LambdaExpression and + taintFromExpr.getType() instanceof ValueExpression + ) + } + + /** + * Holds if `fromNode` to `toNode` is a dataflow step that propagates + * tainted data. + */ + predicate taintFlow(DataFlow::Node fromNode, DataFlow::Node toNode) { + fromNode.asExpr() = taintFromExpr and toNode.asExpr() = this + } +} + +private class ELProcessor extends RefType { + ELProcessor() { hasQualifiedName("javax.el", "ELProcessor") } +} + +private class ExpressionFactory extends RefType { + ExpressionFactory() { hasQualifiedName("javax.el", "ExpressionFactory") } +} + +private class ValueExpression extends RefType { + ValueExpression() { hasQualifiedName("javax.el", "ValueExpression") } +} + +private class MethodExpression extends RefType { + MethodExpression() { hasQualifiedName("javax.el", "MethodExpression") } +} + +private class LambdaExpression extends RefType { + LambdaExpression() { hasQualifiedName("javax.el", "LambdaExpression") } +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JexlInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-094/JexlInjectionLib.qll index 561d7e46ae9..51084a9862c 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JexlInjectionLib.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JexlInjectionLib.qll @@ -1,4 +1,5 @@ import java +import InjectionLib import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking @@ -152,18 +153,6 @@ private predicate createsJexlEngine(DataFlow::Node fromNode, DataFlow::Node toNo ) } -/** - * Holds if `fromNode` to `toNode` is a dataflow step that returns data from - * a bean by calling one of its getters. - */ -private predicate returnsDataFromBean(DataFlow::Node fromNode, DataFlow::Node toNode) { - exists(MethodAccess ma, Method m | ma.getMethod() = m | - m instanceof GetterMethod and - ma.getQualifier() = fromNode.asExpr() and - ma = toNode.asExpr() - ) -} - /** * A methods in the `JexlEngine` class that gets or sets a property with a JEXL expression. */ diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/SaferExpressionEvaluationWithJuel.java b/java/ql/src/experimental/Security/CWE/CWE-094/SaferExpressionEvaluationWithJuel.java new file mode 100644 index 00000000000..54fb9a0ed36 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/SaferExpressionEvaluationWithJuel.java @@ -0,0 +1,10 @@ +String input = getRemoteUserInput(); +String pattern = "(inside|outside)\\.(temperature|humidity)"; +if (!input.matches(pattern)) { + throw new IllegalArgumentException("Unexpected exression"); +} +String expression = "${" + input + "}"; +ExpressionFactory factory = new de.odysseus.el.ExpressionFactoryImpl(); +ValueExpression e = factory.createValueExpression(context, expression, Object.class); +SimpleContext context = getContext(); +Object result = e.getValue(context); \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/UnsafeExpressionEvaluationWithJuel.java b/java/ql/src/experimental/Security/CWE/CWE-094/UnsafeExpressionEvaluationWithJuel.java new file mode 100644 index 00000000000..27afa0fcb49 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/UnsafeExpressionEvaluationWithJuel.java @@ -0,0 +1,5 @@ +String expression = "${" + getRemoteUserInput() + "}"; +ExpressionFactory factory = new de.odysseus.el.ExpressionFactoryImpl(); +ValueExpression e = factory.createValueExpression(context, expression, Object.class); +SimpleContext context = getContext(); +Object result = e.getValue(context); \ No newline at end of file From adb1ed380ac4736860eac8ea6cfadb8f38497db5 Mon Sep 17 00:00:00 2001 From: Artem Smotrakov Date: Sun, 21 Mar 2021 20:36:47 +0300 Subject: [PATCH 0059/1429] Added tests for Jakarta expression injection --- .../CWE/CWE-094/JakartaExpressionInjection.ql | 10 +-- .../CWE-094/JakartaExpressionInjectionLib.qll | 8 +- .../JakartaExpressionInjection.expected | 59 +++++++++++++ .../CWE-094/JakartaExpressionInjection.java | 87 +++++++++++++++++++ .../CWE-094/JakartaExpressionInjection.qlref | 1 + .../query-tests/security/CWE-094/options | 3 +- .../stubs/java-ee-el/javax/el/ELContext.java | 3 + .../stubs/java-ee-el/javax/el/ELManager.java | 5 ++ .../java-ee-el/javax/el/ELProcessor.java | 7 ++ .../javax/el/ExpressionFactory.java | 17 ++++ .../java-ee-el/javax/el/LambdaExpression.java | 8 ++ .../java-ee-el/javax/el/MethodExpression.java | 5 ++ .../javax/el/StandardELContext.java | 5 ++ .../java-ee-el/javax/el/ValueExpression.java | 6 ++ .../de/odysseus/el/ExpressionFactoryImpl.java | 5 ++ .../de/odysseus/el/util/SimpleContext.java | 5 ++ 16 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.qlref create mode 100644 java/ql/test/stubs/java-ee-el/javax/el/ELContext.java create mode 100644 java/ql/test/stubs/java-ee-el/javax/el/ELManager.java create mode 100644 java/ql/test/stubs/java-ee-el/javax/el/ELProcessor.java create mode 100644 java/ql/test/stubs/java-ee-el/javax/el/ExpressionFactory.java create mode 100644 java/ql/test/stubs/java-ee-el/javax/el/LambdaExpression.java create mode 100644 java/ql/test/stubs/java-ee-el/javax/el/MethodExpression.java create mode 100644 java/ql/test/stubs/java-ee-el/javax/el/StandardELContext.java create mode 100644 java/ql/test/stubs/java-ee-el/javax/el/ValueExpression.java create mode 100644 java/ql/test/stubs/juel-2.2/de/odysseus/el/ExpressionFactoryImpl.java create mode 100644 java/ql/test/stubs/juel-2.2/de/odysseus/el/util/SimpleContext.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql index b94c98201de..8190ec3d61f 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql @@ -1,6 +1,6 @@ /** - * @name Java EE Expression Language injection - * @description Evaluation of a user-controlled Jave EE expression + * @name Jakarta Expression Language injection + * @description Evaluation of a user-controlled expression * may lead to arbitrary code execution. * @kind path-problem * @problem.severity error @@ -11,10 +11,10 @@ */ import java -import JavaEEExpressionInjectionLib +import JakartaExpressionInjectionLib import DataFlow::PathGraph -from DataFlow::PathNode source, DataFlow::PathNode sink, JavaEEExpressionInjectionConfig conf +from DataFlow::PathNode source, DataFlow::PathNode sink, JakartaExpressionInjectionConfig conf where conf.hasFlowPath(source, sink) -select sink.getNode(), source, sink, "Java EE Expression Language injection from $@.", +select sink.getNode(), source, sink, "Jakarta Expression Language injection from $@.", source.getNode(), "this user input" diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll index d43b31c9888..bc22f7c3257 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll @@ -5,10 +5,10 @@ import semmle.code.java.dataflow.TaintTracking /** * A taint-tracking configuration for unsafe user input - * that is used to construct and evaluate a Java EE expression. + * that is used to construct and evaluate an expression. */ -class JavaEEExpressionInjectionConfig extends TaintTracking::Configuration { - JavaEEExpressionInjectionConfig() { this = "JavaEEExpressionInjectionConfig" } +class JakartaExpressionInjectionConfig extends TaintTracking::Configuration { + JakartaExpressionInjectionConfig() { this = "JakartaExpressionInjectionConfig" } override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } @@ -22,7 +22,7 @@ class JavaEEExpressionInjectionConfig extends TaintTracking::Configuration { /** * A sink for Expresssion Language injection vulnerabilities, - * i.e. method calls that run evaluation of a Java EE expression. + * i.e. method calls that run evaluation of an expression. */ private class ExpressionEvaluationSink extends DataFlow::ExprNode { ExpressionEvaluationSink() { diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.expected new file mode 100644 index 00000000000..111cc81541c --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.expected @@ -0,0 +1,59 @@ +edges +| JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:24:31:24:40 | expression : String | +| JakartaExpressionInjection.java:24:31:24:40 | expression : String | JakartaExpressionInjection.java:30:24:30:33 | expression : String | +| JakartaExpressionInjection.java:24:31:24:40 | expression : String | JakartaExpressionInjection.java:37:24:37:33 | expression : String | +| JakartaExpressionInjection.java:24:31:24:40 | expression : String | JakartaExpressionInjection.java:44:24:44:33 | expression : String | +| JakartaExpressionInjection.java:24:31:24:40 | expression : String | JakartaExpressionInjection.java:54:24:54:33 | expression : String | +| JakartaExpressionInjection.java:24:31:24:40 | expression : String | JakartaExpressionInjection.java:61:24:61:33 | expression : String | +| JakartaExpressionInjection.java:24:31:24:40 | expression : String | JakartaExpressionInjection.java:70:24:70:33 | expression : String | +| JakartaExpressionInjection.java:24:31:24:40 | expression : String | JakartaExpressionInjection.java:79:24:79:33 | expression : String | +| JakartaExpressionInjection.java:30:24:30:33 | expression : String | JakartaExpressionInjection.java:32:28:32:37 | expression | +| JakartaExpressionInjection.java:37:24:37:33 | expression : String | JakartaExpressionInjection.java:39:32:39:41 | expression | +| JakartaExpressionInjection.java:44:24:44:33 | expression : String | JakartaExpressionInjection.java:49:13:49:28 | lambdaExpression | +| JakartaExpressionInjection.java:48:49:48:104 | new LambdaExpression(...) : LambdaExpression | JakartaExpressionInjection.java:49:13:49:28 | lambdaExpression | +| JakartaExpressionInjection.java:54:24:54:33 | expression : String | JakartaExpressionInjection.java:56:32:56:41 | expression | +| JakartaExpressionInjection.java:61:24:61:33 | expression : String | JakartaExpressionInjection.java:64:33:64:96 | createValueExpression(...) : ValueExpression | +| JakartaExpressionInjection.java:61:24:61:33 | expression : String | JakartaExpressionInjection.java:65:13:65:13 | e | +| JakartaExpressionInjection.java:61:24:61:33 | expression : String | JakartaExpressionInjection.java:65:13:65:13 | e : ValueExpression | +| JakartaExpressionInjection.java:64:33:64:96 | createValueExpression(...) : ValueExpression | JakartaExpressionInjection.java:48:49:48:104 | new LambdaExpression(...) : LambdaExpression | +| JakartaExpressionInjection.java:64:33:64:96 | createValueExpression(...) : ValueExpression | JakartaExpressionInjection.java:65:13:65:13 | e | +| JakartaExpressionInjection.java:64:33:64:96 | createValueExpression(...) : ValueExpression | JakartaExpressionInjection.java:65:13:65:13 | e : ValueExpression | +| JakartaExpressionInjection.java:65:13:65:13 | e : ValueExpression | JakartaExpressionInjection.java:48:49:48:104 | new LambdaExpression(...) : LambdaExpression | +| JakartaExpressionInjection.java:70:24:70:33 | expression : String | JakartaExpressionInjection.java:73:33:73:96 | createValueExpression(...) : ValueExpression | +| JakartaExpressionInjection.java:70:24:70:33 | expression : String | JakartaExpressionInjection.java:74:13:74:13 | e | +| JakartaExpressionInjection.java:70:24:70:33 | expression : String | JakartaExpressionInjection.java:74:13:74:13 | e : ValueExpression | +| JakartaExpressionInjection.java:73:33:73:96 | createValueExpression(...) : ValueExpression | JakartaExpressionInjection.java:48:49:48:104 | new LambdaExpression(...) : LambdaExpression | +| JakartaExpressionInjection.java:73:33:73:96 | createValueExpression(...) : ValueExpression | JakartaExpressionInjection.java:74:13:74:13 | e | +| JakartaExpressionInjection.java:73:33:73:96 | createValueExpression(...) : ValueExpression | JakartaExpressionInjection.java:74:13:74:13 | e : ValueExpression | +| JakartaExpressionInjection.java:74:13:74:13 | e : ValueExpression | JakartaExpressionInjection.java:48:49:48:104 | new LambdaExpression(...) : LambdaExpression | +| JakartaExpressionInjection.java:79:24:79:33 | expression : String | JakartaExpressionInjection.java:83:13:83:13 | e | +nodes +| JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream | +| JakartaExpressionInjection.java:24:31:24:40 | expression : String | semmle.label | expression : String | +| JakartaExpressionInjection.java:30:24:30:33 | expression : String | semmle.label | expression : String | +| JakartaExpressionInjection.java:32:28:32:37 | expression | semmle.label | expression | +| JakartaExpressionInjection.java:37:24:37:33 | expression : String | semmle.label | expression : String | +| JakartaExpressionInjection.java:39:32:39:41 | expression | semmle.label | expression | +| JakartaExpressionInjection.java:44:24:44:33 | expression : String | semmle.label | expression : String | +| JakartaExpressionInjection.java:48:49:48:104 | new LambdaExpression(...) : LambdaExpression | semmle.label | new LambdaExpression(...) : LambdaExpression | +| JakartaExpressionInjection.java:49:13:49:28 | lambdaExpression | semmle.label | lambdaExpression | +| JakartaExpressionInjection.java:54:24:54:33 | expression : String | semmle.label | expression : String | +| JakartaExpressionInjection.java:56:32:56:41 | expression | semmle.label | expression | +| JakartaExpressionInjection.java:61:24:61:33 | expression : String | semmle.label | expression : String | +| JakartaExpressionInjection.java:64:33:64:96 | createValueExpression(...) : ValueExpression | semmle.label | createValueExpression(...) : ValueExpression | +| JakartaExpressionInjection.java:65:13:65:13 | e | semmle.label | e | +| JakartaExpressionInjection.java:65:13:65:13 | e : ValueExpression | semmle.label | e : ValueExpression | +| JakartaExpressionInjection.java:70:24:70:33 | expression : String | semmle.label | expression : String | +| JakartaExpressionInjection.java:73:33:73:96 | createValueExpression(...) : ValueExpression | semmle.label | createValueExpression(...) : ValueExpression | +| JakartaExpressionInjection.java:74:13:74:13 | e | semmle.label | e | +| JakartaExpressionInjection.java:74:13:74:13 | e : ValueExpression | semmle.label | e : ValueExpression | +| JakartaExpressionInjection.java:79:24:79:33 | expression : String | semmle.label | expression : String | +| JakartaExpressionInjection.java:83:13:83:13 | e | semmle.label | e | +#select +| JakartaExpressionInjection.java:32:28:32:37 | expression | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:32:28:32:37 | expression | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) | this user input | +| JakartaExpressionInjection.java:39:32:39:41 | expression | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:39:32:39:41 | expression | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) | this user input | +| JakartaExpressionInjection.java:49:13:49:28 | lambdaExpression | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:49:13:49:28 | lambdaExpression | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) | this user input | +| JakartaExpressionInjection.java:56:32:56:41 | expression | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:56:32:56:41 | expression | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) | this user input | +| JakartaExpressionInjection.java:65:13:65:13 | e | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:65:13:65:13 | e | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) | this user input | +| JakartaExpressionInjection.java:74:13:74:13 | e | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:74:13:74:13 | e | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) | this user input | +| JakartaExpressionInjection.java:83:13:83:13 | e | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:83:13:83:13 | e | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:22:25:22:47 | getInputStream(...) | this user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.java b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.java new file mode 100644 index 00000000000..a9fb2d45d6f --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.java @@ -0,0 +1,87 @@ +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.function.Consumer; + +import javax.el.ELContext; +import javax.el.ELManager; +import javax.el.ELProcessor; +import javax.el.ExpressionFactory; +import javax.el.LambdaExpression; +import javax.el.MethodExpression; +import javax.el.StandardELContext; +import javax.el.ValueExpression; + +public class JakartaExpressionInjection { + + private static void testWithSocket(Consumer action) throws IOException { + try (ServerSocket serverSocket = new ServerSocket(0)) { + try (Socket socket = serverSocket.accept()) { + byte[] bytes = new byte[1024]; + int n = socket.getInputStream().read(bytes); + String expression = new String(bytes, 0, n); + action.accept(expression); + } + } + } + + private static void testWithELProcessorEval() throws IOException { + testWithSocket(expression -> { + ELProcessor processor = new ELProcessor(); + processor.eval(expression); + }); + } + + private static void testWithELProcessorGetValue() throws IOException { + testWithSocket(expression -> { + ELProcessor processor = new ELProcessor(); + processor.getValue(expression, Object.class); + }); + } + + private static void testWithLambdaExpressionInvoke() throws IOException { + testWithSocket(expression -> { + ExpressionFactory factory = ELManager.getExpressionFactory(); + StandardELContext context = new StandardELContext(factory); + ValueExpression valueExpression = factory.createValueExpression(context, expression, Object.class); + LambdaExpression lambdaExpression = new LambdaExpression(new ArrayList<>(), valueExpression); + lambdaExpression.invoke(context, new Object[0]); + }); + } + + private static void testWithELProcessorSetValue() throws IOException { + testWithSocket(expression -> { + ELProcessor processor = new ELProcessor(); + processor.setValue(expression, new Object()); + }); + } + + private static void testWithJuelValueExpressionGetValue() throws IOException { + testWithSocket(expression -> { + ExpressionFactory factory = new de.odysseus.el.ExpressionFactoryImpl(); + ELContext context = new de.odysseus.el.util.SimpleContext(); + ValueExpression e = factory.createValueExpression(context, expression, Object.class); + e.getValue(context); + }); + } + + private static void testWithJuelValueExpressionSetValue() throws IOException { + testWithSocket(expression -> { + ExpressionFactory factory = new de.odysseus.el.ExpressionFactoryImpl(); + ELContext context = new de.odysseus.el.util.SimpleContext(); + ValueExpression e = factory.createValueExpression(context, expression, Object.class); + e.setValue(context, new Object()); + }); + } + + private static void testWithJuelMethodExpressionInvoke() throws IOException { + testWithSocket(expression -> { + ExpressionFactory factory = new de.odysseus.el.ExpressionFactoryImpl(); + ELContext context = new de.odysseus.el.util.SimpleContext(); + MethodExpression e = factory.createMethodExpression(context, expression, Object.class, new Class[0]); + e.invoke(context, new Object[0]); + }); + } + +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.qlref new file mode 100644 index 00000000000..3154ee5ccad --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/options b/java/ql/test/experimental/query-tests/security/CWE-094/options index a8e30ce30b4..ec3354ea41c 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-094/options +++ b/java/ql/test/experimental/query-tests/security/CWE-094/options @@ -1,2 +1 @@ -//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine - +//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3:${testdir}/../../../../stubs/mvel2-2.4.7:${testdir}/../../../../stubs/jsr223-api:${testdir}/../../../../stubs/apache-commons-jexl-2.1.1:${testdir}/../../../../stubs/apache-commons-jexl-3.1:${testdir}/../../../../stubs/scriptengine:${testdir}/../../../../stubs/java-ee-el:${testdir}/../../../../stubs/juel-2.2 \ No newline at end of file diff --git a/java/ql/test/stubs/java-ee-el/javax/el/ELContext.java b/java/ql/test/stubs/java-ee-el/javax/el/ELContext.java new file mode 100644 index 00000000000..ce3840c69c8 --- /dev/null +++ b/java/ql/test/stubs/java-ee-el/javax/el/ELContext.java @@ -0,0 +1,3 @@ +package javax.el; + +public class ELContext {} diff --git a/java/ql/test/stubs/java-ee-el/javax/el/ELManager.java b/java/ql/test/stubs/java-ee-el/javax/el/ELManager.java new file mode 100644 index 00000000000..7d24f739a3f --- /dev/null +++ b/java/ql/test/stubs/java-ee-el/javax/el/ELManager.java @@ -0,0 +1,5 @@ +package javax.el; + +public class ELManager { + public static ExpressionFactory getExpressionFactory() { return null; } +} \ No newline at end of file diff --git a/java/ql/test/stubs/java-ee-el/javax/el/ELProcessor.java b/java/ql/test/stubs/java-ee-el/javax/el/ELProcessor.java new file mode 100644 index 00000000000..3c523e685c5 --- /dev/null +++ b/java/ql/test/stubs/java-ee-el/javax/el/ELProcessor.java @@ -0,0 +1,7 @@ +package javax.el; + +public class ELProcessor { + public Object eval(String expression) { return null; } + public Object getValue(String expression, Class expectedType) { return null; } + public void setValue(String expression, Object value) {} +} diff --git a/java/ql/test/stubs/java-ee-el/javax/el/ExpressionFactory.java b/java/ql/test/stubs/java-ee-el/javax/el/ExpressionFactory.java new file mode 100644 index 00000000000..31ff79169ac --- /dev/null +++ b/java/ql/test/stubs/java-ee-el/javax/el/ExpressionFactory.java @@ -0,0 +1,17 @@ +package javax.el; + +public class ExpressionFactory { + public MethodExpression createMethodExpression(ELContext context, String expression, Class expectedReturnType, + Class[] expectedParamTypes) { + + return null; + } + + public ValueExpression createValueExpression(ELContext context, String expression, Class expectedType) { + return null; + } + + public ValueExpression createValueExpression(Object instance, Class expectedType) { + return null; + } +} diff --git a/java/ql/test/stubs/java-ee-el/javax/el/LambdaExpression.java b/java/ql/test/stubs/java-ee-el/javax/el/LambdaExpression.java new file mode 100644 index 00000000000..4be01e9d2e4 --- /dev/null +++ b/java/ql/test/stubs/java-ee-el/javax/el/LambdaExpression.java @@ -0,0 +1,8 @@ +package javax.el; + +import java.util.List; + +public class LambdaExpression { + public LambdaExpression(List formalParameters, ValueExpression expression) {} + public Object invoke(Object... args) { return null; } +} diff --git a/java/ql/test/stubs/java-ee-el/javax/el/MethodExpression.java b/java/ql/test/stubs/java-ee-el/javax/el/MethodExpression.java new file mode 100644 index 00000000000..ac50ece80e3 --- /dev/null +++ b/java/ql/test/stubs/java-ee-el/javax/el/MethodExpression.java @@ -0,0 +1,5 @@ +package javax.el; + +public class MethodExpression { + public Object invoke(ELContext context, Object[] params) { return null; } +} diff --git a/java/ql/test/stubs/java-ee-el/javax/el/StandardELContext.java b/java/ql/test/stubs/java-ee-el/javax/el/StandardELContext.java new file mode 100644 index 00000000000..22e3f2a9fc1 --- /dev/null +++ b/java/ql/test/stubs/java-ee-el/javax/el/StandardELContext.java @@ -0,0 +1,5 @@ +package javax.el; + +public class StandardELContext extends ELContext { + public StandardELContext(ExpressionFactory factory) {} +} diff --git a/java/ql/test/stubs/java-ee-el/javax/el/ValueExpression.java b/java/ql/test/stubs/java-ee-el/javax/el/ValueExpression.java new file mode 100644 index 00000000000..9a231215640 --- /dev/null +++ b/java/ql/test/stubs/java-ee-el/javax/el/ValueExpression.java @@ -0,0 +1,6 @@ +package javax.el; + +public class ValueExpression { + public Object getValue(ELContext context) { return null; } + public void setValue(ELContext context, Object value) {} +} diff --git a/java/ql/test/stubs/juel-2.2/de/odysseus/el/ExpressionFactoryImpl.java b/java/ql/test/stubs/juel-2.2/de/odysseus/el/ExpressionFactoryImpl.java new file mode 100644 index 00000000000..a555cf88dee --- /dev/null +++ b/java/ql/test/stubs/juel-2.2/de/odysseus/el/ExpressionFactoryImpl.java @@ -0,0 +1,5 @@ +package de.odysseus.el; + +import javax.el.ExpressionFactory; + +public class ExpressionFactoryImpl extends ExpressionFactory {} diff --git a/java/ql/test/stubs/juel-2.2/de/odysseus/el/util/SimpleContext.java b/java/ql/test/stubs/juel-2.2/de/odysseus/el/util/SimpleContext.java new file mode 100644 index 00000000000..aa4f449e5fa --- /dev/null +++ b/java/ql/test/stubs/juel-2.2/de/odysseus/el/util/SimpleContext.java @@ -0,0 +1,5 @@ +package de.odysseus.el.util; + +import javax.el.ELContext; + +public class SimpleContext extends ELContext {} From 6c246994035a35925d870f97abc58c04162d53f9 Mon Sep 17 00:00:00 2001 From: Artem Smotrakov Date: Sun, 21 Mar 2021 21:16:00 +0300 Subject: [PATCH 0060/1429] Cover both javax.el and jakarta.el packages --- .../CWE-094/JakartaExpressionInjectionLib.qll | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll index bc22f7c3257..22f421b8bf8 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll @@ -77,22 +77,26 @@ private class TaintPropagatingCall extends Call { } } -private class ELProcessor extends RefType { - ELProcessor() { hasQualifiedName("javax.el", "ELProcessor") } +private class JakartaType extends RefType { + JakartaType() { getPackage().hasName(["javax.el", "jakarta.el"]) } } -private class ExpressionFactory extends RefType { - ExpressionFactory() { hasQualifiedName("javax.el", "ExpressionFactory") } +private class ELProcessor extends JakartaType { + ELProcessor() { hasName("ELProcessor") } } -private class ValueExpression extends RefType { - ValueExpression() { hasQualifiedName("javax.el", "ValueExpression") } +private class ExpressionFactory extends JakartaType { + ExpressionFactory() { hasName("ExpressionFactory") } } -private class MethodExpression extends RefType { - MethodExpression() { hasQualifiedName("javax.el", "MethodExpression") } +private class ValueExpression extends JakartaType { + ValueExpression() { hasName("ValueExpression") } +} + +private class MethodExpression extends JakartaType { + MethodExpression() { hasName("MethodExpression") } } private class LambdaExpression extends RefType { - LambdaExpression() { hasQualifiedName("javax.el", "LambdaExpression") } + LambdaExpression() { hasName("LambdaExpression") } } From 1534b387bb64cdcd6d88cf36f8516d2b830fd4d5 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Mon, 22 Mar 2021 00:54:14 +0100 Subject: [PATCH 0061/1429] Java: Improve documentation regarding minus in front of numeric literals --- java/ql/src/semmle/code/java/Expr.qll | 45 ++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/java/ql/src/semmle/code/java/Expr.qll b/java/ql/src/semmle/code/java/Expr.qll index 43ad6634fc1..2e90075c1eb 100755 --- a/java/ql/src/semmle/code/java/Expr.qll +++ b/java/ql/src/semmle/code/java/Expr.qll @@ -638,7 +638,20 @@ class BooleanLiteral extends Literal, @booleanliteral { override string getAPrimaryQlClass() { result = "BooleanLiteral" } } -/** An integer literal. For example, `23`. */ +/** + * An integer literal. For example, `23`. + * + * An integer literal can never be negative except when: + * - It is written in binary, octal or hexadecimal notation + * - It is written in decimal notation, has the value `2147483648` and is preceded + * by a minus; in this case the value of the IntegerLiteral is -2147483648 and + * the preceding minus will *not* be modeled as `MinusExpr`.
    + * In all other cases the preceding minus, if any, will be modeled as separate + * `MinusExpr`. + * + * The last exception is necessary because `2147483648` on its own would not be + * a valid integer literal (and could also not be parsed as CodeQL `int`). + */ class IntegerLiteral extends Literal, @integerliteral { /** Gets the int representation of this literal. */ int getIntValue() { result = getValue().toInt() } @@ -646,17 +659,41 @@ class IntegerLiteral extends Literal, @integerliteral { override string getAPrimaryQlClass() { result = "IntegerLiteral" } } -/** A long literal. For example, `23l`. */ +/** + * A long literal. For example, `23L`. + * + * A long literal can never be negative except when: + * - It is written in binary, octal or hexadecimal notation + * - It is written in decimal notation, has the value `9223372036854775808` and + * is preceded by a minus; in this case the value of the LongLiteral is + * -9223372036854775808 and the preceding minus will *not* be modeled as + * `MinusExpr`.
    + * In all other cases the preceding minus, if any, will be modeled as separate + * `MinusExpr`. + * + * The last exception is necessary because `9223372036854775808` on its own + * would not be a valid long literal. + */ class LongLiteral extends Literal, @longliteral { override string getAPrimaryQlClass() { result = "LongLiteral" } } -/** A floating point literal. For example, `4.2f`. */ +/** + * A float literal. For example, `4.2f`. + * + * A float literal is never negative; a preceding minus, if any, will always + * be modeled as separate `MinusExpr`. + */ class FloatingPointLiteral extends Literal, @floatingpointliteral { override string getAPrimaryQlClass() { result = "FloatingPointLiteral" } } -/** A double literal. For example, `4.2`. */ +/** + * A double literal. For example, `4.2`. + * + * A double literal is never negative; a preceding minus, if any, will always + * be modeled as separate `MinusExpr`. + */ class DoubleLiteral extends Literal, @doubleliteral { override string getAPrimaryQlClass() { result = "DoubleLiteral" } } From 701b935564521d64cc35dc51753493f4dc2782f6 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 22 Mar 2021 00:57:43 +0100 Subject: [PATCH 0062/1429] Python: Add example of QuerySet chain (django) --- python/ql/test/library-tests/frameworks/django/SqlExecution.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/ql/test/library-tests/frameworks/django/SqlExecution.py b/python/ql/test/library-tests/frameworks/django/SqlExecution.py index 67a6ee81a5d..fba2ccdc73e 100644 --- a/python/ql/test/library-tests/frameworks/django/SqlExecution.py +++ b/python/ql/test/library-tests/frameworks/django/SqlExecution.py @@ -27,3 +27,6 @@ def test_model(): raw = RawSQL("so raw") User.objects.annotate(val=raw) # $getSql="so raw" + + # chaining QuerySet calls + User.objects.using("db-name").exclude(username="admin").extra("some sql") # $ MISSING: getSql="some sql" From 7a0bfd1a69c9a0eb7e13d665ad1f4ce67d05d6fd Mon Sep 17 00:00:00 2001 From: Tamas Vajk Date: Mon, 22 Mar 2021 12:20:35 +0100 Subject: [PATCH 0063/1429] Skip through any stub preamble --- csharp/ql/src/Stubs/make_stubs.py | 43 +++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/csharp/ql/src/Stubs/make_stubs.py b/csharp/ql/src/Stubs/make_stubs.py index 19a917cd8ab..dcdd9e87cfe 100644 --- a/csharp/ql/src/Stubs/make_stubs.py +++ b/csharp/ql/src/Stubs/make_stubs.py @@ -71,19 +71,36 @@ if subprocess.check_call(cmd): print('Failed to run the query to generate output file.') exit(1) -# Remove the leading " and trailing " bytes from the file -len = os.stat(outputFile).st_size -f = open(outputFile, "rb") -try: - quote = f.read(6) - if quote != b"\x02\x01\x86'\x85'": - print("Unexpected start characters in file.", quote) - contents = f.read(len-21) - quote = f.read(15) - if quote != b'\x0e\x01\x08#select\x01\x01\x00s\x00': - print("Unexpected end character in file.", quote) -finally: - f.close() +# Remove the leading and trailing bytes from the file +length = os.stat(outputFile).st_size +if length < 20: + contents = b'' +else: + f = open(outputFile, "rb") + try: + countTillSlash = 0 + foundSlash = False + slash = f.read(1) + while slash != b'': + if slash == b'/': + foundSlash = True + break + countTillSlash += 1 + slash = f.read(1) + + if not foundSlash: + countTillSlash = 0 + + f.seek(0) + quote = f.read(countTillSlash) + print("Start characters in file skipped.", quote) + post = b'\x0e\x01\x08#select\x01\x01\x00s\x00' + contents = f.read(length - len(post) - countTillSlash) + quote = f.read(len(post)) + if quote != post: + print("Unexpected end character in file.", quote) + finally: + f.close() f = open(outputFile, "wb") f.write(contents) From c8a6e837b57513df5c8ccd081e193be3443987d1 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 22 Mar 2021 14:36:29 +0100 Subject: [PATCH 0064/1429] Python: Model QuerySet chains in django --- .../2021-03-22-django-queryset-chains.md | 2 + .../src/semmle/python/frameworks/Django.qll | 128 +++++++----------- .../frameworks/django/SqlExecution.py | 2 +- 3 files changed, 54 insertions(+), 78 deletions(-) create mode 100644 python/change-notes/2021-03-22-django-queryset-chains.md diff --git a/python/change-notes/2021-03-22-django-queryset-chains.md b/python/change-notes/2021-03-22-django-queryset-chains.md new file mode 100644 index 00000000000..116a7573360 --- /dev/null +++ b/python/change-notes/2021-03-22-django-queryset-chains.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Improved modeling of `django` to recognize QuerySet chains such as `User.objects.using("db-name").exclude(username="admin").extra("some sql")`. This can lead to new results for `py/sql-injection`. diff --git a/python/ql/src/semmle/python/frameworks/Django.qll b/python/ql/src/semmle/python/frameworks/Django.qll index 9f56bd5a299..384fdf69a15 100644 --- a/python/ql/src/semmle/python/frameworks/Django.qll +++ b/python/ql/src/semmle/python/frameworks/Django.qll @@ -8,6 +8,7 @@ private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking private import semmle.python.Concepts +private import semmle.python.ApiGraphs private import semmle.python.frameworks.PEP249 private import semmle.python.regex @@ -104,9 +105,6 @@ private module Django { // ------------------------------------------------------------------------- // django.db.models // ------------------------------------------------------------------------- - // NOTE: The modelling of django models is currently fairly incomplete. - // It does not fully take `Model`s, `Manager`s, `and QuerySet`s into account. - // It simply identifies some common dangerous cases. /** Gets a reference to the `django.db.models` module. */ private DataFlow::Node models(DataFlow::TypeTracker t) { t.start() and @@ -123,72 +121,52 @@ private module Django { /** Provides models for the `django.db.models` module. */ module models { - /** Provides models for the `django.db.models.Model` class. */ + /** + * Provides models for the `django.db.models.Model` class and subclasses. + * + * See https://docs.djangoproject.com/en/3.1/topics/db/models/. + */ module Model { - /** Gets a reference to the `django.db.models.Model` class. */ - private DataFlow::Node classRef(DataFlow::TypeTracker t) { - t.start() and - result = DataFlow::importNode("django.db.models.Model") - or - t.startInAttr("Model") and - result = models() - or - // subclass - result.asExpr().(ClassExpr).getABase() = classRef(t.continue()).asExpr() - or - exists(DataFlow::TypeTracker t2 | result = classRef(t2).track(t2, t)) + /** Gets a reference to the `flask.views.View` class or any subclass. */ + API::Node subclassRef() { + result = + API::moduleImport("django") + .getMember("db") + .getMember("models") + .getMember("Model") + .getASubclass*() } - - /** Gets a reference to the `django.db.models.Model` class. */ - DataFlow::Node classRef() { result = classRef(DataFlow::TypeTracker::end()) } - } - - /** Gets a reference to the `objects` object of a django model. */ - private DataFlow::Node objects(DataFlow::TypeTracker t) { - t.startInAttr("objects") and - result = Model::classRef() - or - exists(DataFlow::TypeTracker t2 | result = objects(t2).track(t2, t)) - } - - /** Gets a reference to the `objects` object of a model. */ - DataFlow::Node objects() { result = objects(DataFlow::TypeTracker::end()) } - - /** - * Gets a reference to the attribute `attr_name` of an `objects` object. - * WARNING: Only holds for a few predefined attributes. - */ - private DataFlow::Node objects_attr(DataFlow::TypeTracker t, string attr_name) { - attr_name in ["annotate", "extra", "raw"] and - t.startInAttr(attr_name) and - result = objects() - or - // Due to bad performance when using normal setup with `objects_attr(t2, attr_name).track(t2, t)` - // we have inlined that code and forced a join - exists(DataFlow::TypeTracker t2 | - exists(DataFlow::StepSummary summary | - objects_attr_first_join(t2, attr_name, result, summary) and - t = t2.append(summary) - ) - ) - } - - pragma[nomagic] - private predicate objects_attr_first_join( - DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res, - DataFlow::StepSummary summary - ) { - DataFlow::StepSummary::step(objects_attr(t2, attr_name), res, summary) } /** - * Gets a reference to the attribute `attr_name` of an `objects` object. - * WARNING: Only holds for a few predefined attributes. + * Gets a reference to the Manager (django.db.models.Manager) for a django Model, + * accessed by `.objects`. */ - DataFlow::Node objects_attr(string attr_name) { - result = objects_attr(DataFlow::TypeTracker::end(), attr_name) + API::Node manager() { result = Model::subclassRef().getMember("objects") } + + /** + * Gets a method with `name` that returns a QuerySet. + * This method can originate on a QuerySet or a Manager. + * + * See https://docs.djangoproject.com/en/3.1/ref/models/querysets/ + */ + API::Node querySetReturningMethod(string name) { + name in [ + "none", "all", "filter", "exclude", "complex_filter", "union", "intersection", + "difference", "select_for_update", "select_related", "prefetch_related", "order_by", + "distinct", "reverse", "defer", "only", "using", "annotate", "extra", "raw", + "datetimes", "dates", "values", "values_list" + ] and + result = [manager(), querySet()].getMember(name) } + /** + * Gets a reference to a QuerySet (django.db.models.query.QuerySet). + * + * See https://docs.djangoproject.com/en/3.1/ref/models/querysets/ + */ + API::Node querySet() { result = querySetReturningMethod(_).getReturn() } + /** Gets a reference to the `django.db.models.expressions` module. */ private DataFlow::Node expressions(DataFlow::TypeTracker t) { t.start() and @@ -253,14 +231,13 @@ private module Django { * * See https://docs.djangoproject.com/en/3.1/ref/models/querysets/#annotate */ - private class ObjectsAnnotate extends SqlExecution::Range, DataFlow::CfgNode { - override CallNode node; + private class ObjectsAnnotate extends SqlExecution::Range, DataFlow::CallCfgNode { ControlFlowNode sql; ObjectsAnnotate() { - node.getFunction() = django::db::models::objects_attr("annotate").asCfgNode() and - django::db::models::expressions::RawSQL::instance(sql).asCfgNode() in [ - node.getArg(_), node.getArgByName(_) + this = django::db::models::querySetReturningMethod("annotate").getACall() and + django::db::models::expressions::RawSQL::instance(sql) in [ + this.getArg(_), this.getArgByName(_) ] } @@ -274,12 +251,10 @@ private module Django { * - https://docs.djangoproject.com/en/3.1/topics/db/sql/#django.db.models.Manager.raw * - https://docs.djangoproject.com/en/3.1/ref/models/querysets/#raw */ - private class ObjectsRaw extends SqlExecution::Range, DataFlow::CfgNode { - override CallNode node; + private class ObjectsRaw extends SqlExecution::Range, DataFlow::CallCfgNode { + ObjectsRaw() { this = django::db::models::querySetReturningMethod("raw").getACall() } - ObjectsRaw() { node.getFunction() = django::db::models::objects_attr("raw").asCfgNode() } - - override DataFlow::Node getSql() { result.asCfgNode() = node.getArg(0) } + override DataFlow::Node getSql() { result = this.getArg(0) } } /** @@ -287,14 +262,13 @@ private module Django { * * See https://docs.djangoproject.com/en/3.1/ref/models/querysets/#extra */ - private class ObjectsExtra extends SqlExecution::Range, DataFlow::CfgNode { - override CallNode node; - - ObjectsExtra() { node.getFunction() = django::db::models::objects_attr("extra").asCfgNode() } + private class ObjectsExtra extends SqlExecution::Range, DataFlow::CallCfgNode { + ObjectsExtra() { this = django::db::models::querySetReturningMethod("extra").getACall() } override DataFlow::Node getSql() { - result.asCfgNode() = - [node.getArg([0, 1, 3, 4]), node.getArgByName(["select", "where", "tables", "order_by"])] + result in [ + this.getArg([0, 1, 3, 4]), this.getArgByName(["select", "where", "tables", "order_by"]) + ] } } diff --git a/python/ql/test/library-tests/frameworks/django/SqlExecution.py b/python/ql/test/library-tests/frameworks/django/SqlExecution.py index fba2ccdc73e..449983fc898 100644 --- a/python/ql/test/library-tests/frameworks/django/SqlExecution.py +++ b/python/ql/test/library-tests/frameworks/django/SqlExecution.py @@ -29,4 +29,4 @@ def test_model(): User.objects.annotate(val=raw) # $getSql="so raw" # chaining QuerySet calls - User.objects.using("db-name").exclude(username="admin").extra("some sql") # $ MISSING: getSql="some sql" + User.objects.using("db-name").exclude(username="admin").extra("some sql") # $ getSql="some sql" From 993999f64fd519d358b1276d19b81215a7073e6f Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Mon, 22 Mar 2021 17:37:54 +0100 Subject: [PATCH 0065/1429] Java: Add test for negative numeric literals --- .../literals-numeric/NumericLiterals.java | 16 ++++++++++++++++ .../negativeNumericLiteral.expected | 12 ++++++++++++ .../literals-numeric/negativeNumericLiteral.ql | 9 +++++++++ 3 files changed, 37 insertions(+) create mode 100644 java/ql/test/library-tests/literals-numeric/NumericLiterals.java create mode 100644 java/ql/test/library-tests/literals-numeric/negativeNumericLiteral.expected create mode 100644 java/ql/test/library-tests/literals-numeric/negativeNumericLiteral.ql diff --git a/java/ql/test/library-tests/literals-numeric/NumericLiterals.java b/java/ql/test/library-tests/literals-numeric/NumericLiterals.java new file mode 100644 index 00000000000..02f2fbfcc6b --- /dev/null +++ b/java/ql/test/library-tests/literals-numeric/NumericLiterals.java @@ -0,0 +1,16 @@ +class NumericLiterals { + void negativeLiterals() { + float f = -1f; + double d = -1d; + int i1 = -2147483647; + int i2 = -2147483648; // CodeQL models minus as part of literal + int i3 = -0b10000000000000000000000000000000; // binary + int i4 = -020000000000; // octal + int i5 = -0x80000000; // hex + long l1 = -9223372036854775807L; + long l2 = -9223372036854775808L; // CodeQL models minus as part of literal + long l3 = -0b1000000000000000000000000000000000000000000000000000000000000000L; // binary + long l4 = -01000000000000000000000L; // octal + long l5 = -0x8000000000000000L; // hex + } +} diff --git a/java/ql/test/library-tests/literals-numeric/negativeNumericLiteral.expected b/java/ql/test/library-tests/literals-numeric/negativeNumericLiteral.expected new file mode 100644 index 00000000000..95100f259dd --- /dev/null +++ b/java/ql/test/library-tests/literals-numeric/negativeNumericLiteral.expected @@ -0,0 +1,12 @@ +| NumericLiterals.java:3:14:3:15 | 1f | 1.0 | NumericLiterals.java:3:13:3:15 | -... | +| NumericLiterals.java:4:15:4:16 | 1d | 1.0 | NumericLiterals.java:4:14:4:16 | -... | +| NumericLiterals.java:5:13:5:22 | 2147483647 | 2147483647 | NumericLiterals.java:5:12:5:22 | -... | +| NumericLiterals.java:6:12:6:22 | -2147483648 | -2147483648 | NumericLiterals.java:6:7:6:22 | i2 | +| NumericLiterals.java:7:13:7:46 | 0b10000000000000000000000000000000 | -2147483648 | NumericLiterals.java:7:12:7:46 | -... | +| NumericLiterals.java:8:13:8:24 | 020000000000 | -2147483648 | NumericLiterals.java:8:12:8:24 | -... | +| NumericLiterals.java:9:13:9:22 | 0x80000000 | -2147483648 | NumericLiterals.java:9:12:9:22 | -... | +| NumericLiterals.java:10:14:10:33 | 9223372036854775807L | 9223372036854775807 | NumericLiterals.java:10:13:10:33 | -... | +| NumericLiterals.java:11:13:11:33 | -9223372036854775808L | -9223372036854775808 | NumericLiterals.java:11:8:11:33 | l2 | +| NumericLiterals.java:12:14:12:80 | 0b1000000000000000000000000000000000000000000000000000000000000000L | -9223372036854775808 | NumericLiterals.java:12:13:12:80 | -... | +| NumericLiterals.java:13:14:13:37 | 01000000000000000000000L | -9223372036854775808 | NumericLiterals.java:13:13:13:37 | -... | +| NumericLiterals.java:14:14:14:32 | 0x8000000000000000L | -9223372036854775808 | NumericLiterals.java:14:13:14:32 | -... | diff --git a/java/ql/test/library-tests/literals-numeric/negativeNumericLiteral.ql b/java/ql/test/library-tests/literals-numeric/negativeNumericLiteral.ql new file mode 100644 index 00000000000..0fbb3989c7a --- /dev/null +++ b/java/ql/test/library-tests/literals-numeric/negativeNumericLiteral.ql @@ -0,0 +1,9 @@ +import java + +from Literal l +where + l instanceof IntegerLiteral or + l instanceof LongLiteral or + l instanceof FloatingPointLiteral or + l instanceof DoubleLiteral +select l, l.getValue(), l.getParent() From 0e81fd26248875838c751ce44345aee1b8e6c36d Mon Sep 17 00:00:00 2001 From: Taus Brock-Nannestad Date: Mon, 22 Mar 2021 18:41:22 +0100 Subject: [PATCH 0066/1429] Python: Move `Boolean` into `TypeTrackerPrivate` In general, this may be defined already for other languages, so moving it in here will avoid potential clashes. --- python/ql/src/experimental/typetracking/TypeTracker.qll | 5 ----- .../src/experimental/typetracking/TypeTrackerPrivate.qll | 7 +++++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/python/ql/src/experimental/typetracking/TypeTracker.qll b/python/ql/src/experimental/typetracking/TypeTracker.qll index f21d3a2a91b..056e7ba5cf0 100644 --- a/python/ql/src/experimental/typetracking/TypeTracker.qll +++ b/python/ql/src/experimental/typetracking/TypeTracker.qll @@ -78,11 +78,6 @@ module StepSummary { } } -/** - * A utility class that is equivalent to `boolean` but does not require type joining. - */ -private class Boolean extends boolean { - Boolean() { this = true or this = false } } private newtype TTypeTracker = MkTypeTracker(Boolean hasCall, OptionalContentName content) diff --git a/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll b/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll index d26a76b3355..ab39e8be5be 100644 --- a/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll +++ b/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll @@ -95,3 +95,10 @@ predicate basicLoadStep(Node nodeFrom, Node nodeTo, string content) { nodeTo = a ) } + +/** + * A utility class that is equivalent to `boolean` but does not require type joining. + */ +class Boolean extends boolean { + Boolean() { this = true or this = false } +} From 7cdf439b83b3d166ac0b9083968178b7a1e380ca Mon Sep 17 00:00:00 2001 From: Taus Brock-Nannestad Date: Mon, 22 Mar 2021 18:42:24 +0100 Subject: [PATCH 0067/1429] Python: Clean up `basicStoreStep` Moves the `flowsTo` logic into the shared implementation, so that `TypeTrackingPrivate` only has to define the shape of immediate store steps. Also cleans up the documentation to talk a bit more about what `content` can represent, and what caveats there are. --- .../experimental/typetracking/TypeTracker.qll | 48 +++++++++++++++++-- .../typetracking/TypeTrackerPrivate.qll | 32 ++----------- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/python/ql/src/experimental/typetracking/TypeTracker.qll b/python/ql/src/experimental/typetracking/TypeTracker.qll index 056e7ba5cf0..46461d3e22e 100644 --- a/python/ql/src/experimental/typetracking/TypeTracker.qll +++ b/python/ql/src/experimental/typetracking/TypeTracker.qll @@ -2,7 +2,18 @@ private import TypeTrackerPrivate -/** Any string that may appear as the name of a piece of content. */ +/** + * Any string that may appear as the name of a piece of content. This will usually include things like: + * - Attribute names (in Python) + * - Property names (in JavaScript) + * + * In general, this can also be used to model things like stores to specific list indices. To ensure + * correctness, it is important that + * + * - different types of content do not have overlapping names, and + * - the empty string `""` is not a valid piece of content, as it is used to indicate the absence of + * content instead. + */ class ContentName extends string { ContentName() { this = getPossibleContentName() } } @@ -70,14 +81,41 @@ module StepSummary { summary = ReturnStep() or exists(string content | - basicStoreStep(nodeFrom, nodeTo, content) and + localSourceStoreStep(nodeFrom, nodeTo, content) and summary = StoreStep(content) or basicLoadStep(nodeFrom, nodeTo, content) and summary = LoadStep(content) ) } -} + /** + * Holds if `nodeFrom` is being written to the `content` content of the object in `nodeTo`. + * + * Note that `nodeTo` will always be a local source node that flows to the place where the content + * is written in `basicStoreStep`. This may lead to the flow of information going "back in time" + * from the point of view of the execution of the program. + * + * For instance, if we interpret attribute writes in Python as writing to content with the same + * name as the attribute and consider the following snippet + * + * ```python + * def foo(y): + * x = Foo() + * bar(x) + * x.attr = y + * baz(x) + * + * def bar(x): + * z = x.attr + * ``` + * for the attribute write `x.attr = y`, we will have `content` being the literal string `"attr"`, + * `nodeFrom` will be `y`, and `nodeTo` will be the object `Foo()` created on the first line of the + * function. This means we will track the fact that `x.attr` can have the type of `y` into the + * assignment to `z` inside `bar`, even though this attribute write happens _after_ `bar` is called. + */ + predicate localSourceStoreStep(Node nodeFrom, LocalSourceNode nodeTo, string content) { + exists(Node obj | nodeTo.flowsTo(obj) and basicStoreStep(nodeFrom, obj, content)) + } } private newtype TTypeTracker = MkTypeTracker(Boolean hasCall, OptionalContentName content) @@ -92,7 +130,7 @@ private newtype TTypeTracker = MkTypeTracker(Boolean hasCall, OptionalContentNam * * It is recommended that all uses of this type are written in the following form, * for tracking some type `myType`: - * ``` + * ```ql * DataFlow::LocalSourceNode myType(DataFlow::TypeTracker t) { * t.start() and * result = < source of myType > @@ -253,7 +291,7 @@ private newtype TTypeBackTracker = MkTypeBackTracker(Boolean hasReturn, Optional * It is recommended that all uses of this type are written in the following form, * for back-tracking some callback type `myCallback`: * - * ``` + * ```ql * DataFlow::LocalSourceNode myCallback(DataFlow::TypeBackTracker t) { * t.start() and * result = (< some API call >).getArgument(< n >).getALocalSource() diff --git a/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll b/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll index ab39e8be5be..6469eb93d98 100644 --- a/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll +++ b/python/ql/src/experimental/typetracking/TypeTrackerPrivate.qll @@ -13,10 +13,8 @@ predicate typePreservingStep(Node nodeFrom, Node nodeTo) { } /** - * Gets the name of a possible piece of content. This will usually include things like - * - * - Attribute names (in Python) - * - Property names (in JavaScript) + * Gets the name of a possible piece of content. For Python, this is currently only attribute names, + * using the name of the attribute for the corresponding content. */ string getPossibleContentName() { result = any(DataFlowPublic::AttrRef a).getAttributeName() } @@ -54,34 +52,12 @@ predicate returnStep(DataFlowPrivate::ReturnNode nodeFrom, Node nodeTo) { /** * Holds if `nodeFrom` is being written to the `content` content of the object in `nodeTo`. - * - * Note that the choice of `nodeTo` does not have to make sense "chronologically". - * All we care about is whether the `content` content of `nodeTo` can have a specific type, - * and the assumption is that if a specific type appears here, then any access of that - * particular content can yield something of that particular type. - * - * Thus, in an example such as - * - * ```python - * def foo(y): - * x = Foo() - * bar(x) - * x.content = y - * baz(x) - * - * def bar(x): - * z = x.content - * ``` - * for the content write `x.content = y`, we will have `content` being the literal string `"content"`, - * `nodeFrom` will be `y`, and `nodeTo` will be the object `Foo()` created on the first line of the - * function. This means we will track the fact that `x.content` can have the type of `y` into the - * assignment to `z` inside `bar`, even though this content write happens _after_ `bar` is called. */ -predicate basicStoreStep(Node nodeFrom, LocalSourceNode nodeTo, string content) { +predicate basicStoreStep(Node nodeFrom, Node nodeTo, string content) { exists(DataFlowPublic::AttrWrite a | a.mayHaveAttributeName(content) and nodeFrom = a.getValue() and - nodeTo.flowsTo(a.getObject()) + nodeTo = a.getObject() ) } From 08c3bf26d558d1bfaf0504fa1a0ba6837910829a Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Tue, 16 Mar 2021 03:18:26 +0000 Subject: [PATCH 0068/1429] Update the query to accommodate more cases --- .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 73 +++++++++++++++++-- .../SensitiveCookieNotHttpOnly.expected | 8 ++ .../CWE-1004/SensitiveCookieNotHttpOnly.java | 57 +++++++++++++++ 3 files changed, 130 insertions(+), 8 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index 693dad68082..ac33a6cecb2 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -33,7 +33,23 @@ predicate isSensitiveCookieNameExpr(Expr expr) { /** Holds if a string is concatenated with the `HttpOnly` flag. */ predicate hasHttpOnlyExpr(Expr expr) { - expr.(CompileTimeConstantExpr).getStringValue().toLowerCase().matches("%httponly%") or + ( + expr.(CompileTimeConstantExpr).getStringValue().toLowerCase().matches("%httponly%") + or + exists( + StaticMethodAccess ma // String.format("%s=%s;HttpOnly", "sessionkey", sessionKey) + | + ma.getType().getName() = "String" and + ma.getMethod().getName() = "format" and + ma.getArgument(0) + .(CompileTimeConstantExpr) + .getStringValue() + .toLowerCase() + .matches("%httponly%") and + expr = ma + ) + ) + or hasHttpOnlyExpr(expr.(AddExpr).getAnOperand()) } @@ -56,6 +72,40 @@ class CookieClass extends RefType { } } +/** Holds if the `Expr` expr is evaluated to boolean true. */ +predicate isBooleanTrue(Expr expr) { + expr.(CompileTimeConstantExpr).getBooleanValue() = true or + expr.(VarAccess).getVariable().getAnAssignedValue().(CompileTimeConstantExpr).getBooleanValue() = + true +} + +/** Holds if the method or a wrapper method sets the `HttpOnly` flag. */ +predicate setHttpOnlyInCookie(MethodAccess ma) { + ma.getMethod().getName() = "setHttpOnly" and + ( + isBooleanTrue(ma.getArgument(0)) // boolean literal true + or + exists( + MethodAccess mpa, int i // runtime assignment of boolean value true + | + TaintTracking::localTaint(DataFlow::parameterNode(mpa.getMethod().getParameter(i)), + DataFlow::exprNode(ma.getArgument(0))) and + isBooleanTrue(mpa.getArgument(i)) + ) + ) + or + exists(MethodAccess mca | + ma.getMethod().calls(mca.getMethod()) and + setHttpOnlyInCookie(mca) + ) +} + +/** Holds if the method or a wrapper method removes a cookie. */ +predicate removeCookie(MethodAccess ma) { + ma.getMethod().getName() = "setMaxAge" and + ma.getArgument(0).(IntegerLiteral).getIntValue() = 0 +} + /** Sensitive cookie name used in a `Cookie` constructor or a `Set-Cookie` call. */ class SensitiveCookieNameExpr extends Expr { SensitiveCookieNameExpr() { isSensitiveCookieNameExpr(this) } @@ -69,11 +119,16 @@ class CookieResponseSink extends DataFlow::ExprNode { ma.getMethod() instanceof ResponseAddCookieMethod and this.getExpr() = ma.getArgument(0) and not exists( - MethodAccess ma2 // cookie.setHttpOnly(true) + MethodAccess ma2 // a method or wrapper method that invokes cookie.setHttpOnly(true) | - ma2.getMethod().getName() = "setHttpOnly" and - ma2.getArgument(0).(BooleanLiteral).getBooleanValue() = true and - DataFlow::localExprFlow(ma2.getQualifier(), this.getExpr()) + ( + setHttpOnlyInCookie(ma2) or + removeCookie(ma2) + ) and + ( + DataFlow::localExprFlow(ma2.getQualifier(), this.getExpr()) or + DataFlow::localExprFlow(ma2, this.getExpr()) + ) ) or ma instanceof SetCookieMethodAccess and @@ -92,13 +147,15 @@ class CookieResponseSink extends DataFlow::ExprNode { predicate setHttpOnlyInNewCookie(ClassInstanceExpr cie) { cie.getConstructedType().hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and ( - cie.getNumArgument() = 6 and cie.getArgument(5).(BooleanLiteral).getBooleanValue() = true // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) + cie.getNumArgument() = 6 and + isBooleanTrue(cie.getArgument(5)) // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) or cie.getNumArgument() = 8 and cie.getArgument(6).getType() instanceof BooleanType and - cie.getArgument(7).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) + isBooleanTrue(cie.getArgument(7)) // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) or - cie.getNumArgument() = 10 and cie.getArgument(9).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) + cie.getNumArgument() = 10 and + isBooleanTrue(cie.getArgument(9)) // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly) ) } diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected index 8fa688bef2a..dae98e92e67 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected @@ -7,6 +7,9 @@ edges | SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | +| SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie | +| SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie | SensitiveCookieNotHttpOnly.java:102:25:102:64 | createAuthenticationCookie(...) : Cookie | +| SensitiveCookieNotHttpOnly.java:102:25:102:64 | createAuthenticationCookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:103:28:103:33 | cookie | nodes | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | semmle.label | "jwt_token" : String | | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | semmle.label | jwtCookie | @@ -21,6 +24,10 @@ nodes | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | semmle.label | ... + ... : String | | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | semmle.label | ... + ... : String | | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | semmle.label | secString | +| SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | semmle.label | "Presto-UI-Token" : String | +| SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie | semmle.label | cookie : Cookie | +| SensitiveCookieNotHttpOnly.java:102:25:102:64 | createAuthenticationCookie(...) : Cookie | semmle.label | createAuthenticationCookie(...) : Cookie | +| SensitiveCookieNotHttpOnly.java:103:28:103:33 | cookie | semmle.label | cookie | #select | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" | This sensitive cookie | | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" | This sensitive cookie | @@ -31,3 +38,4 @@ nodes | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" | This sensitive cookie | | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... | This sensitive cookie | | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:103:28:103:33 | cookie | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | SensitiveCookieNotHttpOnly.java:103:28:103:33 | cookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" | This sensitive cookie | diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java index 337a99cc096..fdc831d7836 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java @@ -71,6 +71,63 @@ class SensitiveCookieNotHttpOnly { response.addHeader("Set-Cookie", secString); } + // GOOD - Tests set a sensitive cookie header with the `HttpOnly` flag set using `String.format(...)`. + public void addCookie10(HttpServletRequest request, HttpServletResponse response) { + response.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly", "sessionkey", request.getSession().getAttribute("sessionkey"))); + } + + public Cookie createHttpOnlyAuthenticationCookie(HttpServletRequest request, String jwt) { + String PRESTO_UI_COOKIE = "Presto-UI-Token"; + Cookie cookie = new Cookie(PRESTO_UI_COOKIE, jwt); + cookie.setHttpOnly(true); + cookie.setPath("/ui"); + return cookie; + } + + public Cookie createAuthenticationCookie(HttpServletRequest request, String jwt) { + String PRESTO_UI_COOKIE = "Presto-UI-Token"; + Cookie cookie = new Cookie(PRESTO_UI_COOKIE, jwt); + cookie.setPath("/ui"); + return cookie; + } + + // GOOD - Tests set a sensitive cookie header with the `HttpOnly` flag set using a wrapper method. + public void addCookie11(HttpServletRequest request, HttpServletResponse response, String jwt) { + Cookie cookie = createHttpOnlyAuthenticationCookie(request, jwt); + response.addCookie(cookie); + } + + // BAD - Tests set a sensitive cookie header without the `HttpOnly` flag set using a wrapper method. + public void addCookie12(HttpServletRequest request, HttpServletResponse response, String jwt) { + Cookie cookie = createAuthenticationCookie(request, jwt); + response.addCookie(cookie); + } + + private Cookie createCookie(String name, String value, Boolean httpOnly){ + Cookie cookie = null; + cookie = new Cookie(name, value); + cookie.setDomain("/"); + cookie.setHttpOnly(httpOnly); + + //for production https + cookie.setSecure(true); + + cookie.setMaxAge(60*60*24*30); + cookie.setPath("/"); + + return cookie; + } + + // GOOD - Tests set a sensitive cookie header with the `HttpOnly` flag set through a boolean variable using a wrapper method. + public void addCookie13(HttpServletRequest request, HttpServletResponse response, String refreshToken) { + response.addCookie(createCookie("refresh_token", refreshToken, true)); + } + + // BAD - Tests set a sensitive cookie header with the `HttpOnly` flag not set through a boolean variable using a wrapper method. + public void addCookie14(HttpServletRequest request, HttpServletResponse response, String refreshToken) { + response.addCookie(createCookie("refresh_token", refreshToken, false)); + } + // GOOD - CSRF token doesn't need to have the `HttpOnly` flag set. public void addCsrfCookie(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Spring put the CSRF token in session attribute "_csrf" From fe0e7f5eac232e2aae155fe332c57c9e30e61cdf Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Thu, 25 Mar 2021 01:45:13 +0000 Subject: [PATCH 0069/1429] Change method check to taint flow --- .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 70 ++++++++++--------- .../SensitiveCookieNotHttpOnly.expected | 10 +-- .../CWE-1004/SensitiveCookieNotHttpOnly.java | 18 ++++- 3 files changed, 58 insertions(+), 40 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index ac33a6cecb2..db9adc2ee09 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -11,6 +11,7 @@ import java import semmle.code.java.dataflow.FlowSteps import semmle.code.java.frameworks.Servlets import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.dataflow.TaintTracking2 import DataFlow::PathGraph /** Gets a regular expression for matching common names of sensitive cookies. */ @@ -31,28 +32,6 @@ predicate isSensitiveCookieNameExpr(Expr expr) { isSensitiveCookieNameExpr(expr.(AddExpr).getAnOperand()) } -/** Holds if a string is concatenated with the `HttpOnly` flag. */ -predicate hasHttpOnlyExpr(Expr expr) { - ( - expr.(CompileTimeConstantExpr).getStringValue().toLowerCase().matches("%httponly%") - or - exists( - StaticMethodAccess ma // String.format("%s=%s;HttpOnly", "sessionkey", sessionKey) - | - ma.getType().getName() = "String" and - ma.getMethod().getName() = "format" and - ma.getArgument(0) - .(CompileTimeConstantExpr) - .getStringValue() - .toLowerCase() - .matches("%httponly%") and - expr = ma - ) - ) - or - hasHttpOnlyExpr(expr.(AddExpr).getAnOperand()) -} - /** The method call `Set-Cookie` of `addHeader` or `setHeader`. */ class SetCookieMethodAccess extends MethodAccess { SetCookieMethodAccess() { @@ -64,6 +43,22 @@ class SetCookieMethodAccess extends MethodAccess { } } +/** + * A taint configuration tracking flow from the text `httponly` to argument 1 of + * `SetCookieMethodAccess`. + */ +class MatchesHttpOnlyConfiguration extends TaintTracking2::Configuration { + MatchesHttpOnlyConfiguration() { this = "MatchesHttpOnlyConfiguration" } + + override predicate isSource(DataFlow::Node source) { + source.asExpr().(CompileTimeConstantExpr).getStringValue().toLowerCase().matches("%httponly%") + } + + override predicate isSink(DataFlow::Node sink) { + sink.asExpr() = any(SetCookieMethodAccess ma).getArgument(1) + } +} + /** The cookie class of Java EE. */ class CookieClass extends RefType { CookieClass() { @@ -79,7 +74,7 @@ predicate isBooleanTrue(Expr expr) { true } -/** Holds if the method or a wrapper method sets the `HttpOnly` flag. */ +/** Holds if the method call sets the `HttpOnly` flag. */ predicate setHttpOnlyInCookie(MethodAccess ma) { ma.getMethod().getName() = "setHttpOnly" and ( @@ -93,14 +88,24 @@ predicate setHttpOnlyInCookie(MethodAccess ma) { isBooleanTrue(mpa.getArgument(i)) ) ) - or - exists(MethodAccess mca | - ma.getMethod().calls(mca.getMethod()) and - setHttpOnlyInCookie(mca) - ) } -/** Holds if the method or a wrapper method removes a cookie. */ +/** + * A taint configuration tracking flow of a method or a wrapper method that sets + * the `HttpOnly` flag. + */ +class SetHttpOnlyInCookieConfiguration extends TaintTracking2::Configuration { + SetHttpOnlyInCookieConfiguration() { this = "SetHttpOnlyInCookieConfiguration" } + + override predicate isSource(DataFlow::Node source) { any() } + + override predicate isSink(DataFlow::Node sink) { + sink.asExpr() = + any(MethodAccess ma | ma.getMethod() instanceof ResponseAddCookieMethod).getArgument(0) + } +} + +/** Holds if the method call removes a cookie. */ predicate removeCookie(MethodAccess ma) { ma.getMethod().getName() = "setMaxAge" and ma.getArgument(0).(IntegerLiteral).getIntValue() = 0 @@ -125,15 +130,14 @@ class CookieResponseSink extends DataFlow::ExprNode { setHttpOnlyInCookie(ma2) or removeCookie(ma2) ) and - ( - DataFlow::localExprFlow(ma2.getQualifier(), this.getExpr()) or - DataFlow::localExprFlow(ma2, this.getExpr()) + exists(SetHttpOnlyInCookieConfiguration cc | + cc.hasFlow(DataFlow::exprNode(ma2.getQualifier()), this) ) ) or ma instanceof SetCookieMethodAccess and this.getExpr() = ma.getArgument(1) and - not hasHttpOnlyExpr(this.getExpr()) // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure") + not exists(MatchesHttpOnlyConfiguration cc | cc.hasFlowToExpr(ma.getArgument(1))) // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure") ) and not isTestMethod(ma) // Test class or method ) diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected index dae98e92e67..a9d126ca4a9 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.expected @@ -8,8 +8,8 @@ edges | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie | -| SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie | SensitiveCookieNotHttpOnly.java:102:25:102:64 | createAuthenticationCookie(...) : Cookie | -| SensitiveCookieNotHttpOnly.java:102:25:102:64 | createAuthenticationCookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:103:28:103:33 | cookie | +| SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie | SensitiveCookieNotHttpOnly.java:110:25:110:64 | createAuthenticationCookie(...) : Cookie | +| SensitiveCookieNotHttpOnly.java:110:25:110:64 | createAuthenticationCookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:111:28:111:33 | cookie | nodes | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | semmle.label | "jwt_token" : String | | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | semmle.label | jwtCookie | @@ -26,8 +26,8 @@ nodes | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | semmle.label | secString | | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | semmle.label | "Presto-UI-Token" : String | | SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie | semmle.label | cookie : Cookie | -| SensitiveCookieNotHttpOnly.java:102:25:102:64 | createAuthenticationCookie(...) : Cookie | semmle.label | createAuthenticationCookie(...) : Cookie | -| SensitiveCookieNotHttpOnly.java:103:28:103:33 | cookie | semmle.label | cookie | +| SensitiveCookieNotHttpOnly.java:110:25:110:64 | createAuthenticationCookie(...) : Cookie | semmle.label | createAuthenticationCookie(...) : Cookie | +| SensitiveCookieNotHttpOnly.java:111:28:111:33 | cookie | semmle.label | cookie | #select | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" | This sensitive cookie | | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" | This sensitive cookie | @@ -38,4 +38,4 @@ nodes | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" | This sensitive cookie | | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... | This sensitive cookie | | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... | This sensitive cookie | -| SensitiveCookieNotHttpOnly.java:103:28:103:33 | cookie | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | SensitiveCookieNotHttpOnly.java:103:28:103:33 | cookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" | This sensitive cookie | +| SensitiveCookieNotHttpOnly.java:111:28:111:33 | cookie | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | SensitiveCookieNotHttpOnly.java:111:28:111:33 | cookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" | This sensitive cookie | diff --git a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java index fdc831d7836..0bb912f6ce6 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java +++ b/java/ql/test/experimental/query-tests/security/CWE-1004/SensitiveCookieNotHttpOnly.java @@ -91,6 +91,14 @@ class SensitiveCookieNotHttpOnly { return cookie; } + public Cookie removeAuthenticationCookie(HttpServletRequest request, String jwt) { + String PRESTO_UI_COOKIE = "Presto-UI-Token"; + Cookie cookie = new Cookie(PRESTO_UI_COOKIE, jwt); + cookie.setPath("/ui"); + cookie.setMaxAge(0); + return cookie; + } + // GOOD - Tests set a sensitive cookie header with the `HttpOnly` flag set using a wrapper method. public void addCookie11(HttpServletRequest request, HttpServletResponse response, String jwt) { Cookie cookie = createHttpOnlyAuthenticationCookie(request, jwt); @@ -103,6 +111,12 @@ class SensitiveCookieNotHttpOnly { response.addCookie(cookie); } + // GOOD - Tests remove a sensitive cookie header without the `HttpOnly` flag set using a wrapper method. + public void addCookie13(HttpServletRequest request, HttpServletResponse response, String jwt) { + Cookie cookie = removeAuthenticationCookie(request, jwt); + response.addCookie(cookie); + } + private Cookie createCookie(String name, String value, Boolean httpOnly){ Cookie cookie = null; cookie = new Cookie(name, value); @@ -119,12 +133,12 @@ class SensitiveCookieNotHttpOnly { } // GOOD - Tests set a sensitive cookie header with the `HttpOnly` flag set through a boolean variable using a wrapper method. - public void addCookie13(HttpServletRequest request, HttpServletResponse response, String refreshToken) { + public void addCookie14(HttpServletRequest request, HttpServletResponse response, String refreshToken) { response.addCookie(createCookie("refresh_token", refreshToken, true)); } // BAD - Tests set a sensitive cookie header with the `HttpOnly` flag not set through a boolean variable using a wrapper method. - public void addCookie14(HttpServletRequest request, HttpServletResponse response, String refreshToken) { + public void addCookie15(HttpServletRequest request, HttpServletResponse response, String refreshToken) { response.addCookie(createCookie("refresh_token", refreshToken, false)); } From 57bd3f3c1459c04aa0098a254522bcc405f86d80 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Thu, 25 Mar 2021 10:44:26 +0000 Subject: [PATCH 0070/1429] Optimize the taint flow source --- .../CWE-1004/SensitiveCookieNotHttpOnly.ql | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql index db9adc2ee09..f1bc7879b8a 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql @@ -97,7 +97,10 @@ predicate setHttpOnlyInCookie(MethodAccess ma) { class SetHttpOnlyInCookieConfiguration extends TaintTracking2::Configuration { SetHttpOnlyInCookieConfiguration() { this = "SetHttpOnlyInCookieConfiguration" } - override predicate isSource(DataFlow::Node source) { any() } + override predicate isSource(DataFlow::Node source) { + source.asExpr() = + any(MethodAccess ma | setHttpOnlyInCookie(ma) or removeCookie(ma)).getQualifier() + } override predicate isSink(DataFlow::Node sink) { sink.asExpr() = @@ -123,21 +126,11 @@ class CookieResponseSink extends DataFlow::ExprNode { ( ma.getMethod() instanceof ResponseAddCookieMethod and this.getExpr() = ma.getArgument(0) and - not exists( - MethodAccess ma2 // a method or wrapper method that invokes cookie.setHttpOnly(true) - | - ( - setHttpOnlyInCookie(ma2) or - removeCookie(ma2) - ) and - exists(SetHttpOnlyInCookieConfiguration cc | - cc.hasFlow(DataFlow::exprNode(ma2.getQualifier()), this) - ) - ) + not exists(SetHttpOnlyInCookieConfiguration cc | cc.hasFlowTo(this)) or ma instanceof SetCookieMethodAccess and this.getExpr() = ma.getArgument(1) and - not exists(MatchesHttpOnlyConfiguration cc | cc.hasFlowToExpr(ma.getArgument(1))) // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure") + not exists(MatchesHttpOnlyConfiguration cc | cc.hasFlowTo(this)) // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure") ) and not isTestMethod(ma) // Test class or method ) From 2576c86ebf2e8a87a8cbe5165ae2ab95a7bec155 Mon Sep 17 00:00:00 2001 From: alexet Date: Thu, 25 Mar 2021 16:16:16 +0000 Subject: [PATCH 0071/1429] Docs: Update the language specification for changes to super. --- .../ql-language-reference/ql-language-specification.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/codeql/ql-language-reference/ql-language-specification.rst b/docs/codeql/ql-language-reference/ql-language-specification.rst index 0fe54210504..ad88b791019 100644 --- a/docs/codeql/ql-language-reference/ql-language-specification.rst +++ b/docs/codeql/ql-language-reference/ql-language-specification.rst @@ -1116,8 +1116,6 @@ A super expression may only occur in a QL program as the receiver expression for If a super expression includes a ``type``, then that type must be a class that the enclosing class inherits from. -If the super expression does not include a type, then the enclosing class must have a single declared base type, and that base type must be a class. - The value of a super expression is the same as the value of ``this`` in the named tuple. Casts @@ -1169,7 +1167,12 @@ A valid call with results *resolves* to a set of predicates. The ways a call can - If the call has no receiver and the predicate name is a selection identifier, then the qualifier is resolved as a module (see "`Module resolution <#module-resolution>`__"). The identifier is then resolved in the exported predicate environment of the qualifier module. -- If the call has a super expression as the receiver, then it resolves to a member predicate in a class the enclosing class inherits from. If the super expression is unqualified, then the super-class is the single class that the current class inherits from. If there is not exactly one such class, then the program is invalid. Otherwise the super-class is the class named by the qualifier of the super expression. The predicate is resolved by looking up its name and arity in the exported predicate environment of the super-class. +- If the call has a super expression as the receiver, then it resolves to a member predicate in a class that the enclosing class inherits from: + - If the super expression is unqualified and there is a single class that the current class inherits from then the super-class is that class. + - If the super expression is unqualified and there is are multiple classes that the current class inherits from then the super-class is the domain type. + - Otherwise the super-class is the class named by the qualifier of the super expression. + + The predicate is resolved by looking up its name and arity in the exported predicate environment of the super-class. - If the type of the receiver is the same as the enclosing class, the predicate is resolved by looking up its name and arity in the visible predicate environment of the class. From 2ca95166d916cc89bf2d7086aa664ef3757c95e4 Mon Sep 17 00:00:00 2001 From: Porcuiney Hairs Date: Wed, 13 Jan 2021 01:32:54 +0530 Subject: [PATCH 0072/1429] Java : add query to detect insecure loading of Dex File --- .../CWE/CWE-094/InsecureDexLoading.qhelp | 44 ++++++++ .../CWE/CWE-094/InsecureDexLoading.ql | 20 ++++ .../CWE/CWE-094/InsecureDexLoading.qll | 100 ++++++++++++++++++ .../CWE/CWE-094/InsecureDexLoadingBad.java | 32 ++++++ .../CWE/CWE-094/InsecureDexLoadingGood.java | 23 ++++ 5 files changed, 219 insertions(+) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.qll create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoadingBad.java create mode 100644 java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoadingGood.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.qhelp b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.qhelp new file mode 100644 index 00000000000..feda3af3fc2 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.qhelp @@ -0,0 +1,44 @@ + + + +

    +Shared world writable storage spaces are not secure to load Dex libraries from. A malicious actor can replace a dex file with a maliciously crafted file +which when loaded by the app can lead to code execution. +

    +
    + + +

    + Loading a file from private storage instead of a world writable one can prevent this issue. + As the attacker cannot access files stored by the app in its private storage. +

    +
    + + +

    + The following example loads a Dex file from a shared world writable location. in this case, + since the `/sdcard` directory is on external storage, any one can read/write to the location. + bypassing all Android security policies. Hence, this is insecure. +

    + + +

    + The next example loads a Dex file stored inside the app's private storage. + This is not exploitable as nobody else except the app can access the data stored here. +

    + +
    + + +
  • + Android Documentation: + Data and file storage overview + . +
  • +
  • + Android Documentation: + DexClassLoader + . +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.ql b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.ql new file mode 100644 index 00000000000..58d9844d38a --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.ql @@ -0,0 +1,20 @@ +/** + * @name Insecure loading of an Android Dex File + * @description Loading a DEX library located in a world-readable/ writable location such as + * a SD card can cause arbitary code execution vulnerabilities. + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/android-insecure-dex-loading + * @tags security + * external/cwe/cwe-094 + */ + +import java +import InsecureDexLoading +import DataFlow::PathGraph + +from DataFlow::PathNode source, DataFlow::PathNode sink, InsecureDexConfiguration conf +where conf.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "Potential arbitary code execution due to $@.", + source.getNode(), "a value loaded from a world readable/writable source." diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.qll b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.qll new file mode 100644 index 00000000000..2a4b387be7e --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoading.qll @@ -0,0 +1,100 @@ +import java +import semmle.code.java.dataflow.FlowSources + +/** + * A taint-tracking configuration fordetecting unsafe use of a + * `DexClassLoader` by an Android app. + */ +class InsecureDexConfiguration extends TaintTracking::Configuration { + InsecureDexConfiguration() { this = "Insecure Dex File Load" } + + override predicate isSource(DataFlow::Node source) { source instanceof InsecureDexSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof InsecureDexSink } + + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { + flowStep(pred, succ) + } +} + +/** A data flow source for insecure Dex class loading vulnerabilities. */ +abstract class InsecureDexSource extends DataFlow::Node { } + +/** A data flow sink for insecure Dex class loading vulnerabilities. */ +abstract class InsecureDexSink extends DataFlow::Node { } + +private predicate flowStep(DataFlow::Node pred, DataFlow::Node succ) { + // propagate from a `java.io.File` via the `File.getAbsolutePath` call. + exists(MethodAccess m | + m.getMethod().getDeclaringType() instanceof TypeFile and + m.getMethod().hasName("getAbsolutePath") and + m.getQualifier() = pred.asExpr() and + m = succ.asExpr() + ) + or + // propagate from a `java.io.File` via the `File.toString` call. + exists(MethodAccess m | + m.getMethod().getDeclaringType() instanceof TypeFile and + m.getMethod().hasName("toString") and + m.getQualifier() = pred.asExpr() and + m = succ.asExpr() + ) + or + // propagate to newly created `File` if the parent directory of the new `File` is tainted + exists(ConstructorCall cc | + cc.getConstructedType() instanceof TypeFile and + cc.getArgument(0) = pred.asExpr() and + cc = succ.asExpr() + ) +} + +/** + * An argument to a `DexClassLoader` call taken as a sink for + * insecure Dex class loading vulnerabilities. + */ +private class DexClassLoader extends InsecureDexSink { + DexClassLoader() { + exists(ConstructorCall cc | + cc.getConstructedType().hasQualifiedName("dalvik.system", "DexClassLoader") + | + this.asExpr() = cc.getArgument(0) + ) + } +} + +/** + * An `File` instance which reads from an SD card + * taken as a source for insecure Dex class loading vulnerabilities. + */ +private class ExternalFile extends InsecureDexSource { + ExternalFile() { + exists(ConstructorCall cc, Argument a | + cc.getConstructedType() instanceof TypeFile and + a = cc.getArgument(0) and + a.(CompileTimeConstantExpr).getStringValue().matches("%sdcard%") + | + this.asExpr() = a + ) + } +} + +/** + * A directory or file which may be stored in an world writable directory + * taken as a source for insecure Dex class loading vulnerabilities. + */ +private class ExternalStorageDirSource extends InsecureDexSource { + ExternalStorageDirSource() { + exists(Method m | + m.getDeclaringType().hasQualifiedName("android.os", "Environment") and + m.hasName("getExternalStorageDirectory") + or + m.getDeclaringType().hasQualifiedName("android.content", "Context") and + m.hasName([ + "getExternalFilesDir", "getExternalFilesDirs", "getExternalMediaDirs", + "getExternalCacheDir", "getExternalCacheDirs" + ]) + | + this.asExpr() = m.getAReference() + ) + } +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoadingBad.java b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoadingBad.java new file mode 100644 index 00000000000..d8fdd828f4f --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoadingBad.java @@ -0,0 +1,32 @@ + +import android.app.Application; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.os.Bundle; + +import dalvik.system.DexClassLoader; +import dalvik.system.DexFile; + +public class InsecureDexLoading extends Application { + @Override + public void onCreate() { + super.onCreate(); + updateChecker(); + } + + private void updateChecker() { + try { + File file = new File("/sdcard/updater.apk"); + if (file.exists() && file.isFile() && file.length() <= 1000) { + DexClassLoader cl = new DexClassLoader(file.getAbsolutePath(), getCacheDir().getAbsolutePath(), null, + getClassLoader()); + int version = (int) cl.loadClass("my.package.class").getDeclaredMethod("myMethod").invoke(null); + if (Build.VERSION.SDK_INT < version) { + Toast.makeText(this, "Securely loaded Dex!", Toast.LENGTH_LONG).show(); + } + } + } catch (Exception e) { + // ignore + } + } +} \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoadingGood.java b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoadingGood.java new file mode 100644 index 00000000000..e45e3938f7b --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-094/InsecureDexLoadingGood.java @@ -0,0 +1,23 @@ +public class SecureDexLoading extends Application { + @Override + public void onCreate() { + super.onCreate(); + updateChecker(); + } + + private void updateChecker() { + try { + File file = new File(getCacheDir() + "/updater.apk"); + if (file.exists() && file.isFile() && file.length() <= 1000) { + DexClassLoader cl = new DexClassLoader(file.getAbsolutePath(), getCacheDir().getAbsolutePath(), null, + getClassLoader()); + int version = (int) cl.loadClass("my.package.class").getDeclaredMethod("myMethod").invoke(null); + if (Build.VERSION.SDK_INT < version) { + Toast.makeText(this, "Securely loaded Dex!", Toast.LENGTH_LONG).show(); + } + } + } catch (Exception e) { + // ignore + } + } +} \ No newline at end of file From 62a0775cf69c8bda45aeb9b0cdf60f28dafd9c72 Mon Sep 17 00:00:00 2001 From: yoff Date: Thu, 25 Mar 2021 23:09:11 +0100 Subject: [PATCH 0073/1429] Update python/ql/src/Security/CWE-327/examples/secure_protocol.py Co-authored-by: Rasmus Wriedt Larsen --- python/ql/src/Security/CWE-327/examples/secure_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/Security/CWE-327/examples/secure_protocol.py b/python/ql/src/Security/CWE-327/examples/secure_protocol.py index 94b3557d017..e349bdae832 100644 --- a/python/ql/src/Security/CWE-327/examples/secure_protocol.py +++ b/python/ql/src/Security/CWE-327/examples/secure_protocol.py @@ -4,7 +4,7 @@ import ssl hostname = 'www.python.org' context = ssl.SSLContext(ssl.PROTOCOL_TLS) context.options |= ssl.OP_NO_TLSv1 -context.options |= ssl.OP_NO_TLSv1_1 # This added by me +context.options |= ssl.OP_NO_TLSv1_1 with socket.create_connection((hostname, 443)) as sock: with context.wrap_socket(sock, server_hostname=hostname) as ssock: From 2b257318f1c9e1d4a27f9022d94a08fc3e4cc3da Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Thu, 25 Mar 2021 23:22:24 +0100 Subject: [PATCH 0074/1429] Python: more precise comment --- python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py index 3ff1207b527..ab80ed47dac 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.py @@ -28,7 +28,7 @@ ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1_2) SSLContext(protocol=ssl.PROTOCOL_TLSv1_2) SSL.Context(SSL.TLSv1_2_METHOD) -# possibly secure versions specified +# insecure versions allowed by specified range SSLContext(protocol=ssl.PROTOCOL_SSLv23) SSLContext(protocol=ssl.PROTOCOL_TLS) SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) From 54dad57cf4a22a9d01a434d0633606fa8adf7c8f Mon Sep 17 00:00:00 2001 From: yoff Date: Fri, 26 Mar 2021 00:25:40 +0100 Subject: [PATCH 0075/1429] Update python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py Co-authored-by: Rasmus Wriedt Larsen --- python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py b/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py index e4205c49824..fa771411882 100644 --- a/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py +++ b/python/ql/test/query-tests/Security/CWE-327/pyOpenSSL_fluent.py @@ -23,6 +23,8 @@ def test_fluent_no_TLSv1(): def test_fluent_safe(): hostname = 'www.python.org' context = SSL.Context(SSL.SSLv23_METHOD) + context.set_options(SSL.OP_NO_SSLv2) + context.set_options(SSL.OP_NO_SSLv3) context.set_options(SSL.OP_NO_TLSv1) context.set_options(SSL.OP_NO_TLSv1_1) From 554404575d243ffdff19559e8cbbf4c300019d32 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 00:29:40 +0100 Subject: [PATCH 0076/1429] Python: fix typo and name. --- python/ql/src/Security/CWE-327/ReadMe.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/Security/CWE-327/ReadMe.md b/python/ql/src/Security/CWE-327/ReadMe.md index c5330a78fda..680ae56f493 100644 --- a/python/ql/src/Security/CWE-327/ReadMe.md +++ b/python/ql/src/Security/CWE-327/ReadMe.md @@ -12,7 +12,7 @@ This should be kept up to date; the world is moving fast and protocols are being - `ssl.wrap_socket` is creating insecure connections, use `SSLContext.wrap_socket` instead. [link](https://docs.python.org/3/library/ssl.html#ssl.wrap_socket) > Deprecated since version 3.7: Since Python 3.2 and 2.7.9, it is recommended to use the `SSLContext.wrap_socket()` instead of `wrap_socket()`. The top-level function is limited and creates an insecure client socket without server name indication or hostname matching. -- Default consteructors are fine, a sluent api is used to constrain possible protocols later. +- Default consteructors are fine, a fluent api is used to constrain possible protocols later. ## Current recomendation From 9488b8bb18fa89614480024700f51c3500f96725 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 00:31:56 +0100 Subject: [PATCH 0077/1429] Python: actually rename --- python/ql/src/Security/CWE-327/{ReadMe.md => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename python/ql/src/Security/CWE-327/{ReadMe.md => README.md} (100%) diff --git a/python/ql/src/Security/CWE-327/ReadMe.md b/python/ql/src/Security/CWE-327/README.md similarity index 100% rename from python/ql/src/Security/CWE-327/ReadMe.md rename to python/ql/src/Security/CWE-327/README.md From d33b04cd965de99180434c998f96a15f98c1f5fa Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 26 Mar 2021 02:33:40 +0000 Subject: [PATCH 0078/1429] Query to detect plaintext credentials in Java properties files --- .../CWE-555/CredentialsInPropertiesFile.qhelp | 45 ++++++++++ .../CWE-555/CredentialsInPropertiesFile.ql | 87 +++++++++++++++++++ .../CWE/CWE-555/configuration.properties | 26 ++++++ .../CredentialsInPropertiesFile.expected | 5 ++ .../CWE-555/CredentialsInPropertiesFile.qlref | 1 + .../security/CWE-555/configuration.properties | 37 ++++++++ .../security/CWE-555/messages.properties | 8 ++ 7 files changed, 209 insertions(+) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp create mode 100644 java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-555/configuration.properties create mode 100644 java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected create mode 100644 java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.qlref create mode 100644 java/ql/test/experimental/query-tests/security/CWE-555/configuration.properties create mode 100644 java/ql/test/experimental/query-tests/security/CWE-555/messages.properties diff --git a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp new file mode 100644 index 00000000000..afbd40685ba --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp @@ -0,0 +1,45 @@ + + + +

    + Credentials management issues occur when credentials are stored in plaintext in + an application’s properties file. Common credentials include but are not limited + to LDAP, mail, database, proxy account, and so on. Storing plaintext credentials + in a properties file allows anyone who can read the file access to the protected + resource. Good credentials management guidelines require that credentials never + be stored in plaintext. +

    +
    + + +

    + Credentials stored in properties files should be encrypted and recycled regularly. + In a Java EE deployment scenario, utilities provided by application servers like + keystore and password vault can be used to encrypt and manage credentials. +

    +
    + + +

    + In the first example, the credentials of a LDAP and datasource properties are stored + in cleartext in the properties file. +

    + +

    + In the second example, the credentials of a LDAP and datasource properties are stored + in the encrypted format. +

    + +
    + + +
  • + OWASP: + Password Plaintext Storage +
  • +
  • + Medium (Rajeev Shukla): + Encrypting database password in the application.properties file +
  • +
    +
    diff --git a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql new file mode 100644 index 00000000000..2ab074e56d8 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql @@ -0,0 +1,87 @@ +/** + * @name Cleartext Credentials in Properties File + * @description Finds cleartext credentials in Java properties files. + * @kind problem + * @id java/credentials-in-properties + * @tags security + * external/cwe/cwe-555 + * external/cwe/cwe-256 + * external/cwe/cwe-260 + */ + +import java +import semmle.code.configfiles.ConfigFiles + +private string suspicious() { + result = "%password%" or + result = "%passwd%" or + result = "%account%" or + result = "%accnt%" or + result = "%credential%" or + result = "%token%" or + result = "%secret%" or + result = "%access%key%" +} + +private string nonSuspicious() { + result = "%hashed%" or + result = "%encrypted%" or + result = "%crypt%" +} + +/** Holds if the value is not cleartext credentials. */ +bindingset[value] +predicate isNotCleartextCredentials(string value) { + value = "" // Empty string + or + value.length() < 7 // Typical credentials are no less than 6 characters + or + value.matches("% %") // Sentences containing spaces + or + value.regexpMatch(".*[^a-zA-Z\\d]{3,}.*") // Contain repeated non-alphanumeric characters such as a fake password pass**** or ???? + or + value.matches("@%") // Starts with the "@" sign + or + value.regexpMatch("\\$\\{.*\\}") // Variable placeholder ${credentials} + or + value.matches("%=") // A basic check of encrypted credentials ending with padding characters + or + value.matches("ENC(%)") // Encrypted value + or + value.toLowerCase().matches(suspicious()) // Could be message properties or fake passwords +} + +/** + * Holds if the credentials are in a non-production properties file indicated by: + * a) in a non-production directory + * b) with a non-production file name + */ +predicate isNonProdCredentials(CredentialsConfig cc) { + cc.getFile().getAbsolutePath().matches(["%dev%", "%test%", "%sample%"]) and + not cc.getFile().getAbsolutePath().matches("%codeql%") // CodeQL test cases +} + +/** The properties file with configuration key/value pairs. */ +class ConfigProperties extends ConfigPair { + ConfigProperties() { this.getFile().getBaseName().toLowerCase().matches("%.properties") } +} + +/** The credentials configuration property. */ +class CredentialsConfig extends ConfigProperties { + CredentialsConfig() { + this.getNameElement().getName().trim().toLowerCase().matches(suspicious()) and + not this.getNameElement().getName().trim().toLowerCase().matches(nonSuspicious()) + } + + string getName() { result = this.getNameElement().getName().trim() } + + string getValue() { result = this.getValueElement().getValue().trim() } +} + +from CredentialsConfig cc +where + not isNotCleartextCredentials(cc.getValue()) and + not isNonProdCredentials(cc) +select cc, + "Plaintext credentials " + cc.getName() + " have cleartext value " + cc.getValue() + + " in properties file." diff --git a/java/ql/src/experimental/Security/CWE/CWE-555/configuration.properties b/java/ql/src/experimental/Security/CWE/CWE-555/configuration.properties new file mode 100644 index 00000000000..55e8b0d86da --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-555/configuration.properties @@ -0,0 +1,26 @@ +#***************************** LDAP Credentials *****************************************# + +ldap.ldapHost = ldap.example.com +ldap.ldapPort = 636 +ldap.loginDN = cn=Directory Manager + +#### BAD: LDAP credentials are stored in cleartext #### +ldap.password = mysecpass + +#### GOOD: LDAP credentials are stored in the encrypted format #### +ldap.password = eFRZ3Cqo5zDJWMYLiaEupw== + +ldap.domain1 = example +ldap.domain2 = com +ldap.url= ldaps://ldap.example.com:636/dc=example,dc=com + +#*************************** MS SQL Database Connection **********************************# +datasource1.driverClassName = com.microsoft.sqlserver.jdbc.SQLServerDriver +datasource1.url = jdbc:sqlserver://ms.example.com\\exampledb:1433; +datasource1.username = sa + +#### BAD: Datasource credentials are stored in cleartext #### +datasource1.password = Passw0rd@123 + +#### GOOD: Datasource credentials are stored in the encrypted format #### +datasource1.password = VvOgflYS1EUzJdVNDoBcnA== diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected new file mode 100644 index 00000000000..0ce33913932 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected @@ -0,0 +1,5 @@ +| configuration.properties:6:1:6:25 | ldap.password=mysecpass | Plaintext credentials ldap.password have cleartext value mysecpass in properties file. | +| configuration.properties:18:1:18:35 | datasource1.password=Passw0rd@123 | Plaintext credentials datasource1.password have cleartext value Passw0rd@123 in properties file. | +| configuration.properties:25:1:25:31 | mail.password=MysecPWxWa@1993 | Plaintext credentials mail.password have cleartext value MysecPWxWa@1993 in properties file. | +| configuration.properties:33:1:33:50 | com.example.aws.s3.access_key=AKMAMQPBYMCD6YSAYCBA | Plaintext credentials com.example.aws.s3.access_key have cleartext value AKMAMQPBYMCD6YSAYCBA in properties file. | +| configuration.properties:34:1:34:70 | com.example.aws.s3.secret_key=8lMPSfWzZq+wcWtck5+QPLOJDZzE783pS09/IO3k | Plaintext credentials com.example.aws.s3.secret_key have cleartext value 8lMPSfWzZq+wcWtck5+QPLOJDZzE783pS09/IO3k in properties file. | diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.qlref b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.qlref new file mode 100644 index 00000000000..e2536bfe883 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/configuration.properties b/java/ql/test/experimental/query-tests/security/CWE-555/configuration.properties new file mode 100644 index 00000000000..a044161f097 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-555/configuration.properties @@ -0,0 +1,37 @@ +#***************************** LDAP Credentials *****************************************# +ldap.ldapHost = ldap.example.com +ldap.ldapPort = 636 +ldap.loginDN = cn=Directory Manager +#### BAD: LDAP credentials are stored in cleartext #### +ldap.password = mysecpass +#### GOOD: LDAP credentials are stored in the encrypted format #### +ldap.password = eFRZ3Cqo5zDJWMYLiaEupw== +ldap.domain1 = example +ldap.domain2 = com +ldap.url= ldaps://ldap.example.com:636/dc=example,dc=com + +#*************************** MS SQL Database Connection **********************************# +datasource1.driverClassName = com.microsoft.sqlserver.jdbc.SQLServerDriver +datasource1.url = jdbc:sqlserver://ms.example.com\\exampledb:1433; +datasource1.username = sa +#### BAD: Datasource credentials are stored in cleartext #### +datasource1.password = Passw0rd@123 +#### GOOD: Datasource credentials are stored in the encrypted format #### +datasource1.password = VvOgflYS1EUzJdVNDoBcnA== + +#*************************** Mail Connection **********************************# +mail.username = test@example.com +#### BAD: Mail credentials are stored in cleartext #### +mail.password = MysecPWxWa@1993 +#### GOOD: Mail credentials are stored in the encrypted format #### +mail.password = M*********@1993 + +#*************************** AWS S3 Connection **********************************# +com.example.aws.s3.bucket_name=com-bucket-1 +com.example.aws.s3.directory_name=com-directory-1 +#### BAD: Access keys are stored in properties file in cleartext #### +com.example.aws.s3.access_key=AKMAMQPBYMCD6YSAYCBA +com.example.aws.s3.secret_key=8lMPSfWzZq+wcWtck5+QPLOJDZzE783pS09/IO3k +#### GOOD: Access keys are not stored in properties file #### +com.example.aws.s3.access_key=${ENV:AWS_ACCESS_KEY_ID} +com.example.aws.s3.secret_key=${ENV:AWS_SECRET_ACCESS_KEY} diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/messages.properties b/java/ql/test/experimental/query-tests/security/CWE-555/messages.properties new file mode 100644 index 00000000000..fac63ec23e8 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-555/messages.properties @@ -0,0 +1,8 @@ +prompt.username=Username +prompt.password=Password + +forgot_password.error=Please enter a valid email address. +reset_password.error=Passwords must match and not be empty. + +login.password_expired=Your current password has expired. Please reset your password. +login.login_failure=Unable to verify username or password. Please try again. From 936757b4bf385addba174a37c24f029781110553 Mon Sep 17 00:00:00 2001 From: yoff Date: Fri, 26 Mar 2021 08:05:51 +0100 Subject: [PATCH 0079/1429] Update python/ql/src/Security/CWE-327/FluentApiModel.qll Co-authored-by: Rasmus Wriedt Larsen --- python/ql/src/Security/CWE-327/FluentApiModel.qll | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/python/ql/src/Security/CWE-327/FluentApiModel.qll b/python/ql/src/Security/CWE-327/FluentApiModel.qll index d222af70499..4d63a14bdbc 100644 --- a/python/ql/src/Security/CWE-327/FluentApiModel.qll +++ b/python/ql/src/Security/CWE-327/FluentApiModel.qll @@ -39,13 +39,7 @@ class InsecureContextConfiguration extends DataFlow::Configuration { ) } - override predicate isBarrierIn(DataFlow::Node node) { - exists(ProtocolUnrestriction r | - r = library.protocol_unrestriction() and - node = r.getContext() and - r.getUnrestriction() = tracked_version - ) - } + override predicate isBarrierIn(DataFlow::Node node) { this.isSource(node) } } /** From f1619f1ee8f6c5df2aa4d4b862deb46626bed2db Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 08:18:11 +0100 Subject: [PATCH 0080/1429] Python: "source" -> "contextOrigin" --- .../ql/src/Security/CWE-327/InsecureProtocol.ql | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index 194cc1f5ec1..b945f2e609b 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -18,11 +18,11 @@ string callName(AstNode call) { exists(Attribute a | a = call | result = callName(a.getObject()) + "." + a.getName()) } -string sourceName(DataFlow::Node source) { - result = "call to " + callName(source.asCfgNode().(CallNode).getFunction().getNode()) +string originName(DataFlow::Node contextOrigin) { + result = "call to " + callName(contextOrigin.asCfgNode().(CallNode).getFunction().getNode()) or - not source.asCfgNode() instanceof CallNode and - not source instanceof ContextCreation and + not contextOrigin.asCfgNode() instanceof CallNode and + not contextOrigin instanceof ContextCreation and result = "context modification" } @@ -32,11 +32,12 @@ string verb(boolean specific) { specific = false and result = "allowed" } -from DataFlow::Node creation, string insecure_version, DataFlow::Node source, boolean specific +from + DataFlow::Node creation, string insecure_version, DataFlow::Node contextOrigin, boolean specific where - unsafe_connection_creation(creation, insecure_version, source, specific) + unsafe_connection_creation(creation, insecure_version, contextOrigin, specific) or - unsafe_context_creation(creation, insecure_version, source.asCfgNode()) and specific = true + unsafe_context_creation(creation, insecure_version, contextOrigin.asCfgNode()) and specific = true select creation, "Insecure SSL/TLS protocol version " + insecure_version + " " + verb(specific) + " by $@ ", - source, sourceName(source) + contextOrigin, originName(contextOrigin) From e936540863385a7ff8437f890bc7bba24b00c547 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 08:22:09 +0100 Subject: [PATCH 0081/1429] Python: remove internal import --- python/ql/src/Security/CWE-327/Ssl.qll | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index be16138b961..88aaebf67ba 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -1,6 +1,5 @@ import python import semmle.python.ApiGraphs -import semmle.python.dataflow.new.internal.Attributes as Attributes import TlsLibraryModel class SSLContextCreation extends ContextCreation { @@ -104,7 +103,7 @@ class ContextSetVersion extends ProtocolRestriction, ProtocolUnrestriction { ProtocolVersion restriction; ContextSetVersion() { - exists(Attributes::AttrWrite aw | + exists(DataFlow::AttrWrite aw | aw.getObject().asCfgNode() = node and aw.getAttributeName() = "minimum_version" and aw.getValue() = From b21672c81cbc9a82f56e54093652b6a741dbe3e6 Mon Sep 17 00:00:00 2001 From: Alexander Eyers-Taylor Date: Fri, 26 Mar 2021 11:14:09 +0000 Subject: [PATCH 0082/1429] Apply suggestions from code review Co-authored-by: Shati Patel <42641846+shati-patel@users.noreply.github.com> Co-authored-by: Marcono1234 --- .../ql-language-reference/ql-language-specification.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/codeql/ql-language-reference/ql-language-specification.rst b/docs/codeql/ql-language-reference/ql-language-specification.rst index ad88b791019..fb89100b401 100644 --- a/docs/codeql/ql-language-reference/ql-language-specification.rst +++ b/docs/codeql/ql-language-reference/ql-language-specification.rst @@ -1168,9 +1168,9 @@ A valid call with results *resolves* to a set of predicates. The ways a call can - If the call has no receiver and the predicate name is a selection identifier, then the qualifier is resolved as a module (see "`Module resolution <#module-resolution>`__"). The identifier is then resolved in the exported predicate environment of the qualifier module. - If the call has a super expression as the receiver, then it resolves to a member predicate in a class that the enclosing class inherits from: - - If the super expression is unqualified and there is a single class that the current class inherits from then the super-class is that class. - - If the super expression is unqualified and there is are multiple classes that the current class inherits from then the super-class is the domain type. - - Otherwise the super-class is the class named by the qualifier of the super expression. + - If the super expression is unqualified and there is a single class that the current class inherits from, then the super-class is that class. + - If the super expression is unqualified and there are multiple classes that the current class inherits from, then the super-class is the domain type. + - Otherwise, the super-class is the class named by the qualifier of the super expression. The predicate is resolved by looking up its name and arity in the exported predicate environment of the super-class. From 1be2be843dd4a0afca316598e227d4a89aea5433 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 13:08:23 +0100 Subject: [PATCH 0083/1429] Python: update test expectations --- .../test/query-tests/Security/CWE-327/InsecureProtocol.expected | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected index e578a335d84..f4cd96eb82e 100644 --- a/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected +++ b/python/ql/test/query-tests/Security/CWE-327/InsecureProtocol.expected @@ -17,8 +17,6 @@ | pyOpenSSL_fluent.py:18:27:18:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv2 allowed by $@ | pyOpenSSL_fluent.py:15:15:15:44 | ControlFlowNode for Attribute() | call to SSL.Context | | pyOpenSSL_fluent.py:18:27:18:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | pyOpenSSL_fluent.py:15:15:15:44 | ControlFlowNode for Attribute() | call to SSL.Context | | pyOpenSSL_fluent.py:18:27:18:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | pyOpenSSL_fluent.py:15:15:15:44 | ControlFlowNode for Attribute() | call to SSL.Context | -| pyOpenSSL_fluent.py:29:27:29:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv2 allowed by $@ | pyOpenSSL_fluent.py:25:15:25:44 | ControlFlowNode for Attribute() | call to SSL.Context | -| pyOpenSSL_fluent.py:29:27:29:33 | ControlFlowNode for context | Insecure SSL/TLS protocol version SSLv3 allowed by $@ | pyOpenSSL_fluent.py:25:15:25:44 | ControlFlowNode for Attribute() | call to SSL.Context | | ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1 allowed by $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:9:14:9:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:6:15:6:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | | ssl_fluent.py:19:14:19:20 | ControlFlowNode for context | Insecure SSL/TLS protocol version TLSv1_1 allowed by $@ | ssl_fluent.py:15:15:15:46 | ControlFlowNode for Attribute() | call to ssl.SSLContext | From 2e948da3b4a5de3e0bc56d4ad0d19f4f787d5473 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 13:08:45 +0100 Subject: [PATCH 0084/1429] Python: suggested refactor --- python/ql/src/Security/CWE-327/FluentApiModel.qll | 5 ++--- python/ql/src/Security/CWE-327/InsecureProtocol.ql | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/python/ql/src/Security/CWE-327/FluentApiModel.qll b/python/ql/src/Security/CWE-327/FluentApiModel.qll index 4d63a14bdbc..543c32799bc 100644 --- a/python/ql/src/Security/CWE-327/FluentApiModel.qll +++ b/python/ql/src/Security/CWE-327/FluentApiModel.qll @@ -67,9 +67,8 @@ predicate unsafe_connection_creation( } /** A connection is created insecurely without reference to a context. */ -predicate unsafe_context_creation(DataFlow::Node node, string insecure_version, CallNode call) { +predicate unsafe_context_creation(DataFlow::CallCfgNode call, string insecure_version) { exists(TlsLibrary l, ContextCreation cc | cc = l.insecure_context_creation(insecure_version) | - cc = node and - cc.getNode() = call + cc = call ) } diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index b945f2e609b..a716b894880 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -37,7 +37,9 @@ from where unsafe_connection_creation(creation, insecure_version, contextOrigin, specific) or - unsafe_context_creation(creation, insecure_version, contextOrigin.asCfgNode()) and specific = true + unsafe_context_creation(creation, insecure_version) and + contextOrigin = creation and + specific = true select creation, "Insecure SSL/TLS protocol version " + insecure_version + " " + verb(specific) + " by $@ ", contextOrigin, originName(contextOrigin) From 7d7cbc49db35d0b05f12594984ac36ef529699a1 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 14:20:38 +0100 Subject: [PATCH 0085/1429] Fix comments. This induced fixing the code, since things were wired up wrongly. Currently the only implementation of `insecure_connection_creation` is `ssl.wrap_socket`, which is also the sole target of py/insecure-default-protocol`, so perhaps this part should be turned off? --- .../src/Security/CWE-327/FluentApiModel.qll | 39 +++++++++++++------ .../src/Security/CWE-327/InsecureProtocol.ql | 18 ++++++--- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/python/ql/src/Security/CWE-327/FluentApiModel.qll b/python/ql/src/Security/CWE-327/FluentApiModel.qll index 543c32799bc..169996081dc 100644 --- a/python/ql/src/Security/CWE-327/FluentApiModel.qll +++ b/python/ql/src/Security/CWE-327/FluentApiModel.qll @@ -43,16 +43,21 @@ class InsecureContextConfiguration extends DataFlow::Configuration { } /** - * A connection is created from a context allowing an insecure protocol, - * and that protocol has not been restricted appropriately. + * Holds if `conectionCreation` marks the creation of a connetion based on the contex + * found at `contextOrigin` and allowing `insecure_version`. + * `specific` is true iff the context if configured for a specific protocol version rather + * than for a family of protocols. */ -predicate unsafe_connection_creation( - DataFlow::Node creation, ProtocolVersion insecure_version, DataFlow::Node source, boolean specific +predicate unsafe_connection_creation_with_context( + DataFlow::Node connectionCreation, ProtocolVersion insecure_version, DataFlow::Node contextOrigin, + boolean specific ) { // Connection created from a context allowing `insecure_version`. - exists(InsecureContextConfiguration c, ProtocolUnrestriction cc | c.hasFlow(cc, creation) | + exists(InsecureContextConfiguration c, ProtocolUnrestriction co | + c.hasFlow(co, connectionCreation) + | insecure_version = c.getTrackedVersion() and - source = cc and + contextOrigin = co and specific = false ) or @@ -60,15 +65,27 @@ predicate unsafe_connection_creation( exists(TlsLibrary l, DataFlow::CfgNode cc | cc = l.insecure_connection_creation(insecure_version) | - creation = cc and - source = cc and + connectionCreation = cc and + contextOrigin = cc and specific = true ) } -/** A connection is created insecurely without reference to a context. */ -predicate unsafe_context_creation(DataFlow::CallCfgNode call, string insecure_version) { +/** + * Holds if `conectionCreation` marks the creation of a connetion witout reference to a context + * and allowing `insecure_version`. + * `specific` is true iff the context if configured for a specific protocol version rather + * than for a family of protocols. + */ +predicate unsafe_connection_creation_without_context( + DataFlow::CallCfgNode connectionCreation, string insecure_version +) { + exists(TlsLibrary l | connectionCreation = l.insecure_connection_creation(insecure_version)) +} + +/** Holds if `contextCreation` is creating a context ties to a specific insecure version. */ +predicate unsafe_context_creation(DataFlow::CallCfgNode contextCreation, string insecure_version) { exists(TlsLibrary l, ContextCreation cc | cc = l.insecure_context_creation(insecure_version) | - cc = call + contextCreation = cc ) } diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index a716b894880..974c36ddb0c 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -33,13 +33,19 @@ string verb(boolean specific) { } from - DataFlow::Node creation, string insecure_version, DataFlow::Node contextOrigin, boolean specific + DataFlow::Node connectionCreation, string insecure_version, DataFlow::Node protocolConfiguration, + boolean specific where - unsafe_connection_creation(creation, insecure_version, contextOrigin, specific) + unsafe_connection_creation_with_context(connectionCreation, insecure_version, + protocolConfiguration, specific) or - unsafe_context_creation(creation, insecure_version) and - contextOrigin = creation and + unsafe_connection_creation_without_context(connectionCreation, insecure_version) and + protocolConfiguration = connectionCreation and specific = true -select creation, + or + unsafe_context_creation(protocolConfiguration, insecure_version) and + connectionCreation = protocolConfiguration and + specific = true +select connectionCreation, "Insecure SSL/TLS protocol version " + insecure_version + " " + verb(specific) + " by $@ ", - contextOrigin, originName(contextOrigin) + protocolConfiguration, originName(protocolConfiguration) From 851317e34ffbf27c939d4575fa399c7227c85e6e Mon Sep 17 00:00:00 2001 From: Chris Smowton Date: Thu, 11 Mar 2021 11:42:32 +0000 Subject: [PATCH 0086/1429] Add models for StrBuilder's fluent methods --- .../semmle/code/java/frameworks/apache/Lang.qll | 9 +++++++++ .../apache-commons-lang3/StrBuilderTest.java | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/java/ql/src/semmle/code/java/frameworks/apache/Lang.qll b/java/ql/src/semmle/code/java/frameworks/apache/Lang.qll index deab8f4a692..1ddf8876c3d 100644 --- a/java/ql/src/semmle/code/java/frameworks/apache/Lang.qll +++ b/java/ql/src/semmle/code/java/frameworks/apache/Lang.qll @@ -427,6 +427,15 @@ private class ApacheStrBuilderModel extends SummaryModelCsv { } } +/** + * An Apache Commons-Lang StrBuilder method that returns `this`. + */ +private class ApacheStrBuilderFluentMethod extends FluentMethod { + ApacheStrBuilderFluentMethod() { + this.getReturnType().(RefType).hasQualifiedName("org.apache.commons.lang3.text", "StrBuilder") + } +} + /** * Taint-propagating models for `WordUtils`. */ diff --git a/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTest.java b/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTest.java index bc8887d1150..d460184e176 100644 --- a/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTest.java +++ b/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTest.java @@ -128,6 +128,20 @@ class StrBuilderTest { StrBuilder sb72 = new StrBuilder(); sb72.append(taint()); sink(sb72.toCharArray(0, 0)); // $hasTaintFlow StrBuilder sb73 = new StrBuilder(); sb73.append(taint()); sink(sb73.toStringBuffer()); // $hasTaintFlow StrBuilder sb74 = new StrBuilder(); sb74.append(taint()); sink(sb74.toStringBuilder()); // $hasTaintFlow + + // Tests for fluent methods (those returning `this`): + + StrBuilder fluentTest = new StrBuilder(); + sink(fluentTest.append("Harmless").append(taint()).append("Also harmless").toString()); // $hasTaintFlow + + StrBuilder fluentBackflowTest = new StrBuilder(); + fluentBackflowTest.append("Harmless").append(taint()).append("Also harmless"); + sink(fluentBackflowTest.toString()); // $hasTaintFlow + + // Test the case where the fluent method contributing taint is at the end of a statement: + StrBuilder fluentBackflowTest2 = new StrBuilder(); + fluentBackflowTest2.append("Harmless").append(taint()); + sink(fluentBackflowTest2.toString()); // $hasTaintFlow } } \ No newline at end of file From 3a274424abc0bdc7b6d84678aa3a9697c9f2ace0 Mon Sep 17 00:00:00 2001 From: Chris Smowton Date: Thu, 11 Mar 2021 13:45:55 +0000 Subject: [PATCH 0087/1429] Convert fluent method models to csv and generalise to the three different variants of StrBuilder. --- .../code/java/frameworks/apache/Lang.qll | 86 +++++++++++++++++++ .../apache-commons-lang3/StrBuilderTest.java | 62 +++++++++++++ .../StrBuilderTextTest.java | 76 ++++++++++++++++ .../TextStringBuilderTest.java | 76 ++++++++++++++++ 4 files changed, 300 insertions(+) diff --git a/java/ql/src/semmle/code/java/frameworks/apache/Lang.qll b/java/ql/src/semmle/code/java/frameworks/apache/Lang.qll index 1ddf8876c3d..2422efc83d9 100644 --- a/java/ql/src/semmle/code/java/frameworks/apache/Lang.qll +++ b/java/ql/src/semmle/code/java/frameworks/apache/Lang.qll @@ -427,6 +427,92 @@ private class ApacheStrBuilderModel extends SummaryModelCsv { } } +private class ApacheStrBuilderFluentMethodsModel extends SummaryModelCsv { + override predicate row(string row) { + row = + [ + "org.apache.commons.lang3.text;StrBuilder;false;append;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;appendAll;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;appendFixedWidthPadLeft;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;appendFixedWidthPadRight;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;appendln;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;appendNewLine;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;appendNull;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;appendPadding;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;appendSeparator;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;appendWithSeparators;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;delete;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;deleteAll;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;deleteCharAt;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;deleteFirst;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;ensureCapacity;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;insert;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;minimizeCapacity;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;replace;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;replaceAll;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;replaceFirst;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;reverse;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;setCharAt;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;setLength;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;setNewLineText;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;setNullText;;;Argument[-1];ReturnValue;value", + "org.apache.commons.lang3.text;StrBuilder;false;trim;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;append;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;appendAll;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;appendFixedWidthPadLeft;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;appendFixedWidthPadRight;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;appendln;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;appendNewLine;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;appendNull;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;appendPadding;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;appendSeparator;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;appendWithSeparators;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;delete;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;deleteAll;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;deleteCharAt;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;deleteFirst;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;ensureCapacity;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;insert;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;minimizeCapacity;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;replace;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;replaceAll;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;replaceFirst;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;reverse;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;setCharAt;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;setLength;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;setNewLineText;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;setNullText;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;StrBuilder;false;trim;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;append;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;appendAll;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;appendFixedWidthPadLeft;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;appendFixedWidthPadRight;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;appendln;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;appendNewLine;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;appendNull;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;appendPadding;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;appendSeparator;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;appendWithSeparators;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;delete;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;deleteAll;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;deleteCharAt;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;deleteFirst;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;ensureCapacity;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;insert;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;minimizeCapacity;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;replace;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;replaceAll;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;replaceFirst;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;reverse;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;setCharAt;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;setLength;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;setNewLineText;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;setNullText;;;Argument[-1];ReturnValue;value", + "org.apache.commons.text;TextStringBuilder;false;trim;;;Argument[-1];ReturnValue;value" + ] + } +} + /** * An Apache Commons-Lang StrBuilder method that returns `this`. */ diff --git a/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTest.java b/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTest.java index d460184e176..0c0e386e9c2 100644 --- a/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTest.java +++ b/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTest.java @@ -142,6 +142,68 @@ class StrBuilderTest { StrBuilder fluentBackflowTest2 = new StrBuilder(); fluentBackflowTest2.append("Harmless").append(taint()); sink(fluentBackflowTest2.toString()); // $hasTaintFlow + + // Test all fluent methods are passing taint through to their result: + StrBuilder fluentAllMethodsTest = new StrBuilder(taint()); + sink(fluentAllMethodsTest // $hasTaintFlow + .append("text") + .appendAll("text") + .appendFixedWidthPadLeft("text", 4, ' ') + .appendFixedWidthPadRight("text", 4, ' ') + .appendln("text") + .appendNewLine() + .appendNull() + .appendPadding(0, ' ') + .appendSeparator(',') + .appendWithSeparators(new String[] { }, ",") + .delete(0, 0) + .deleteAll(' ') + .deleteCharAt(0) + .deleteFirst("delme") + .ensureCapacity(100) + .insert(1, "insertme") + .minimizeCapacity() + .replace(0, 0, "replacement") + .replaceAll("find", "replace") + .replaceFirst("find", "replace") + .reverse() + .setCharAt(0, 'a') + .setLength(500) + .setNewLineText("newline") + .setNullText("NULL") + .trim()); + + // Test all fluent methods are passing taint back to their qualifier: + StrBuilder fluentAllMethodsTest2 = new StrBuilder(); + fluentAllMethodsTest2 + .append("text") + .appendAll("text") + .appendFixedWidthPadLeft("text", 4, ' ') + .appendFixedWidthPadRight("text", 4, ' ') + .appendln("text") + .appendNewLine() + .appendNull() + .appendPadding(0, ' ') + .appendSeparator(',') + .appendWithSeparators(new String[] { }, ",") + .delete(0, 0) + .deleteAll(' ') + .deleteCharAt(0) + .deleteFirst("delme") + .ensureCapacity(100) + .insert(1, "insertme") + .minimizeCapacity() + .replace(0, 0, "replacement") + .replaceAll("find", "replace") + .replaceFirst("find", "replace") + .reverse() + .setCharAt(0, 'a') + .setLength(500) + .setNewLineText("newline") + .setNullText("NULL") + .trim() + .append(taint()); + sink(fluentAllMethodsTest2); // $hasTaintFlow } } \ No newline at end of file diff --git a/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTextTest.java b/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTextTest.java index 796900e8a3b..74f0f1d17c9 100644 --- a/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTextTest.java +++ b/java/ql/test/library-tests/frameworks/apache-commons-lang3/StrBuilderTextTest.java @@ -128,6 +128,82 @@ class StrBuilderTextTest { StrBuilder sb72 = new StrBuilder(); sb72.append(taint()); sink(sb72.toCharArray(0, 0)); // $hasTaintFlow StrBuilder sb73 = new StrBuilder(); sb73.append(taint()); sink(sb73.toStringBuffer()); // $hasTaintFlow StrBuilder sb74 = new StrBuilder(); sb74.append(taint()); sink(sb74.toStringBuilder()); // $hasTaintFlow + + // Tests for fluent methods (those returning `this`): + + StrBuilder fluentTest = new StrBuilder(); + sink(fluentTest.append("Harmless").append(taint()).append("Also harmless").toString()); // $hasTaintFlow + + StrBuilder fluentBackflowTest = new StrBuilder(); + fluentBackflowTest.append("Harmless").append(taint()).append("Also harmless"); + sink(fluentBackflowTest.toString()); // $hasTaintFlow + + // Test the case where the fluent method contributing taint is at the end of a statement: + StrBuilder fluentBackflowTest2 = new StrBuilder(); + fluentBackflowTest2.append("Harmless").append(taint()); + sink(fluentBackflowTest2.toString()); // $hasTaintFlow + + // Test all fluent methods are passing taint through to their result: + StrBuilder fluentAllMethodsTest = new StrBuilder(taint()); + sink(fluentAllMethodsTest // $hasTaintFlow + .append("text") + .appendAll("text") + .appendFixedWidthPadLeft("text", 4, ' ') + .appendFixedWidthPadRight("text", 4, ' ') + .appendln("text") + .appendNewLine() + .appendNull() + .appendPadding(0, ' ') + .appendSeparator(',') + .appendWithSeparators(new String[] { }, ",") + .delete(0, 0) + .deleteAll(' ') + .deleteCharAt(0) + .deleteFirst("delme") + .ensureCapacity(100) + .insert(1, "insertme") + .minimizeCapacity() + .replace(0, 0, "replacement") + .replaceAll("find", "replace") + .replaceFirst("find", "replace") + .reverse() + .setCharAt(0, 'a') + .setLength(500) + .setNewLineText("newline") + .setNullText("NULL") + .trim()); + + // Test all fluent methods are passing taint back to their qualifier: + StrBuilder fluentAllMethodsTest2 = new StrBuilder(); + fluentAllMethodsTest2 + .append("text") + .appendAll("text") + .appendFixedWidthPadLeft("text", 4, ' ') + .appendFixedWidthPadRight("text", 4, ' ') + .appendln("text") + .appendNewLine() + .appendNull() + .appendPadding(0, ' ') + .appendSeparator(',') + .appendWithSeparators(new String[] { }, ",") + .delete(0, 0) + .deleteAll(' ') + .deleteCharAt(0) + .deleteFirst("delme") + .ensureCapacity(100) + .insert(1, "insertme") + .minimizeCapacity() + .replace(0, 0, "replacement") + .replaceAll("find", "replace") + .replaceFirst("find", "replace") + .reverse() + .setCharAt(0, 'a') + .setLength(500) + .setNewLineText("newline") + .setNullText("NULL") + .trim() + .append(taint()); + sink(fluentAllMethodsTest2); // $hasTaintFlow } } \ No newline at end of file diff --git a/java/ql/test/library-tests/frameworks/apache-commons-lang3/TextStringBuilderTest.java b/java/ql/test/library-tests/frameworks/apache-commons-lang3/TextStringBuilderTest.java index 69db28cb3e9..e490c11c7cb 100644 --- a/java/ql/test/library-tests/frameworks/apache-commons-lang3/TextStringBuilderTest.java +++ b/java/ql/test/library-tests/frameworks/apache-commons-lang3/TextStringBuilderTest.java @@ -129,6 +129,82 @@ class TextStringBuilderTest { TextStringBuilder sb72 = new TextStringBuilder(); sb72.append(taint()); sink(sb72.toCharArray(0, 0)); // $hasTaintFlow TextStringBuilder sb73 = new TextStringBuilder(); sb73.append(taint()); sink(sb73.toStringBuffer()); // $hasTaintFlow TextStringBuilder sb74 = new TextStringBuilder(); sb74.append(taint()); sink(sb74.toStringBuilder()); // $hasTaintFlow + + // Tests for fluent methods (those returning `this`): + + TextStringBuilder fluentTest = new TextStringBuilder(); + sink(fluentTest.append("Harmless").append(taint()).append("Also harmless").toString()); // $hasTaintFlow + + TextStringBuilder fluentBackflowTest = new TextStringBuilder(); + fluentBackflowTest.append("Harmless").append(taint()).append("Also harmless"); + sink(fluentBackflowTest.toString()); // $hasTaintFlow + + // Test the case where the fluent method contributing taint is at the end of a statement: + TextStringBuilder fluentBackflowTest2 = new TextStringBuilder(); + fluentBackflowTest2.append("Harmless").append(taint()); + sink(fluentBackflowTest2.toString()); // $hasTaintFlow + + // Test all fluent methods are passing taint through to their result: + TextStringBuilder fluentAllMethodsTest = new TextStringBuilder(taint()); + sink(fluentAllMethodsTest // $hasTaintFlow + .append("text") + .appendAll("text") + .appendFixedWidthPadLeft("text", 4, ' ') + .appendFixedWidthPadRight("text", 4, ' ') + .appendln("text") + .appendNewLine() + .appendNull() + .appendPadding(0, ' ') + .appendSeparator(',') + .appendWithSeparators(new String[] { }, ",") + .delete(0, 0) + .deleteAll(' ') + .deleteCharAt(0) + .deleteFirst("delme") + .ensureCapacity(100) + .insert(1, "insertme") + .minimizeCapacity() + .replace(0, 0, "replacement") + .replaceAll("find", "replace") + .replaceFirst("find", "replace") + .reverse() + .setCharAt(0, 'a') + .setLength(500) + .setNewLineText("newline") + .setNullText("NULL") + .trim()); + + // Test all fluent methods are passing taint back to their qualifier: + TextStringBuilder fluentAllMethodsTest2 = new TextStringBuilder(); + fluentAllMethodsTest2 + .append("text") + .appendAll("text") + .appendFixedWidthPadLeft("text", 4, ' ') + .appendFixedWidthPadRight("text", 4, ' ') + .appendln("text") + .appendNewLine() + .appendNull() + .appendPadding(0, ' ') + .appendSeparator(',') + .appendWithSeparators(new String[] { }, ",") + .delete(0, 0) + .deleteAll(' ') + .deleteCharAt(0) + .deleteFirst("delme") + .ensureCapacity(100) + .insert(1, "insertme") + .minimizeCapacity() + .replace(0, 0, "replacement") + .replaceAll("find", "replace") + .replaceFirst("find", "replace") + .reverse() + .setCharAt(0, 'a') + .setLength(500) + .setNewLineText("newline") + .setNullText("NULL") + .trim() + .append(taint()); + sink(fluentAllMethodsTest2); // $hasTaintFlow } } \ No newline at end of file From 42b63a61ae82b19a7e61c86bebfe27365d35bf79 Mon Sep 17 00:00:00 2001 From: Chris Smowton Date: Thu, 11 Mar 2021 18:34:11 +0000 Subject: [PATCH 0088/1429] Add change note --- java/change-notes/2021-03-11-commons-strbuilder.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 java/change-notes/2021-03-11-commons-strbuilder.md diff --git a/java/change-notes/2021-03-11-commons-strbuilder.md b/java/change-notes/2021-03-11-commons-strbuilder.md new file mode 100644 index 00000000000..ce8f647ab0f --- /dev/null +++ b/java/change-notes/2021-03-11-commons-strbuilder.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* Added support for the Apache Commons Lang and Commons Text StrBuilder class, and its successor TextStringBuilder. From 8155334fa7b4b72015e1b75bf2563b0b96c699bb Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 15:57:07 +0100 Subject: [PATCH 0089/1429] Python: More elaborate qldoc also refactor code to match --- .../src/Security/CWE-327/FluentApiModel.qll | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/python/ql/src/Security/CWE-327/FluentApiModel.qll b/python/ql/src/Security/CWE-327/FluentApiModel.qll index 169996081dc..d4eb13a133d 100644 --- a/python/ql/src/Security/CWE-327/FluentApiModel.qll +++ b/python/ql/src/Security/CWE-327/FluentApiModel.qll @@ -2,9 +2,21 @@ import python import TlsLibraryModel /** - * Configuration to track flow from the creation of a context to - * that context being used to create a connection. - * Flow is broken if the insecure protocol of interest is being restricted. + * Configuration to determine the state of a context being used to create + * a conection. + * + * The state is in terms of whether a specific protocol is allowed. This is + * either true or false when the context is created and can then be modified + * later by either restricting or unrestricting the protocol (see the predicates + * `isRestriction` and `isUnrestriction`). + * + * Since we are interested in the final state, we want the flow to start from + * the last unrestriction, so we disallow flow into unrestrictions. We also + * model the creation as an unrestriction of everything it allows, to account + * for the common case where the creation plays the role of "last unrestriction". + * + * Since we really want "the last unrestriction, not nullified by a restriction", + * we also disallow flow into restrictions. */ class InsecureContextConfiguration extends DataFlow::Configuration { TlsLibrary library; @@ -17,29 +29,35 @@ class InsecureContextConfiguration extends DataFlow::Configuration { ProtocolVersion getTrackedVersion() { result = tracked_version } - override predicate isSource(DataFlow::Node source) { - // source = library.unspecific_context_creation() - exists(ProtocolUnrestriction pu | - pu = library.protocol_unrestriction() and - pu.getUnrestriction() = tracked_version - | - source = pu.getContext() - ) - } + override predicate isSource(DataFlow::Node source) { this.isUnrestriction(source) } override predicate isSink(DataFlow::Node sink) { sink = library.connection_creation().getContext() } - override predicate isBarrierOut(DataFlow::Node node) { + override predicate isBarrierIn(DataFlow::Node node) { + this.isRestriction(node) + or + this.isUnrestriction(node) + } + + private predicate isRestriction(DataFlow::Node node) { exists(ProtocolRestriction r | r = library.protocol_restriction() and - node = r.getContext() and r.getRestriction() = tracked_version + | + node = r.getContext() ) } - override predicate isBarrierIn(DataFlow::Node node) { this.isSource(node) } + private predicate isUnrestriction(DataFlow::Node node) { + exists(ProtocolUnrestriction pu | + pu = library.protocol_unrestriction() and + pu.getUnrestriction() = tracked_version + | + node = pu.getContext() + ) + } } /** From 98dfe1a00a14e6dfc8c27d84cfeb2ee363515f56 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 17:27:43 +0100 Subject: [PATCH 0090/1429] Python: Elaborate qldoc and renames to match --- python/ql/src/Security/CWE-327/Ssl.qll | 34 ++++++++++++++++++-------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index 88aaebf67ba..110b4bf185a 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -45,7 +45,7 @@ class OptionsAugOr extends ProtocolRestriction { ( aa.getValue() = flag or - impliesValue(aa.getValue(), flag, false, false) + impliesBitSet(aa.getValue(), flag, false, false) ) ) } @@ -70,7 +70,7 @@ class OptionsAugAndNot extends ProtocolUnrestriction { ( aa.getValue() = notFlag or - impliesValue(aa.getValue(), notFlag, true, true) + impliesBitSet(aa.getValue(), notFlag, true, true) ) ) } @@ -80,22 +80,36 @@ class OptionsAugAndNot extends ProtocolUnrestriction { override ProtocolVersion getUnrestriction() { result = restriction } } -/** Whether `part` evaluates to `partIsTrue` if `whole` evaluates to `wholeIsTrue`. */ -predicate impliesValue(BinaryExpr whole, Expr part, boolean partIsTrue, boolean wholeIsTrue) { +/** + * Holds if + * for every bit, _b_: + * `wholeHasBitSet` represents that _b_ is set in `whole` + * implies + * `partHasBitSet` represents that _b_ is set in `part` + * + * As an example take `whole` = `part1 & part2`. Then + * `impliesBitSet(whole, part1, true, true)` holds + * because for any bit in `whole`, if that bit is set it must also be set in `part1`. + * + * Similarly for `whole` = `part1 | part2`. Here + * `impliesBitSet(whole, part1, false, false)` holds + * because for any bit in `whole`, if that bit is not set, it cannot be set in `part1`. + */ +predicate impliesBitSet(BinaryExpr whole, Expr part, boolean partHasBitSet, boolean wholeHasBitSet) { whole.getOp() instanceof BitAnd and ( - wholeIsTrue = true and partIsTrue = true and part in [whole.getLeft(), whole.getRight()] + wholeHasBitSet = true and partHasBitSet = true and part in [whole.getLeft(), whole.getRight()] or - wholeIsTrue = true and - impliesValue([whole.getLeft(), whole.getRight()], part, partIsTrue, wholeIsTrue) + wholeHasBitSet = true and + impliesBitSet([whole.getLeft(), whole.getRight()], part, partHasBitSet, wholeHasBitSet) ) or whole.getOp() instanceof BitOr and ( - wholeIsTrue = false and partIsTrue = false and part in [whole.getLeft(), whole.getRight()] + wholeHasBitSet = false and partHasBitSet = false and part in [whole.getLeft(), whole.getRight()] or - wholeIsTrue = false and - impliesValue([whole.getLeft(), whole.getRight()], part, partIsTrue, wholeIsTrue) + wholeHasBitSet = false and + impliesBitSet([whole.getLeft(), whole.getRight()], part, partHasBitSet, wholeHasBitSet) ) } From 470b4d86582595101439a043ab4cd4db935b11a2 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 17:35:36 +0100 Subject: [PATCH 0091/1429] Python: Add missing qldoc --- python/ql/src/Security/CWE-327/TlsLibraryModel.qll | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll index 3ab880e8bd9..f66d663a7e2 100644 --- a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll +++ b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll @@ -58,6 +58,10 @@ abstract class ProtocolUnrestriction extends DataFlow::CfgNode { abstract ProtocolVersion getUnrestriction(); } +/** + * A context is being created with a range of allowed protocols. + * This also serves as unrestricting these protocols. + */ abstract class UnspecificContextCreation extends ContextCreation, ProtocolUnrestriction { TlsLibrary library; ProtocolFamily family; @@ -77,6 +81,7 @@ abstract class UnspecificContextCreation extends ContextCreation, ProtocolUnrest } } +/** A model of a TLS library. */ abstract class TlsLibrary extends string { TlsLibrary() { this in ["ssl", "pyOpenSSL"] } From 44d62df3f72719d8ba57a2519877db2a179f4ec4 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 17:51:18 +0100 Subject: [PATCH 0092/1429] Python: Fix model of `TLS` and add reference --- python/ql/src/Security/CWE-327/TlsLibraryModel.qll | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll index f66d663a7e2..245a60b0295 100644 --- a/python/ql/src/Security/CWE-327/TlsLibraryModel.qll +++ b/python/ql/src/Security/CWE-327/TlsLibraryModel.qll @@ -71,11 +71,14 @@ abstract class UnspecificContextCreation extends ContextCreation, ProtocolUnrest override DataFlow::CfgNode getContext() { result = this } override ProtocolVersion getUnrestriction() { + // see https://www.openssl.org/docs/man1.1.0/man3/TLS_method.html family = "TLS" and - result in ["TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] + result in ["SSLv3", "TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] or // This can negotiate a TLS 1.3 connection (!) - // see https://docs.python.org/3/library/ssl.html#ssl-contexts + // see + // - https://docs.python.org/3/library/ssl.html#ssl-contexts + // - https://www.openssl.org/docs/man1.0.2/man3/TLSv1_method.html family = "SSLv23" and result in ["SSLv2", "SSLv3", "TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] } From a72b1340eb6bb3bdbea9cabd6bdd942a9365b848 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 26 Mar 2021 16:51:43 +0000 Subject: [PATCH 0093/1429] Add a comment on how to run the query --- .../Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql index 3acd22e767a..772ac6cd209 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-016/InsecureSpringActuatorConfig.ql @@ -8,6 +8,14 @@ * external/cwe-016 */ +/* + * Note this query requires properties files to be indexed before it can produce results. + * If creating your own database with the CodeQL CLI, you should run + * `codeql database index-files --language=properties ...` + * If using lgtm.com, you should add `properties_files: true` to the index block of your + * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction) + */ + import java import semmle.code.configfiles.ConfigFiles import semmle.code.xml.MavenPom From e0352fe7638223a1d998bdd26752e358d2ec8e92 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 23:26:24 +0100 Subject: [PATCH 0094/1429] Python: remove deprecated section of qhelp file --- python/ql/src/Security/CWE-327/InsecureProtocol.qhelp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp b/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp index cfcebd0930d..9ecc7da0d60 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.qhelp @@ -32,12 +32,6 @@ All cases should be updated to use a secure protocol, such as PROTOCOL_TLSv1_2.

    -

    - Note that ssl.wrap_socket has been deprecated in - Python 3.7. A preferred alternative is to use - ssl.SSLContext, which is supported in Python 2.7.9 and - 3.2 and later versions. -

    Note that ssl.wrap_socket has been deprecated in Python 3.7. The recommended alternatives are: From bf81122fc6b26584e85d4f0b7b4f854a2296471b Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Fri, 26 Mar 2021 23:37:19 +0100 Subject: [PATCH 0095/1429] Python: fix typo and add linebreaks --- python/ql/src/Security/CWE-327/FluentApiModel.qll | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/ql/src/Security/CWE-327/FluentApiModel.qll b/python/ql/src/Security/CWE-327/FluentApiModel.qll index d4eb13a133d..803b7adab40 100644 --- a/python/ql/src/Security/CWE-327/FluentApiModel.qll +++ b/python/ql/src/Security/CWE-327/FluentApiModel.qll @@ -63,7 +63,8 @@ class InsecureContextConfiguration extends DataFlow::Configuration { /** * Holds if `conectionCreation` marks the creation of a connetion based on the contex * found at `contextOrigin` and allowing `insecure_version`. - * `specific` is true iff the context if configured for a specific protocol version rather + * + * `specific` is true iff the context is configured for a specific protocol version rather * than for a family of protocols. */ predicate unsafe_connection_creation_with_context( @@ -92,7 +93,8 @@ predicate unsafe_connection_creation_with_context( /** * Holds if `conectionCreation` marks the creation of a connetion witout reference to a context * and allowing `insecure_version`. - * `specific` is true iff the context if configured for a specific protocol version rather + * + * `specific` is true iff the context is configured for a specific protocol version rather * than for a family of protocols. */ predicate unsafe_connection_creation_without_context( From bd863884479d4373e17717208441bbe6423d831b Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Sat, 27 Mar 2021 01:07:15 +0100 Subject: [PATCH 0096/1429] Python: Add typetracker to constrain attribute. --- python/ql/src/Security/CWE-327/Ssl.qll | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index 110b4bf185a..7f141523390 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -22,10 +22,28 @@ class SSLDefaultContextCreation extends ContextCreation { override DataFlow::CfgNode getProtocol() { none() } } +/** Gets a reference to an `ssl.Context` instance. */ +private DataFlow::LocalSourceNode sslContextInstance(DataFlow::TypeTracker t) { + t.start() and + result = API::moduleImport("ssl").getMember(["SSLContext", "create_default_context"]).getACall() + or + exists(DataFlow::TypeTracker t2 | result = sslContextInstance(t2).track(t2, t)) +} + +/** Gets a reference to an `ssl.Context` instance. */ +DataFlow::Node sslContextInstance() { + sslContextInstance(DataFlow::TypeTracker::end()).flowsTo(result) +} + class WrapSocketCall extends ConnectionCreation { override CallNode node; - WrapSocketCall() { node.getFunction().(AttrNode).getName() = "wrap_socket" } + WrapSocketCall() { + exists(DataFlow::AttrRead call | node.getFunction() = call.asCfgNode() | + call.getAttributeName() = "wrap_socket" and + call.getObject() = sslContextInstance() + ) + } override DataFlow::CfgNode getContext() { result.getNode() = node.getFunction().(AttrNode).getObject() From a53cbc1631c2cfc994f74d4c5ce9e39f6c38abbc Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Sat, 27 Mar 2021 00:11:01 +0000 Subject: [PATCH 0097/1429] Update qldoc and make the query more readable --- .../CWE-555/CredentialsInPropertiesFile.qhelp | 4 +- .../CWE-555/CredentialsInPropertiesFile.ql | 22 +++-- .../CWE-555/CredentialsInPropertiesFile.ql | 84 +++++++++++++++++++ .../CWE-555/CredentialsInPropertiesFile.qlref | 1 - 4 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.qlref diff --git a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp index afbd40685ba..0869b886260 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp @@ -15,7 +15,7 @@

    Credentials stored in properties files should be encrypted and recycled regularly. In a Java EE deployment scenario, utilities provided by application servers like - keystore and password vault can be used to encrypt and manage credentials. + keystores and password vaults can be used to encrypt and manage credentials.

    @@ -27,7 +27,7 @@

    In the second example, the credentials of a LDAP and datasource properties are stored - in the encrypted format. + in an encrypted format.

    diff --git a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql index 2ab074e56d8..1e9b9906096 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql @@ -9,10 +9,18 @@ * external/cwe/cwe-260 */ +/* + * Note this query requires properties files to be indexed before it can produce results. + * If creating your own database with the CodeQL CLI, you should run + * `codeql database index-files --language=properties ...` + * If using lgtm.com, you should add `properties_files: true` to the index block of your + * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction) + */ + import java import semmle.code.configfiles.ConfigFiles -private string suspicious() { +private string possibleSecretName() { result = "%password%" or result = "%passwd%" or result = "%account%" or @@ -23,7 +31,7 @@ private string suspicious() { result = "%access%key%" } -private string nonSuspicious() { +private string possibleEncryptedSecretName() { result = "%hashed%" or result = "%encrypted%" or result = "%crypt%" @@ -48,7 +56,8 @@ predicate isNotCleartextCredentials(string value) { or value.matches("ENC(%)") // Encrypted value or - value.toLowerCase().matches(suspicious()) // Could be message properties or fake passwords + // Could be a message property for UI display or fake passwords, e.g. login.password_expired=Your current password has expired. + value.toLowerCase().matches(possibleSecretName()) } /** @@ -57,8 +66,7 @@ predicate isNotCleartextCredentials(string value) { * b) with a non-production file name */ predicate isNonProdCredentials(CredentialsConfig cc) { - cc.getFile().getAbsolutePath().matches(["%dev%", "%test%", "%sample%"]) and - not cc.getFile().getAbsolutePath().matches("%codeql%") // CodeQL test cases + cc.getFile().getAbsolutePath().matches(["%dev%", "%test%", "%sample%"]) } /** The properties file with configuration key/value pairs. */ @@ -69,8 +77,8 @@ class ConfigProperties extends ConfigPair { /** The credentials configuration property. */ class CredentialsConfig extends ConfigProperties { CredentialsConfig() { - this.getNameElement().getName().trim().toLowerCase().matches(suspicious()) and - not this.getNameElement().getName().trim().toLowerCase().matches(nonSuspicious()) + this.getNameElement().getName().trim().toLowerCase().matches(possibleSecretName()) and + not this.getNameElement().getName().trim().toLowerCase().matches(possibleEncryptedSecretName()) } string getName() { result = this.getNameElement().getName().trim() } diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql new file mode 100644 index 00000000000..bb7495dc4d9 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql @@ -0,0 +1,84 @@ +/** + * @name Cleartext Credentials in Properties File + * @description Finds cleartext credentials in Java properties files. + * @kind problem + * @id java/credentials-in-properties + * @tags security + * external/cwe/cwe-555 + * external/cwe/cwe-256 + * external/cwe/cwe-260 + */ + +/* + * Note this query requires properties files to be indexed before it can produce results. + * If creating your own database with the CodeQL CLI, you should run + * `codeql database index-files --language=properties ...` + * If using lgtm.com, you should add `properties_files: true` to the index block of your + * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction) + */ + +import java +import semmle.code.configfiles.ConfigFiles + +private string possibleSecretName() { + result = "%password%" or + result = "%passwd%" or + result = "%account%" or + result = "%accnt%" or + result = "%credential%" or + result = "%token%" or + result = "%secret%" or + result = "%access%key%" +} + +private string possibleEncryptedSecretName() { + result = "%hashed%" or + result = "%encrypted%" or + result = "%crypt%" +} + +/** Holds if the value is not cleartext credentials. */ +bindingset[value] +predicate isNotCleartextCredentials(string value) { + value = "" // Empty string + or + value.length() < 7 // Typical credentials are no less than 6 characters + or + value.matches("% %") // Sentences containing spaces + or + value.regexpMatch(".*[^a-zA-Z\\d]{3,}.*") // Contain repeated non-alphanumeric characters such as a fake password pass**** or ???? + or + value.matches("@%") // Starts with the "@" sign + or + value.regexpMatch("\\$\\{.*\\}") // Variable placeholder ${credentials} + or + value.matches("%=") // A basic check of encrypted credentials ending with padding characters + or + value.matches("ENC(%)") // Encrypted value + or + // Could be a message property for UI display or fake passwords, e.g. login.password_expired=Your current password has expired. + value.toLowerCase().matches(possibleSecretName()) +} + +/** The properties file with configuration key/value pairs. */ +class ConfigProperties extends ConfigPair { + ConfigProperties() { this.getFile().getBaseName().toLowerCase().matches("%.properties") } +} + +/** The credentials configuration property. */ +class CredentialsConfig extends ConfigProperties { + CredentialsConfig() { + this.getNameElement().getName().trim().toLowerCase().matches(possibleSecretName()) and + not this.getNameElement().getName().trim().toLowerCase().matches(possibleEncryptedSecretName()) + } + + string getName() { result = this.getNameElement().getName().trim() } + + string getValue() { result = this.getValueElement().getValue().trim() } +} + +from CredentialsConfig cc +where not isNotCleartextCredentials(cc.getValue()) +select cc, + "Plaintext credentials " + cc.getName() + " have cleartext value " + cc.getValue() + + " in properties file." diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.qlref b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.qlref deleted file mode 100644 index e2536bfe883..00000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.qlref +++ /dev/null @@ -1 +0,0 @@ -experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql \ No newline at end of file From 7a511c5682ca1aec9368a8e1f0c2106fd0912408 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Sat, 27 Mar 2021 02:20:59 +0100 Subject: [PATCH 0098/1429] Python: update naming --- python/ql/src/Security/CWE-327/InsecureProtocol.ql | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index 974c36ddb0c..37aab84795e 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -18,11 +18,12 @@ string callName(AstNode call) { exists(Attribute a | a = call | result = callName(a.getObject()) + "." + a.getName()) } -string originName(DataFlow::Node contextOrigin) { - result = "call to " + callName(contextOrigin.asCfgNode().(CallNode).getFunction().getNode()) +string configName(DataFlow::Node protocolConfiguration) { + result = + "call to " + callName(protocolConfiguration.asCfgNode().(CallNode).getFunction().getNode()) or - not contextOrigin.asCfgNode() instanceof CallNode and - not contextOrigin instanceof ContextCreation and + not protocolConfiguration.asCfgNode() instanceof CallNode and + not protocolConfiguration instanceof ContextCreation and result = "context modification" } @@ -48,4 +49,4 @@ where specific = true select connectionCreation, "Insecure SSL/TLS protocol version " + insecure_version + " " + verb(specific) + " by $@ ", - protocolConfiguration, originName(protocolConfiguration) + protocolConfiguration, configName(protocolConfiguration) From 16902c2f56993f4ca29f3f05ca1ce1ac35b656be Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Sat, 27 Mar 2021 02:40:13 +0100 Subject: [PATCH 0099/1429] Python: handle default argument --- python/ql/src/Security/CWE-327/Ssl.qll | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/ql/src/Security/CWE-327/Ssl.qll b/python/ql/src/Security/CWE-327/Ssl.qll index 7f141523390..928a5f74e2d 100644 --- a/python/ql/src/Security/CWE-327/Ssl.qll +++ b/python/ql/src/Security/CWE-327/Ssl.qll @@ -160,6 +160,10 @@ class UnspecificSSLContextCreation extends SSLContextCreation, UnspecificContext // These are turned off by default // see https://docs.python.org/3/library/ssl.html#ssl-contexts not result in ["SSLv2", "SSLv3"] + or + // The default argument is TLS and the SSL versions are turned off by default. + not exists(this.getProtocol()) and + result in ["TLSv1", "TLSv1_1", "TLSv1_2", "TLSv1_3"] } } From 6d72b4fd39b51315cf0720c35455bb3b66447cae Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Sat, 27 Mar 2021 03:10:43 +0100 Subject: [PATCH 0100/1429] Python: Limit pretty printing to relevant nodes --- .../src/Security/CWE-327/InsecureProtocol.ql | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/python/ql/src/Security/CWE-327/InsecureProtocol.ql b/python/ql/src/Security/CWE-327/InsecureProtocol.ql index 37aab84795e..19f6acf5512 100644 --- a/python/ql/src/Security/CWE-327/InsecureProtocol.ql +++ b/python/ql/src/Security/CWE-327/InsecureProtocol.ql @@ -12,13 +12,46 @@ import python import FluentApiModel -string callName(AstNode call) { +// Helper for pretty printer `configName`. +// This is a consequence of missing pretty priting. +// We do not want to evaluate our bespoke pretty printer +// for all `DataFlow::Node`s so we define a sub class of interesting ones. +class ProtocolConfiguration extends DataFlow::Node { + ProtocolConfiguration() { + unsafe_connection_creation_with_context(_, _, this, _) + or + unsafe_connection_creation_without_context(this, _) + or + unsafe_context_creation(this, _) + } +} + +// Helper for pretty printer `callName`. +// This is a consequence of missing pretty priting. +// We do not want to evaluate our bespoke pretty printer +// for all `AstNode`s so we define a sub class of interesting ones. +// +// Note that AstNode is abstract and AstNode_ is a library class, so +// we have to extend @py_ast_node. +class Namable extends @py_ast_node { + Namable() { + exists(ProtocolConfiguration protocolConfiguration | + this = protocolConfiguration.asCfgNode().(CallNode).getFunction().getNode() + ) + or + exists(Namable attr | this = attr.(Attribute).getObject()) + } + + string toString() { result = "AstNode" } +} + +string callName(Namable call) { result = call.(Name).getId() or exists(Attribute a | a = call | result = callName(a.getObject()) + "." + a.getName()) } -string configName(DataFlow::Node protocolConfiguration) { +string configName(ProtocolConfiguration protocolConfiguration) { result = "call to " + callName(protocolConfiguration.asCfgNode().(CallNode).getFunction().getNode()) or From 5ce3f9d6ff6eb740d8c6add31fd7c95fa79f6453 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Sun, 28 Mar 2021 03:15:06 +0000 Subject: [PATCH 0101/1429] Update qldoc and enhance the query --- .../CWE-555/CredentialsInPropertiesFile.qhelp | 6 +- .../CWE-555/CredentialsInPropertiesFile.ql | 84 +++++++++++++------ .../CredentialsInPropertiesFile.expected | 10 +-- .../CWE-555/CredentialsInPropertiesFile.ql | 81 ++++++++++++------ .../security/CWE-555/PropertiesUtils.java | 57 +++++++++++++ .../security/CWE-555/messages.properties | 1 + 6 files changed, 179 insertions(+), 60 deletions(-) create mode 100644 java/ql/test/experimental/query-tests/security/CWE-555/PropertiesUtils.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp index 0869b886260..5549f188f1f 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.qhelp @@ -3,7 +3,7 @@

    Credentials management issues occur when credentials are stored in plaintext in - an application’s properties file. Common credentials include but are not limited + an application's properties file. Common credentials include but are not limited to LDAP, mail, database, proxy account, and so on. Storing plaintext credentials in a properties file allows anyone who can read the file access to the protected resource. Good credentials management guidelines require that credentials never @@ -21,12 +21,12 @@

    - In the first example, the credentials of a LDAP and datasource properties are stored + In the first example, the credentials for the LDAP and datasource properties are stored in cleartext in the properties file.

    - In the second example, the credentials of a LDAP and datasource properties are stored + In the second example, the credentials for the LDAP and datasource properties are stored in an encrypted format.

    diff --git a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql index 1e9b9906096..8bbc0d00a46 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql @@ -14,28 +14,22 @@ * If creating your own database with the CodeQL CLI, you should run * `codeql database index-files --language=properties ...` * If using lgtm.com, you should add `properties_files: true` to the index block of your - * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction) + * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction#customizing-index) */ import java import semmle.code.configfiles.ConfigFiles +import semmle.code.java.dataflow.FlowSources private string possibleSecretName() { - result = "%password%" or - result = "%passwd%" or - result = "%account%" or - result = "%accnt%" or - result = "%credential%" or - result = "%token%" or - result = "%secret%" or - result = "%access%key%" + result = + [ + "%password%", "%passwd%", "%account%", "%accnt%", "%credential%", "%token%", "%secret%", + "%access%key%" + ] } -private string possibleEncryptedSecretName() { - result = "%hashed%" or - result = "%encrypted%" or - result = "%crypt%" -} +private string possibleEncryptedSecretName() { result = ["%hashed%", "%encrypted%", "%crypt%"] } /** Holds if the value is not cleartext credentials. */ bindingset[value] @@ -69,27 +63,63 @@ predicate isNonProdCredentials(CredentialsConfig cc) { cc.getFile().getAbsolutePath().matches(["%dev%", "%test%", "%sample%"]) } -/** The properties file with configuration key/value pairs. */ -class ConfigProperties extends ConfigPair { - ConfigProperties() { this.getFile().getBaseName().toLowerCase().matches("%.properties") } -} - /** The credentials configuration property. */ -class CredentialsConfig extends ConfigProperties { +class CredentialsConfig extends ConfigPair { CredentialsConfig() { this.getNameElement().getName().trim().toLowerCase().matches(possibleSecretName()) and - not this.getNameElement().getName().trim().toLowerCase().matches(possibleEncryptedSecretName()) + not this.getNameElement().getName().trim().toLowerCase().matches(possibleEncryptedSecretName()) and + not isNotCleartextCredentials(this.getValueElement().getValue().trim()) } string getName() { result = this.getNameElement().getName().trim() } string getValue() { result = this.getValueElement().getValue().trim() } + + /** Returns a description of this vulnerability. */ + string getConfigDesc() { + exists( + LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink, MethodAccess ma + | + this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and + cc.hasFlow(source, sink) and + ma.getArgument(0) = sink.asExpr() and + result = "Plaintext credentials " + this.getName() + " are loaded in " + ma + ) + or + not exists(LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink | + this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and + cc.hasFlow(source, sink) + ) and + result = + "Plaintext credentials " + this.getName() + " have cleartext value " + this.getValue() + + " in properties file" + } +} + +/** + * A dataflow configuration tracking flow of a method that loads a credentials property. + */ +class LoadCredentialsConfiguration extends DataFlow::Configuration { + LoadCredentialsConfiguration() { this = "LoadCredentialsConfiguration" } + + override predicate isSource(DataFlow::Node source) { + exists(CredentialsConfig cc | + source.asExpr().(CompileTimeConstantExpr).getStringValue() = cc.getName() + ) + } + + override predicate isSink(DataFlow::Node sink) { + sink.asExpr() = + any(MethodAccess ma | + ma.getMethod() + .getDeclaringType() + .getASupertype*() + .hasQualifiedName("java.util", "Properties") and + ma.getMethod().getName() = "getProperty" + ).getArgument(0) + } } from CredentialsConfig cc -where - not isNotCleartextCredentials(cc.getValue()) and - not isNonProdCredentials(cc) -select cc, - "Plaintext credentials " + cc.getName() + " have cleartext value " + cc.getValue() + - " in properties file." +where not isNonProdCredentials(cc) +select cc, cc.getConfigDesc() diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected index 0ce33913932..1bdc004a446 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected @@ -1,5 +1,5 @@ -| configuration.properties:6:1:6:25 | ldap.password=mysecpass | Plaintext credentials ldap.password have cleartext value mysecpass in properties file. | -| configuration.properties:18:1:18:35 | datasource1.password=Passw0rd@123 | Plaintext credentials datasource1.password have cleartext value Passw0rd@123 in properties file. | -| configuration.properties:25:1:25:31 | mail.password=MysecPWxWa@1993 | Plaintext credentials mail.password have cleartext value MysecPWxWa@1993 in properties file. | -| configuration.properties:33:1:33:50 | com.example.aws.s3.access_key=AKMAMQPBYMCD6YSAYCBA | Plaintext credentials com.example.aws.s3.access_key have cleartext value AKMAMQPBYMCD6YSAYCBA in properties file. | -| configuration.properties:34:1:34:70 | com.example.aws.s3.secret_key=8lMPSfWzZq+wcWtck5+QPLOJDZzE783pS09/IO3k | Plaintext credentials com.example.aws.s3.secret_key have cleartext value 8lMPSfWzZq+wcWtck5+QPLOJDZzE783pS09/IO3k in properties file. | +| configuration.properties:6:1:6:25 | ldap.password=mysecpass | Plaintext credentials ldap.password are loaded in getProperty(...) | +| configuration.properties:18:1:18:35 | datasource1.password=Passw0rd@123 | Plaintext credentials datasource1.password are loaded in getProperty(...) | +| configuration.properties:25:1:25:31 | mail.password=MysecPWxWa@1993 | Plaintext credentials mail.password are loaded in getProperty(...) | +| configuration.properties:33:1:33:50 | com.example.aws.s3.access_key=AKMAMQPBYMCD6YSAYCBA | Plaintext credentials com.example.aws.s3.access_key are loaded in getProperty(...) | +| configuration.properties:34:1:34:70 | com.example.aws.s3.secret_key=8lMPSfWzZq+wcWtck5+QPLOJDZzE783pS09/IO3k | Plaintext credentials com.example.aws.s3.secret_key are loaded in getProperty(...) | diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql index bb7495dc4d9..b6890e4ac9f 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql +++ b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql @@ -14,28 +14,22 @@ * If creating your own database with the CodeQL CLI, you should run * `codeql database index-files --language=properties ...` * If using lgtm.com, you should add `properties_files: true` to the index block of your - * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction) + * lgtm.yml file (see https://lgtm.com/help/lgtm/java-extraction#customizing-index) */ import java import semmle.code.configfiles.ConfigFiles +import semmle.code.java.dataflow.FlowSources private string possibleSecretName() { - result = "%password%" or - result = "%passwd%" or - result = "%account%" or - result = "%accnt%" or - result = "%credential%" or - result = "%token%" or - result = "%secret%" or - result = "%access%key%" + result = + [ + "%password%", "%passwd%", "%account%", "%accnt%", "%credential%", "%token%", "%secret%", + "%access%key%" + ] } -private string possibleEncryptedSecretName() { - result = "%hashed%" or - result = "%encrypted%" or - result = "%crypt%" -} +private string possibleEncryptedSecretName() { result = ["%hashed%", "%encrypted%", "%crypt%"] } /** Holds if the value is not cleartext credentials. */ bindingset[value] @@ -60,25 +54,62 @@ predicate isNotCleartextCredentials(string value) { value.toLowerCase().matches(possibleSecretName()) } -/** The properties file with configuration key/value pairs. */ -class ConfigProperties extends ConfigPair { - ConfigProperties() { this.getFile().getBaseName().toLowerCase().matches("%.properties") } -} - /** The credentials configuration property. */ -class CredentialsConfig extends ConfigProperties { +class CredentialsConfig extends ConfigPair { CredentialsConfig() { this.getNameElement().getName().trim().toLowerCase().matches(possibleSecretName()) and - not this.getNameElement().getName().trim().toLowerCase().matches(possibleEncryptedSecretName()) + not this.getNameElement().getName().trim().toLowerCase().matches(possibleEncryptedSecretName()) and + not isNotCleartextCredentials(this.getValueElement().getValue().trim()) } string getName() { result = this.getNameElement().getName().trim() } string getValue() { result = this.getValueElement().getValue().trim() } + + /** Returns a description of this vulnerability. */ + string getConfigDesc() { + exists( + LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink, MethodAccess ma + | + this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and + cc.hasFlow(source, sink) and + ma.getArgument(0) = sink.asExpr() and + result = "Plaintext credentials " + this.getName() + " are loaded in " + ma + ) + or + not exists(LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink | + this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and + cc.hasFlow(source, sink) + ) and + result = + "Plaintext credentials " + this.getName() + " have cleartext value " + this.getValue() + + " in properties file" + } +} + +/** + * A dataflow configuration tracking flow of a method that loads a credentials property. + */ +class LoadCredentialsConfiguration extends DataFlow::Configuration { + LoadCredentialsConfiguration() { this = "LoadCredentialsConfiguration" } + + override predicate isSource(DataFlow::Node source) { + exists(CredentialsConfig cc | + source.asExpr().(CompileTimeConstantExpr).getStringValue() = cc.getName() + ) + } + + override predicate isSink(DataFlow::Node sink) { + sink.asExpr() = + any(MethodAccess ma | + ma.getMethod() + .getDeclaringType() + .getASupertype*() + .hasQualifiedName("java.util", "Properties") and + ma.getMethod().getName() = "getProperty" + ).getArgument(0) + } } from CredentialsConfig cc -where not isNotCleartextCredentials(cc.getValue()) -select cc, - "Plaintext credentials " + cc.getName() + " have cleartext value " + cc.getValue() + - " in properties file." +select cc, cc.getConfigDesc() diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/PropertiesUtils.java b/java/ql/test/experimental/query-tests/security/CWE-555/PropertiesUtils.java new file mode 100644 index 00000000000..b54995a9967 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-555/PropertiesUtils.java @@ -0,0 +1,57 @@ +import java.io.IOException; +import java.util.Properties; + +public class PropertiesUtils { + /* Properties declaration. */ + private static Properties properties; + + /** Static block to initializing the properties. */ + static { + properties = new Properties(); + try { + properties.load(PropertiesUtils.class.getClassLoader().getResourceAsStream("configuration.properties")); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** Returns the LDAP DN property value. */ + public static String getLdapDN() { + return properties.getProperty("ldap.loginDN"); + } + + /** Returns the LDAP password property value. */ + public static String getLdapPassword() { + return properties.getProperty("ldap.password"); + } + + /** Returns the SQL Server username property value. */ + public static String getMSDataSourceUserName() { + return properties.getProperty("datasource1.username"); + } + + /** Returns the SQL Server password property value. */ + public static String getMSDataSourcePassword() { + return properties.getProperty("datasource1.password"); + } + + /** Returns the mail account property value. */ + public static String getMailUserName() { + return properties.getProperty("mail.username"); + } + + /** Returns the mail password property value. */ + public static String getMailPassword() { + return properties.getProperty("mail.password"); + } + + /** Returns the AWS Access Key property value. */ + public static String getAWSAccessKey() { + return properties.getProperty("com.example.aws.s3.access_key"); + } + + /** Returns the AWS Secret Key property value. */ + public static String getAWSSecretKey() { + return properties.getProperty("com.example.aws.s3.secret_key"); + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/messages.properties b/java/ql/test/experimental/query-tests/security/CWE-555/messages.properties index fac63ec23e8..27a244c6071 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-555/messages.properties +++ b/java/ql/test/experimental/query-tests/security/CWE-555/messages.properties @@ -1,3 +1,4 @@ +# GOOD: UI display messages; not credentials prompt.username=Username prompt.password=Password From 799d509f26d0954025fea5302945387194fdef46 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 18 Mar 2021 17:31:41 +0100 Subject: [PATCH 0102/1429] Upload LDAP Injection query, qhelp and tests --- .../src/Security/CWE-090/LDAPInjection.qhelp | 32 +++++++ .../ql/src/Security/CWE-090/LDAPInjection.ql | 86 +++++++++++++++++++ .../Security/CWE-090/tests/ldap3_sanitized.py | 58 +++++++++++++ .../CWE-090/tests/ldap3_sanitized_asObj.py | 57 ++++++++++++ .../CWE-090/tests/ldap3_unsanitized.py | 56 ++++++++++++ .../CWE-090/tests/ldap3_unsanitized_asObj.py | 55 ++++++++++++ .../Security/CWE-090/tests/ldap_sanitized.py | 56 ++++++++++++ .../CWE-090/tests/ldap_sanitized_asObj.py | 57 ++++++++++++ .../CWE-090/tests/ldap_unsanitized.py | 52 +++++++++++ .../CWE-090/tests/ldap_unsanitized_asObj.py | 52 +++++++++++ 10 files changed, 561 insertions(+) create mode 100644 python/ql/src/Security/CWE-090/LDAPInjection.qhelp create mode 100644 python/ql/src/Security/CWE-090/LDAPInjection.ql create mode 100644 python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py create mode 100644 python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py create mode 100644 python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py create mode 100644 python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py create mode 100644 python/ql/src/Security/CWE-090/tests/ldap_sanitized.py create mode 100644 python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py create mode 100644 python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py create mode 100644 python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py diff --git a/python/ql/src/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/Security/CWE-090/LDAPInjection.qhelp new file mode 100644 index 00000000000..e077ccc3afe --- /dev/null +++ b/python/ql/src/Security/CWE-090/LDAPInjection.qhelp @@ -0,0 +1,32 @@ + + + + +

    If an LDAP query is built by a not sanitized user-provided value, a user is likely to be able to run malicious LDAP queries.

    +
    + + +

    In case user input must compose an LDAP query, it should be escaped in order to avoid a malicious user supplying special characters that change the actual purpose of the query. To do so, functions that ldap frameworks provide such as escape_filter_chars should be applied to that user input. + + + + +

  • + OWASP + LDAP Injection +
  • +
  • + SonarSource + RSPEC-2078 +
  • +
  • + Python + LDAP Documentation +
  • +
  • + CWE- + 090 +
  • +
    + + \ No newline at end of file diff --git a/python/ql/src/Security/CWE-090/LDAPInjection.ql b/python/ql/src/Security/CWE-090/LDAPInjection.ql new file mode 100644 index 00000000000..d6ff1ad5607 --- /dev/null +++ b/python/ql/src/Security/CWE-090/LDAPInjection.ql @@ -0,0 +1,86 @@ +/** + * @name Python LDAP Injection + * @description Python LDAP Injection through search filter + * @kind path-problem + * @problem.severity error + * @id python/ldap-injection + * @tags experimental + * security + * external/cwe/cwe-090 + */ + +import python +import semmle.python.dataflow.new.RemoteFlowSources +import semmle.python.dataflow.new.DataFlow +import semmle.python.dataflow.new.TaintTracking +import semmle.python.dataflow.new.internal.TaintTrackingPublic +import DataFlow::PathGraph + +class InitializeSink extends DataFlow::Node { + InitializeSink() { + exists(SsaVariable initVar, CallNode searchCall | + // get variable whose value equals a call to ldap.initialize + initVar.getDefinition().getImmediateDominator() = Value::named("ldap.initialize").getACall() and + // get the Call in which the previous variable is used + initVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and + // restrict that call's attribute (something.this) to match %search% + searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and + // set the third argument (search_filter) as sink + this.asExpr() = searchCall.getArg(2).getNode() + // set the first argument (DN) as sink + // or this.asExpr() = searchCall.getArg(0) // Should this be set? + ) + } +} + +class ConnectionSink extends DataFlow::Node { + ConnectionSink() { + exists(SsaVariable connVar, CallNode searchCall | + // get variable whose value equals a call to ldap.initialize + connVar.getDefinition().getImmediateDominator() = Value::named("ldap3.Connection").getACall() and + // get the Call in which the previous variable is used + connVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and + // restrict that call's attribute (something.this) to match %search% + searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and + // set the second argument (search_filter) as sink + this.asExpr() = searchCall.getArg(1).getNode() + // set the first argument (DN) as sink + // or this.asExpr() = searchCall.getArg(0) // Should this be set? + ) + } +} + +class EscapeSanitizer extends DataFlow::Node { + EscapeSanitizer() { + exists(Call c | + ( + // avoid flow through any %escape% function + c.getFunc().(Attribute).getName().matches("%escape%") or // something.%escape%() + c.getFunc().(Name).getId().matches("%escape%") // %escape%() + ) and + this.asExpr() = c + ) + } +} + +class LDAPInjectionSink extends DataFlow::Node { + LDAPInjectionSink() { + this instanceof InitializeSink or + this instanceof ConnectionSink + } +} + +class LDAPInjectionFlowConfig extends TaintTracking::Configuration { + LDAPInjectionFlowConfig() { this = "LDAPInjectionFlowConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPInjectionSink } + + override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof EscapeSanitizer } +} + +from LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink +where config.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "$@ LDAP query executes $@.", sink.getNode(), "This", + source.getNode(), "a user-provided value" diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py b/python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py new file mode 100644 index 00000000000..05bc1e24f71 --- /dev/null +++ b/python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py @@ -0,0 +1,58 @@ +import ldap3 +from ldap3.utils.conv import escape_filter_chars +from flask import request, Flask + +app = Flask(__name__) + + +@app.route("/tainted_var") +def tainted_var(): + sanitized_dn = "dc=%s" % request.args['dc'] + sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars( + request.args['username']) + + srv = ldap3.Server('localhost', port=1337) + conn = ldap3.Connection(srv, user=sanitized_dn, auto_bind=True) + conn.search(sanitized_dn, sanitized_filter) + return conn.response + + +@app.route("/var_tainted") +def var_tainted(): + sanitized_dn = request.args['dc'] + sanitized_filter = request.args['username'] + + dn = "dc=%s" % sanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter) + + srv = ldap3.Server('localhost', port=1337) + conn = ldap3.Connection(srv, user=dn, auto_bind=True) + conn.search(dn, search_filter) + return conn.response + + +@app.route("/direct") +def direct(): + srv = ldap3.Server('localhost', port=1337) + conn = ldap3.Connection(srv, user="dc=%s" % + request.args['dc'], auto_bind=True) + conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" % + escape_filter_chars(request.args['username'])) + return conn.response + + +@ app.route("/with_") +def with_(): + sanitized_dn = request.args['dc'] + sanitized_filter = escape_filter_chars(request.args['username']) + + dn = "dc=%s" % sanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter + + srv = ldap3.Server('localhost', port=1337) + with ldap3.Connection(server, auto_bind=True) as conn: + conn.search(dn, search_filter) + return conn.response + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py b/python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py new file mode 100644 index 00000000000..c7dbb345058 --- /dev/null +++ b/python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py @@ -0,0 +1,57 @@ +from ldap3 import Server, Connection +from ldap3.utils.conv import escape_filter_chars +from flask import request, Flask + +app = Flask(__name__) + + +@app.route("/tainted_var") +def tainted_var(): + sanitized_dn = "dc=%s" % request.args['dc'] + sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars( + request.args['username']) + + srv = Server('localhost', port=1337) + conn = Connection(srv, user=sanitized_dn, auto_bind=True) + conn.search(sanitized_dn, sanitized_filter) + return conn.response + + +@app.route("/var_tainted") +def var_tainted(): + sanitized_dn = request.args['dc'] + sanitized_filter = request.args['username'] + + dn = "dc=%s" % sanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter) + + srv = Server('localhost', port=1337) + conn = Connection(srv, user=dn, auto_bind=True) + conn.search(dn, search_filter) + return conn.response + + +@app.route("/direct") +def direct(): + srv = Server('localhost', port=1337) + conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True) + conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" % + escape_filter_chars(request.args['username'])) + return conn.response + + +@app.route("/with_2") +def with_2(): + sanitized_dn = request.args['dc'] + sanitized_filter = escape_filter_chars(request.args['username']) + + dn = "dc=%s" % sanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter + + srv = Server('localhost', port=1337) + with Connection(server, auto_bind=True) as conn: + conn.search(dn, search_filter) + return conn.response + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py b/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py new file mode 100644 index 00000000000..fcd00c0269c --- /dev/null +++ b/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py @@ -0,0 +1,56 @@ +import ldap3 +from flask import request, Flask + +app = Flask(__name__) + + +@app.route("/tainted_var") +def tainted_var(): + unsanitized_dn = "dc=%s" % request.args['dc'] + unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username'] + + srv = ldap3.Server('localhost', port=1337) + conn = ldap3.Connection(srv, user=unsanitized_dn, auto_bind=True) + conn.search(unsanitized_dn, unsanitized_filter) + return conn.response + + +@app.route("/var_tainted") +def var_tainted(): + unsanitized_dn = request.args['dc'] + unsanitized_filter = request.args['username'] + + dn = "dc=%s" % unsanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter + + srv = ldap3.Server('localhost', port=1337) + conn = ldap3.Connection(srv, user=dn, auto_bind=True) + conn.search(dn, search_filter) + return conn.response + + +@app.route("/direct") +def direct(): + srv = ldap3.Server('localhost', port=1337) + conn = ldap3.Connection(srv, user="dc=%s" % + request.args['dc'], auto_bind=True) + conn.search("dc=%s" % unsanitized_dn, + "(&(objectClass=*)(uid=%s))" % request.args['username']) + return conn.response + + +@app.route("/with_") +def with_(): + unsanitized_dn = request.args['dc'] + unsanitized_filter = request.args['username'] + + dn = "dc=%s" % unsanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter + + srv = ldap3.Server('localhost', port=1337) + with ldap3.Connection(server, auto_bind=True) as conn: + conn.search(dn, search_filter) + return conn.response + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py b/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py new file mode 100644 index 00000000000..634ee992f3e --- /dev/null +++ b/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py @@ -0,0 +1,55 @@ +from ldap3 import Server, Connection +from flask import request, Flask + +app = Flask(__name__) + + +@app.route("/tainted_var") +def tainted_var(): + unsanitized_dn = "dc=%s" % request.args['dc'] + unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username'] + + srv = Server('localhost', port=1337) + conn = Connection(srv, user=unsanitized_dn, auto_bind=True) + conn.search(unsanitized_dn, unsanitized_filter) + return conn.response + + +@app.route("/var_tainted") +def var_tainted(): + unsanitized_dn = request.args['dc'] + unsanitized_filter = request.args['username'] + + dn = "dc=%s" % unsanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter + + srv = Server('localhost', port=1337) + conn = Connection(srv, user=dn, auto_bind=True) + conn.search(dn, search_filter) + return conn.response + + +@app.route("/direct") +def direct(): + srv = Server('localhost', port=1337) + conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True) + conn.search( + "dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" % request.args['username']) + return conn.response + + +@app.route("/with_2") +def with_2(): + unsanitized_dn = request.args['dc'] + unsanitized_filter = request.args['username'] + + dn = "dc=%s" % unsanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter + + srv = Server('localhost', port=1337) + with Connection(server, auto_bind=True) as conn: + conn.search(dn, search_filter) + return conn.response + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-090/tests/ldap_sanitized.py b/python/ql/src/Security/CWE-090/tests/ldap_sanitized.py new file mode 100644 index 00000000000..83bafa79195 --- /dev/null +++ b/python/ql/src/Security/CWE-090/tests/ldap_sanitized.py @@ -0,0 +1,56 @@ +from flask import request, Flask +import ldap +import ldap.filter +import ldap.dn + +app = Flask(__name__) + + +@app.route("/tainted_var") +def tainted_var(): + sanitized_dn = "dc=%s" % ldap.dn.escape_dn_chars(request.args['dc']) + sanitized_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars( + request.args['username']) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + sanitized_dn, ldap.SCOPE_SUBTREE, sanitized_filter) + return user[0] + + +@app.route("/var_tainted") +def var_tainted(): + sanitized_dn = request.args['dc'] + sanitized_filter = request.args['username'] + + dn = "dc=%s" % ldap.dn.escape_dn_chars(sanitized_dn) + search_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(sanitized_filter) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/direct") +def direct(): + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s("dc=%s" % ldap.dn.escape_dn_chars( + request.args['dc']), ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(request.args['username'])) + return user[0] + + +@app.route("/with_") +def with_(): + sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc']) + sanitized_filter = ldap.filter.escape_filter_chars( + request.args['username']) + + dn = "dc=%s" % sanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter + + with ldap.initialize("ldap://127.0.0.1:1337") as ldap_connection: + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py b/python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py new file mode 100644 index 00000000000..1c9662a4bcf --- /dev/null +++ b/python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py @@ -0,0 +1,57 @@ +from flask import request, Flask +from ldap import initialize +import ldap.filter +import ldap.dn + +app = Flask(__name__) + + +@app.route("/tainted_var_2") +def tainted_var_2(): + sanitized_dn = "dc=%s" % ldap.dn.escape_dn_chars(request.args['dc']) + sanitized_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars( + request.args['username']) + + ldap_connection = initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + sanitized_dn, ldap.SCOPE_SUBTREE, sanitized_filter) + return user[0] + + +@app.route("/var_tainted_2") +def var_tainted_2(): + sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc']) + sanitized_filter = ldap.filter.escape_filter_chars( + request.args['username']) + + dn = "dc=%s" % sanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter + + ldap_connection = initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/direct_2") +def direct_2(): + ldap_connection = initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s("dc=%s" % ldap.dn.escape_dn_chars( + request.args['dc']), ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(request.args['username'])) + return user[0] + + +@app.route("/with_2") +def with_2(): + sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc']) + sanitized_filter = ldap.filter.escape_filter_chars( + request.args['username']) + + dn = "dc=%s" % sanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter + + with initialize("ldap://127.0.0.1:1337") as ldap_connection: + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py b/python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py new file mode 100644 index 00000000000..8b22596b578 --- /dev/null +++ b/python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py @@ -0,0 +1,52 @@ +from flask import request, Flask +import ldap + +app = Flask(__name__) + + +@app.route("/tainted_var") +def tainted_var(): + unsanitized_dn = "dc=%s" % request.args['dc'] + unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + unsanitized_dn, ldap.SCOPE_SUBTREE, unsanitized_filter) + return user[0] + + +@app.route("/var_tainted") +def var_tainted(): + unsanitized_dn = request.args['dc'] + unsanitized_filter = request.args['username'] + + dn = "dc=%s" % unsanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/direct") +def direct(): + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + "dc=%s" % request.args['dc'], ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % request.args['username']) + return user[0] + + +@app.route("/with_") +def with_(): + sanitized_dn = request.args['dc'] + sanitized_filter = request.args['username'] + + dn = "dc=%s" % sanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter + + with ldap.initialize("ldap://127.0.0.1:1337") as ldap_connection: + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py b/python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py new file mode 100644 index 00000000000..49774dda2ec --- /dev/null +++ b/python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py @@ -0,0 +1,52 @@ +from flask import request, Flask +from ldap import initialize + +app = Flask(__name__) + + +@app.route("/tainted_var") +def tainted_var(): + unsanitized_dn = "dc=%s" % request.args['dc'] + unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username'] + + ldap_connection = initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + unsanitized_dn, ldap.SCOPE_SUBTREE, unsanitized_filter) + return user[0] + + +@app.route("/var_tainted") +def var_tainted(): + unsanitized_dn = request.args['dc'] + unsanitized_filter = request.args['username'] + + dn = "dc=%s" % unsanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter + + ldap_connection = initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/direct") +def direct(): + ldap_connection = initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + "dc=%s" % request.args['dc'], ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % request.args['username']) + return user[0] + + +@app.route("/with_2") +def with_2(): + sanitized_dn = request.args['dc'] + sanitized_filter = request.args['username'] + + dn = "dc=%s" % sanitized_dn + search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter + + with initialize("ldap://127.0.0.1:1337") as ldap_connection: + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + +# if __name__ == "__main__": +# app.run(debug=True) From 719b48cbaf8193f57560bd499e890f679c8d7931 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 18 Mar 2021 20:21:47 +0100 Subject: [PATCH 0103/1429] Move to experimental folder --- .../src/{ => experimental}/Security/CWE-090/LDAPInjection.qhelp | 0 .../ql/src/{ => experimental}/Security/CWE-090/LDAPInjection.ql | 0 .../{ => experimental}/Security/CWE-090/tests/ldap3_sanitized.py | 0 .../Security/CWE-090/tests/ldap3_sanitized_asObj.py | 0 .../Security/CWE-090/tests/ldap3_unsanitized.py | 0 .../Security/CWE-090/tests/ldap3_unsanitized_asObj.py | 0 .../{ => experimental}/Security/CWE-090/tests/ldap_sanitized.py | 0 .../Security/CWE-090/tests/ldap_sanitized_asObj.py | 0 .../{ => experimental}/Security/CWE-090/tests/ldap_unsanitized.py | 0 .../Security/CWE-090/tests/ldap_unsanitized_asObj.py | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename python/ql/src/{ => experimental}/Security/CWE-090/LDAPInjection.qhelp (100%) rename python/ql/src/{ => experimental}/Security/CWE-090/LDAPInjection.ql (100%) rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap3_sanitized.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap3_sanitized_asObj.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap3_unsanitized.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap3_unsanitized_asObj.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap_sanitized.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap_sanitized_asObj.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap_unsanitized.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap_unsanitized_asObj.py (100%) diff --git a/python/ql/src/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp similarity index 100% rename from python/ql/src/Security/CWE-090/LDAPInjection.qhelp rename to python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp diff --git a/python/ql/src/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql similarity index 100% rename from python/ql/src/Security/CWE-090/LDAPInjection.ql rename to python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized.py similarity index 100% rename from python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py rename to python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized.py diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py similarity index 100% rename from python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py rename to python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized.py similarity index 100% rename from python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py rename to python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized.py diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py similarity index 100% rename from python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py rename to python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py diff --git a/python/ql/src/Security/CWE-090/tests/ldap_sanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py similarity index 100% rename from python/ql/src/Security/CWE-090/tests/ldap_sanitized.py rename to python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py diff --git a/python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py similarity index 100% rename from python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py rename to python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py diff --git a/python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py similarity index 100% rename from python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py rename to python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py diff --git a/python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py similarity index 100% rename from python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py rename to python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py From 95a1dae3154367e47fcaaa97cc35dfdb9b1c1fcb Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 18 Mar 2021 20:36:38 +0100 Subject: [PATCH 0104/1429] Precision warn and Remove CWE reference --- .../ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp | 4 ---- python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp index e077ccc3afe..a570549abfc 100644 --- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp +++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp @@ -23,10 +23,6 @@ Python LDAP Documentation -
  • - CWE- - 090 -
  • \ No newline at end of file diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql index d6ff1ad5607..55e477be50a 100644 --- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql +++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql @@ -9,6 +9,7 @@ * external/cwe/cwe-090 */ +// Determine precision above import python import semmle.python.dataflow.new.RemoteFlowSources import semmle.python.dataflow.new.DataFlow From 85ec82a389a76ef721d879e7c294550bb56fe8ac Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sun, 28 Mar 2021 21:07:08 +0200 Subject: [PATCH 0105/1429] Refactor in progress --- .../Security/CWE-090/LDAPInjection.ql | 82 +++---------------- .../ldap3_unsanitized.py => ldap3_bad.py} | 0 .../ldap3_sanitized.py => ldap3_good.py} | 0 .../CWE-090/tests/ldap3_sanitized_asObj.py | 57 ------------- .../CWE-090/tests/ldap3_unsanitized_asObj.py | 55 ------------- .../Security/CWE-090/tests/ldap_sanitized.py | 56 ------------- .../CWE-090/tests/ldap_sanitized_asObj.py | 57 ------------- .../CWE-090/tests/ldap_unsanitized.py | 52 ------------ .../CWE-090/tests/ldap_unsanitized_asObj.py | 52 ------------ .../Security/CWE-090/unit_tests/ldap_bad.py | 46 +++++++++++ .../Security/CWE-090/unit_tests/ldap_good.py | 60 ++++++++++++++ .../experimental/semmle/python/Concepts.qll | 43 ++++++++++ .../semmle/python/frameworks/Stdlib.qll | 56 +++++++++++++ .../security/injection/LDAPInjection.qll | 21 +++++ 14 files changed, 236 insertions(+), 401 deletions(-) rename python/ql/src/experimental/Security/CWE-090/{tests/ldap3_unsanitized.py => ldap3_bad.py} (100%) rename python/ql/src/experimental/Security/CWE-090/{tests/ldap3_sanitized.py => ldap3_good.py} (100%) delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py create mode 100644 python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py create mode 100644 python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py create mode 100644 python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql index 55e477be50a..778b771f883 100644 --- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql +++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql @@ -11,77 +11,15 @@ // Determine precision above import python -import semmle.python.dataflow.new.RemoteFlowSources -import semmle.python.dataflow.new.DataFlow -import semmle.python.dataflow.new.TaintTracking -import semmle.python.dataflow.new.internal.TaintTrackingPublic +import experimental.semmle.python.security.injection.LDAPInjection import DataFlow::PathGraph -class InitializeSink extends DataFlow::Node { - InitializeSink() { - exists(SsaVariable initVar, CallNode searchCall | - // get variable whose value equals a call to ldap.initialize - initVar.getDefinition().getImmediateDominator() = Value::named("ldap.initialize").getACall() and - // get the Call in which the previous variable is used - initVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and - // restrict that call's attribute (something.this) to match %search% - searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and - // set the third argument (search_filter) as sink - this.asExpr() = searchCall.getArg(2).getNode() - // set the first argument (DN) as sink - // or this.asExpr() = searchCall.getArg(0) // Should this be set? - ) - } -} - -class ConnectionSink extends DataFlow::Node { - ConnectionSink() { - exists(SsaVariable connVar, CallNode searchCall | - // get variable whose value equals a call to ldap.initialize - connVar.getDefinition().getImmediateDominator() = Value::named("ldap3.Connection").getACall() and - // get the Call in which the previous variable is used - connVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and - // restrict that call's attribute (something.this) to match %search% - searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and - // set the second argument (search_filter) as sink - this.asExpr() = searchCall.getArg(1).getNode() - // set the first argument (DN) as sink - // or this.asExpr() = searchCall.getArg(0) // Should this be set? - ) - } -} - -class EscapeSanitizer extends DataFlow::Node { - EscapeSanitizer() { - exists(Call c | - ( - // avoid flow through any %escape% function - c.getFunc().(Attribute).getName().matches("%escape%") or // something.%escape%() - c.getFunc().(Name).getId().matches("%escape%") // %escape%() - ) and - this.asExpr() = c - ) - } -} - -class LDAPInjectionSink extends DataFlow::Node { - LDAPInjectionSink() { - this instanceof InitializeSink or - this instanceof ConnectionSink - } -} - -class LDAPInjectionFlowConfig extends TaintTracking::Configuration { - LDAPInjectionFlowConfig() { this = "LDAPInjectionFlowConfig" } - - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - - override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPInjectionSink } - - override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof EscapeSanitizer } -} - -from LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink -where config.hasFlowPath(source, sink) -select sink.getNode(), source, sink, "$@ LDAP query executes $@.", sink.getNode(), "This", - source.getNode(), "a user-provided value" +from + LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink, + LDAPQuery castedSink +where + config.hasFlowPath(source, sink) and + castedSink = sink.getNode() +select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@ probably leaking $@.", + sink.getNode(), "This", source.getNode(), "a user-provided value", castedSink.getLDAPNode(), + castedSink.getLDAPPart(), castedSink.getAttrList(), "this attribute(s)" diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized.py b/python/ql/src/experimental/Security/CWE-090/ldap3_bad.py similarity index 100% rename from python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized.py rename to python/ql/src/experimental/Security/CWE-090/ldap3_bad.py diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized.py b/python/ql/src/experimental/Security/CWE-090/ldap3_good.py similarity index 100% rename from python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized.py rename to python/ql/src/experimental/Security/CWE-090/ldap3_good.py diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py deleted file mode 100644 index c7dbb345058..00000000000 --- a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py +++ /dev/null @@ -1,57 +0,0 @@ -from ldap3 import Server, Connection -from ldap3.utils.conv import escape_filter_chars -from flask import request, Flask - -app = Flask(__name__) - - -@app.route("/tainted_var") -def tainted_var(): - sanitized_dn = "dc=%s" % request.args['dc'] - sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars( - request.args['username']) - - srv = Server('localhost', port=1337) - conn = Connection(srv, user=sanitized_dn, auto_bind=True) - conn.search(sanitized_dn, sanitized_filter) - return conn.response - - -@app.route("/var_tainted") -def var_tainted(): - sanitized_dn = request.args['dc'] - sanitized_filter = request.args['username'] - - dn = "dc=%s" % sanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter) - - srv = Server('localhost', port=1337) - conn = Connection(srv, user=dn, auto_bind=True) - conn.search(dn, search_filter) - return conn.response - - -@app.route("/direct") -def direct(): - srv = Server('localhost', port=1337) - conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True) - conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" % - escape_filter_chars(request.args['username'])) - return conn.response - - -@app.route("/with_2") -def with_2(): - sanitized_dn = request.args['dc'] - sanitized_filter = escape_filter_chars(request.args['username']) - - dn = "dc=%s" % sanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter - - srv = Server('localhost', port=1337) - with Connection(server, auto_bind=True) as conn: - conn.search(dn, search_filter) - return conn.response - -# if __name__ == "__main__": -# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py deleted file mode 100644 index 634ee992f3e..00000000000 --- a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py +++ /dev/null @@ -1,55 +0,0 @@ -from ldap3 import Server, Connection -from flask import request, Flask - -app = Flask(__name__) - - -@app.route("/tainted_var") -def tainted_var(): - unsanitized_dn = "dc=%s" % request.args['dc'] - unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username'] - - srv = Server('localhost', port=1337) - conn = Connection(srv, user=unsanitized_dn, auto_bind=True) - conn.search(unsanitized_dn, unsanitized_filter) - return conn.response - - -@app.route("/var_tainted") -def var_tainted(): - unsanitized_dn = request.args['dc'] - unsanitized_filter = request.args['username'] - - dn = "dc=%s" % unsanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter - - srv = Server('localhost', port=1337) - conn = Connection(srv, user=dn, auto_bind=True) - conn.search(dn, search_filter) - return conn.response - - -@app.route("/direct") -def direct(): - srv = Server('localhost', port=1337) - conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True) - conn.search( - "dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" % request.args['username']) - return conn.response - - -@app.route("/with_2") -def with_2(): - unsanitized_dn = request.args['dc'] - unsanitized_filter = request.args['username'] - - dn = "dc=%s" % unsanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter - - srv = Server('localhost', port=1337) - with Connection(server, auto_bind=True) as conn: - conn.search(dn, search_filter) - return conn.response - -# if __name__ == "__main__": -# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py deleted file mode 100644 index 83bafa79195..00000000000 --- a/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py +++ /dev/null @@ -1,56 +0,0 @@ -from flask import request, Flask -import ldap -import ldap.filter -import ldap.dn - -app = Flask(__name__) - - -@app.route("/tainted_var") -def tainted_var(): - sanitized_dn = "dc=%s" % ldap.dn.escape_dn_chars(request.args['dc']) - sanitized_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars( - request.args['username']) - - ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s( - sanitized_dn, ldap.SCOPE_SUBTREE, sanitized_filter) - return user[0] - - -@app.route("/var_tainted") -def var_tainted(): - sanitized_dn = request.args['dc'] - sanitized_filter = request.args['username'] - - dn = "dc=%s" % ldap.dn.escape_dn_chars(sanitized_dn) - search_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(sanitized_filter) - - ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] - - -@app.route("/direct") -def direct(): - ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s("dc=%s" % ldap.dn.escape_dn_chars( - request.args['dc']), ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(request.args['username'])) - return user[0] - - -@app.route("/with_") -def with_(): - sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc']) - sanitized_filter = ldap.filter.escape_filter_chars( - request.args['username']) - - dn = "dc=%s" % sanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter - - with ldap.initialize("ldap://127.0.0.1:1337") as ldap_connection: - user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] - -# if __name__ == "__main__": -# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py deleted file mode 100644 index 1c9662a4bcf..00000000000 --- a/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py +++ /dev/null @@ -1,57 +0,0 @@ -from flask import request, Flask -from ldap import initialize -import ldap.filter -import ldap.dn - -app = Flask(__name__) - - -@app.route("/tainted_var_2") -def tainted_var_2(): - sanitized_dn = "dc=%s" % ldap.dn.escape_dn_chars(request.args['dc']) - sanitized_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars( - request.args['username']) - - ldap_connection = initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s( - sanitized_dn, ldap.SCOPE_SUBTREE, sanitized_filter) - return user[0] - - -@app.route("/var_tainted_2") -def var_tainted_2(): - sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc']) - sanitized_filter = ldap.filter.escape_filter_chars( - request.args['username']) - - dn = "dc=%s" % sanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter - - ldap_connection = initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] - - -@app.route("/direct_2") -def direct_2(): - ldap_connection = initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s("dc=%s" % ldap.dn.escape_dn_chars( - request.args['dc']), ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(request.args['username'])) - return user[0] - - -@app.route("/with_2") -def with_2(): - sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc']) - sanitized_filter = ldap.filter.escape_filter_chars( - request.args['username']) - - dn = "dc=%s" % sanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter - - with initialize("ldap://127.0.0.1:1337") as ldap_connection: - user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] - -# if __name__ == "__main__": -# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py deleted file mode 100644 index 8b22596b578..00000000000 --- a/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py +++ /dev/null @@ -1,52 +0,0 @@ -from flask import request, Flask -import ldap - -app = Flask(__name__) - - -@app.route("/tainted_var") -def tainted_var(): - unsanitized_dn = "dc=%s" % request.args['dc'] - unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username'] - - ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s( - unsanitized_dn, ldap.SCOPE_SUBTREE, unsanitized_filter) - return user[0] - - -@app.route("/var_tainted") -def var_tainted(): - unsanitized_dn = request.args['dc'] - unsanitized_filter = request.args['username'] - - dn = "dc=%s" % unsanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter - - ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] - - -@app.route("/direct") -def direct(): - ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s( - "dc=%s" % request.args['dc'], ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % request.args['username']) - return user[0] - - -@app.route("/with_") -def with_(): - sanitized_dn = request.args['dc'] - sanitized_filter = request.args['username'] - - dn = "dc=%s" % sanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter - - with ldap.initialize("ldap://127.0.0.1:1337") as ldap_connection: - user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] - -# if __name__ == "__main__": -# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py deleted file mode 100644 index 49774dda2ec..00000000000 --- a/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py +++ /dev/null @@ -1,52 +0,0 @@ -from flask import request, Flask -from ldap import initialize - -app = Flask(__name__) - - -@app.route("/tainted_var") -def tainted_var(): - unsanitized_dn = "dc=%s" % request.args['dc'] - unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username'] - - ldap_connection = initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s( - unsanitized_dn, ldap.SCOPE_SUBTREE, unsanitized_filter) - return user[0] - - -@app.route("/var_tainted") -def var_tainted(): - unsanitized_dn = request.args['dc'] - unsanitized_filter = request.args['username'] - - dn = "dc=%s" % unsanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter - - ldap_connection = initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] - - -@app.route("/direct") -def direct(): - ldap_connection = initialize("ldap://127.0.0.1:1337") - user = ldap_connection.search_s( - "dc=%s" % request.args['dc'], ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % request.args['username']) - return user[0] - - -@app.route("/with_2") -def with_2(): - sanitized_dn = request.args['dc'] - sanitized_filter = request.args['username'] - - dn = "dc=%s" % sanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter - - with initialize("ldap://127.0.0.1:1337") as ldap_connection: - user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] - -# if __name__ == "__main__": -# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py new file mode 100644 index 00000000000..a409a38e019 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py @@ -0,0 +1,46 @@ +from flask import request, Flask +import ldap + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter, ["testAttr1", "testAttr2"]) + + +@app.route("/direct") +def direct(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + user = ldap.initialize("ldap://127.0.0.1:1337").search_s( + unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter, ["testAttr1", "testAttr2"]) + + +@app.route("/normal_argbyname") +def normal_argbyname(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + unsafe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=unsafe_filter) + + +@app.route("/direct_argbyname") +def direct_argbyname(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + user = ldap.initialize("ldap://127.0.0.1:1337").search_s( + unsafe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=unsafe_filter) + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py new file mode 100644 index 00000000000..1d04505fa28 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py @@ -0,0 +1,60 @@ +from flask import request, Flask +import ldap +import ldap.filter +import ldap.dn + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + safe_dn = ldap.dn.escape_dn_chars(unsafe_dn) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + safe_dn, ldap.SCOPE_SUBTREE, safe_filter, ["testAttr1", "testAttr2"]) + + +@app.route("/direct") +def direct(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + safe_dn = ldap.dn.escape_dn_chars(unsafe_dn) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + user = ldap.initialize("ldap://127.0.0.1:1337").search_s( + safe_dn, ldap.SCOPE_SUBTREE, safe_filter, ["testAttr1", "testAttr2"]) + + +@app.route("/normal_argbyname") +def normal_argbyname(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + safe_dn = ldap.dn.escape_dn_chars(unsafe_dn) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + safe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=safe_filter) + + +@app.route("/direct_argbyname") +def direct_argbyname(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + safe_dn = ldap.dn.escape_dn_chars(unsafe_dn) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + user = ldap.initialize("ldap://127.0.0.1:1337").search_s( + safe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=safe_filter) + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 904b7967ee8..e2b278e40da 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -13,3 +13,46 @@ private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking private import experimental.semmle.python.Frameworks +private import semmle.python.ApiGraphs + +/** + * To-Do + * + * LDAPQuery -> collect functions executing a search filter/DN + * LDAPEscape -> collect functions escaping a search filter/DN + */ +module LDAPQuery { + abstract class Range extends DataFlow::Node { + abstract DataFlow::Node getLDAPNode(); + + abstract string getLDAPPart(); + + abstract DataFlow::Node getAttrs(); + } +} + +class LDAPQuery extends DataFlow::Node { + LDAPQuery::Range range; + + LDAPQuery() { this = range } + + DataFlow::Node getLDAPNode() { result = range.getLDAPNode() } + + string getLDAPPart() { result = range.getLDAPPart() } + + DataFlow::Node getAttrs() { result = range.getAttrs() } +} + +module LDAPEscape { + abstract class Range extends DataFlow::Node { + abstract DataFlow::Node getEscapeNode(); + } +} + +class LDAPEscape extends DataFlow::Node { + LDAPEscape::Range range; + + LDAPEscape() { this = range } + + DataFlow::Node getEscapeNode() { result = range.getEscapeNode() } +} diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll index 420caf0d73b..1ef42edbdbb 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll @@ -9,3 +9,59 @@ private import semmle.python.dataflow.new.TaintTracking private import semmle.python.dataflow.new.RemoteFlowSources private import experimental.semmle.python.Concepts private import semmle.python.ApiGraphs + +private module LDAP { + private module LDAP2 { + private class LDAP2QueryMethods extends string { + LDAP2QueryMethods() { + this in ["search", "search_s", "search_st", "search_ext", "search_ext_s"] + } + } + + private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range { + DataFlow::Node ldapNode; + string ldapPart; + DataFlow::Node attrs; + + LDAP2Query() { + exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode initCall | + this.getFunction() = searchMethod and + initCall = API::moduleImport("ldap").getMember("initialize").getACall() and + initCall = searchMethod.getObject().getALocalSource() and + searchMethod.getAttributeName() instanceof LDAP2QueryMethods and + ( + ( + ldapNode = this.getArg(2) or + ldapNode = this.getArgByName("filterstr") + ) and + ldapPart = "search_filter" + or + ldapNode = this.getArg(0) and + ldapPart = "DN" + ) and + ( // what if they're not set? + attrs = this.getArg(3) or + attrs = this.getArgByName("attrlist") + ) + ) + } + + override DataFlow::Node getLDAPNode() { result = ldapNode } + + override string getLDAPPart() { result = ldapPart } + + override DataFlow::Node getAttrs() { result = attrs } + } + + private class LDAP2EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range { + DataFlow::Node escapeNode; + + LDAP2EscapeDN() { + this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall() and + escapeNode = this.getArg(0) + } + + override DataFlow::Node getEscapeNode() { result = escapeNode } + } + } +} diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll new file mode 100644 index 00000000000..f5fa2306f31 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll @@ -0,0 +1,21 @@ +/** + * Provides a taint-tracking configuration for detecting LDAP injection vulnerabilities + */ + +import python +import experimental.semmle.python.Concepts +import semmle.python.dataflow.new.DataFlow +import semmle.python.dataflow.new.TaintTracking +import semmle.python.dataflow.new.RemoteFlowSources + +/** + * A taint-tracking configuration for detecting regular expression injections. + */ +class LDAPInjectionFlowConfig extends TaintTracking::Configuration { + LDAPInjectionFlowConfig() { this = "LDAPInjectionFlowConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery lQ).getLDAPNode() } + // override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof RemoteFlowSource } // any(LDAPEscape ldapEsc).getEscapeNode() } +} From 093c63ea3b15cd33b08b8bd1d1058ffc3c6dd075 Mon Sep 17 00:00:00 2001 From: ihsinme Date: Sun, 28 Mar 2021 23:42:36 +0300 Subject: [PATCH 0106/1429] Update OperatorPrecedenceLogicErrorWhenUseBoolType.expected --- .../tests/OperatorPrecedenceLogicErrorWhenUseBoolType.expected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.expected b/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.expected index 76062fc360a..1209c7e7830 100644 --- a/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.expected +++ b/cpp/ql/test/experimental/query-tests/Security/CWE/CWE-788/semmle/tests/OperatorPrecedenceLogicErrorWhenUseBoolType.expected @@ -1,4 +1,4 @@ -| test.cpp:10:3:10:10 | ... = ... | this expression needs attention | +| test.cpp:10:8:10:10 | - ... | this expression needs attention | | test.cpp:12:3:12:6 | ... ++ | this expression needs attention | | test.cpp:13:3:13:6 | ++ ... | this expression needs attention | | test.cpp:14:6:14:21 | ... = ... | this expression needs attention | From 3f215d0954ea56c34f90837e259553be82675f76 Mon Sep 17 00:00:00 2001 From: ihsinme Date: Sun, 28 Mar 2021 23:43:22 +0300 Subject: [PATCH 0107/1429] Update OperatorPrecedenceLogicErrorWhenUseBoolType.ql --- ...ratorPrecedenceLogicErrorWhenUseBoolType.ql | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql index 034df703bc3..4f30f112eb0 100644 --- a/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql +++ b/cpp/ql/src/experimental/Security/CWE/CWE-783/OperatorPrecedenceLogicErrorWhenUseBoolType.ql @@ -13,17 +13,17 @@ */ import cpp -import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis +import semmle.code.cpp.valuenumbering.HashCons /** Holds if `exp` increments a boolean value. */ -predicate incrementBoolType(Expr exp) { - exp.(IncrementOperation).getOperand().getType() instanceof BoolType +predicate incrementBoolType(IncrementOperation exp) { + exp.getOperand().getType() instanceof BoolType } /** Holds if `exp` applies the unary minus operator to a boolean type. */ -predicate revertSignBoolType(Expr exp) { - exp.(AssignExpr).getRValue().(UnaryMinusExpr).getAnOperand().getType() instanceof BoolType and - exp.(AssignExpr).getLValue().getType() instanceof BoolType +predicate revertSignBoolType(UnaryMinusExpr exp) { + exp.getAnOperand().getType() instanceof BoolType and + exp.getFullyConverted().getType() instanceof BoolType } /** Holds, if this is an expression, uses comparison and assignment outside of execution precedence. */ @@ -33,6 +33,12 @@ predicate assignBoolType(Expr exp) { exp.isCondition() and not co.isParenthesised() and not exp.(AssignExpr).getLValue().getType() instanceof BoolType and + not exists(Expr exbl | + hashCons(exbl.(AssignExpr).getLValue()) = hashCons(exp.(AssignExpr).getLValue()) and + not exbl.isCondition() and + exbl.(AssignExpr).getRValue().getType() instanceof BoolType and + exbl.(AssignExpr).getLValue().getType() = exp.(AssignExpr).getLValue().getType() + ) and co.getLeftOperand() instanceof FunctionCall and not co.getRightOperand().getType() instanceof BoolType and not co.getRightOperand().getValue() = "0" and From 0775d35591511db2003b3b7453c6bfc2284ba250 Mon Sep 17 00:00:00 2001 From: haby0 Date: Mon, 29 Mar 2021 12:02:37 +0800 Subject: [PATCH 0108/1429] update VerificationMethodFlowConfig, add if test --- .../Security/CWE/CWE-352/JsonpInjection.java | 35 ++-- .../Security/CWE/CWE-352/JsonpInjection.qhelp | 2 +- .../Security/CWE/CWE-352/JsonpInjection.ql | 27 ++-- .../CWE/CWE-352/JsonpInjectionLib.qll | 47 ++++-- .../JsonpController.java | 35 ++-- .../JsonpInjection.expected | 138 ++++++++-------- .../JsonpController.java | 37 +++-- .../JsonpInjection.expected | 147 ++++++++--------- .../JsonpController.java | 37 +++-- .../JsonpInjection.expected | 150 +++++++++--------- 10 files changed, 362 insertions(+), 293 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java index 7f479a8c023..8e2a0c9005f 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.java @@ -26,9 +26,6 @@ public class JsonpInjection { hashMap.put("password","123456"); } - private String name = null; - - @GetMapping(value = "jsonp1") @ResponseBody public String bad1(HttpServletRequest request) { @@ -77,7 +74,6 @@ public class JsonpInjection { PrintWriter pw = null; Gson gson = new Gson(); String result = gson.toJson(hashMap); - String resultStr = null; pw = response.getWriter(); resultStr = jsonpCallback + "(" + result + ")"; @@ -109,13 +105,25 @@ public class JsonpInjection { return resultStr; } - @GetMapping(value = "jsonp8") @ResponseBody + public String bad8(HttpServletRequest request) { + String resultStr = null; + String token = request.getParameter("token"); + boolean result = verifToken(token); //Just check. + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + + @GetMapping(value = "jsonp9") + @ResponseBody public String good1(HttpServletRequest request) { String resultStr = null; - String token = request.getParameter("token"); - if (verifToken(token)){ + String referer = request.getParameter("referer"); + if (verifReferer(referer)){ String jsonpCallback = request.getParameter("jsonpCallback"); String jsonStr = getJsonStr(hashMap); resultStr = jsonpCallback + "(" + jsonStr + ")"; @@ -125,7 +133,7 @@ public class JsonpInjection { } - @GetMapping(value = "jsonp9") + @GetMapping(value = "jsonp10") @ResponseBody public String good2(HttpServletRequest request) { String resultStr = null; @@ -140,7 +148,7 @@ public class JsonpInjection { return resultStr; } - @RequestMapping(value = "jsonp10") + @RequestMapping(value = "jsonp11") @ResponseBody public String good3(HttpServletRequest request) { JSONObject parameterObj = readToJSONObect(request); @@ -151,7 +159,7 @@ public class JsonpInjection { return resultStr; } - @RequestMapping(value = "jsonp11") + @RequestMapping(value = "jsonp12") @ResponseBody public String good4(@RequestParam("file") MultipartFile file,HttpServletRequest request) { if(null == file){ @@ -200,4 +208,11 @@ public class JsonpInjection { } return true; } + + public static boolean verifReferer(String str){ + if (str != "xxxx"){ + return false; + } + return true; + } } \ No newline at end of file diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp index 93c167d6c2c..2712f30a3f2 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.qhelp @@ -14,7 +14,7 @@ When there is a cross-domain problem, the problem of sensitive information leaka -

    The following examples show the bad case and the good case respectively. Bad case, such as bad1 to bad7, +

    The following examples show the bad case and the good case respectively. Bad case, such as bad1 to bad8, will cause information leakage problems when there are cross-domain problems. In a good case, for example, in the good1 method and the good2 method, use the verifToken method to do the random token Verification can solve the problem of information leakage caused by cross-domain.

    diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql index eb4fe4e5b66..6e333d83993 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjection.ql @@ -18,20 +18,18 @@ import DataFlow::PathGraph /** Determine whether there is a verification method for the remote streaming source data flow path method. */ predicate existsFilterVerificationMethod() { - exists(MethodAccess ma, Node existsNode, Method m | - ma.getMethod() instanceof VerificationMethodClass and - existsNode.asExpr() = ma and - m = getACallingCallableOrSelf(existsNode.getEnclosingCallable()) and + exists(DataFlow::Node source, DataFlow::Node sink, VerificationMethodFlowConfig vmfc, Method m | + vmfc.hasFlow(source, sink) and + m = getACallingCallableOrSelf(source.getEnclosingCallable()) and isDoFilterMethod(m) ) } /** Determine whether there is a verification method for the remote streaming source data flow path method. */ predicate existsServletVerificationMethod(Node checkNode) { - exists(MethodAccess ma, Node existsNode | - ma.getMethod() instanceof VerificationMethodClass and - existsNode.asExpr() = ma and - getACallingCallableOrSelf(existsNode.getEnclosingCallable()) = + exists(DataFlow::Node source, DataFlow::Node sink, VerificationMethodFlowConfig vmfc | + vmfc.hasFlow(source, sink) and + getACallingCallableOrSelf(source.getEnclosingCallable()) = getACallingCallableOrSelf(checkNode.getEnclosingCallable()) ) } @@ -40,13 +38,14 @@ predicate existsServletVerificationMethod(Node checkNode) { class RequestResponseFlowConfig extends TaintTracking::Configuration { RequestResponseFlowConfig() { this = "RequestResponseFlowConfig" } - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + override predicate isSource(DataFlow::Node source) { + source instanceof RemoteFlowSource and + getACallingCallableOrSelf(source.getEnclosingCallable()) instanceof RequestGetMethod + } - override predicate isSink(DataFlow::Node sink) { sink instanceof XssSink } - - /** Eliminate the method of calling the node is not the get method. */ - override predicate isSanitizer(DataFlow::Node node) { - not getACallingCallableOrSelf(node.getEnclosingCallable()) instanceof RequestGetMethod + override predicate isSink(DataFlow::Node sink) { + sink instanceof XssSink and + getACallingCallableOrSelf(sink.getEnclosingCallable()) instanceof RequestGetMethod } override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { diff --git a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll index d0e00bcb634..bf90926f72f 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-352/JsonpInjectionLib.qll @@ -3,30 +3,47 @@ import DataFlow import JsonStringLib import semmle.code.java.security.XSS import semmle.code.java.dataflow.DataFlow +import semmle.code.java.dataflow.DataFlow3 import semmle.code.java.dataflow.FlowSources import semmle.code.java.frameworks.spring.SpringController +/** A data flow configuration is tracing flow from the access to the authentication method of token/auth/referer/origin to if condition. */ +class VerificationMethodToIfFlowConfig extends DataFlow3::Configuration { + VerificationMethodToIfFlowConfig() { this = "VerificationMethodToIfFlowConfig" } + + override predicate isSource(DataFlow::Node src) { + exists(MethodAccess ma, BarrierGuard bg | ma = bg | + ( + ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") + or + ma.getMethod().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") + ) and + ma = src.asExpr() + ) + } + + override predicate isSink(DataFlow::Node sink) { + exists(IfStmt is | is.getCondition() = sink.asExpr()) + } +} + /** Taint-tracking configuration tracing flow from untrusted inputs to verification of remote user input. */ -class VerificationMethodFlowConfig extends TaintTracking::Configuration { +class VerificationMethodFlowConfig extends TaintTracking2::Configuration { VerificationMethodFlowConfig() { this = "VerificationMethodFlowConfig" } override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { - exists(MethodAccess ma | - ma.getMethod().getAParameter().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") and - ma.getAnArgument() = sink.asExpr() - ) - } -} - -/** The parameter names of this method are token/auth/referer/origin. */ -class VerificationMethodClass extends Method { - VerificationMethodClass() { - exists(MethodAccess ma, VerificationMethodFlowConfig vmfc, Node node | - this = ma.getMethod() and - node.asExpr() = ma.getAnArgument() and - vmfc.hasFlowTo(node) + exists(MethodAccess ma, BarrierGuard bg, int i, VerificationMethodToIfFlowConfig vmtifc | + ma = bg + | + ( + ma.getMethod().getParameter(i).getName().regexpMatch("(?i).*(token|auth|referer|origin).*") + or + ma.getMethod().getName().regexpMatch("(?i).*(token|auth|referer|origin).*") + ) and + ma.getArgument(i) = sink.asExpr() and + vmtifc.hasFlow(exprNode(ma), _) ) } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpController.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpController.java index e5b5e70a38d..e875da2f699 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpController.java +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpController.java @@ -26,9 +26,6 @@ public class JsonpController { hashMap.put("password","123456"); } - private String name = null; - - @GetMapping(value = "jsonp1") @ResponseBody public String bad1(HttpServletRequest request) { @@ -77,7 +74,6 @@ public class JsonpController { PrintWriter pw = null; Gson gson = new Gson(); String result = gson.toJson(hashMap); - String resultStr = null; pw = response.getWriter(); resultStr = jsonpCallback + "(" + result + ")"; @@ -109,13 +105,25 @@ public class JsonpController { return resultStr; } - @GetMapping(value = "jsonp8") @ResponseBody + public String bad8(HttpServletRequest request) { + String resultStr = null; + String token = request.getParameter("token"); + boolean result = verifToken(token); //Just check. + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + + @GetMapping(value = "jsonp9") + @ResponseBody public String good1(HttpServletRequest request) { String resultStr = null; - String token = request.getParameter("token"); - if (verifToken(token)){ + String referer = request.getParameter("referer"); + if (verifReferer(referer)){ String jsonpCallback = request.getParameter("jsonpCallback"); String jsonStr = getJsonStr(hashMap); resultStr = jsonpCallback + "(" + jsonStr + ")"; @@ -125,7 +133,7 @@ public class JsonpController { } - @GetMapping(value = "jsonp9") + @GetMapping(value = "jsonp10") @ResponseBody public String good2(HttpServletRequest request) { String resultStr = null; @@ -140,7 +148,7 @@ public class JsonpController { return resultStr; } - @RequestMapping(value = "jsonp10") + @RequestMapping(value = "jsonp11") @ResponseBody public String good3(HttpServletRequest request) { JSONObject parameterObj = readToJSONObect(request); @@ -151,7 +159,7 @@ public class JsonpController { return resultStr; } - @RequestMapping(value = "jsonp11") + @RequestMapping(value = "jsonp12") @ResponseBody public String good4(@RequestParam("file") MultipartFile file,HttpServletRequest request) { if(null == file){ @@ -200,4 +208,11 @@ public class JsonpController { } return true; } + + public static boolean verifReferer(String str){ + if (str != "xxxx"){ + return false; + } + return true; + } } diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.expected index 501565f2b4e..3da805c6a69 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithFilter/JsonpInjection.expected @@ -1,80 +1,76 @@ edges -| JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | -| JsonpController.java:39:21:39:54 | ... + ... : String | JsonpController.java:40:16:40:24 | resultStr | -| JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | -| JsonpController.java:48:21:48:80 | ... + ... : String | JsonpController.java:49:16:49:24 | resultStr | -| JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | -| JsonpController.java:58:21:58:55 | ... + ... : String | JsonpController.java:59:16:59:24 | resultStr | -| JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | -| JsonpController.java:68:21:68:54 | ... + ... : String | JsonpController.java:69:16:69:24 | resultStr | -| JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | -| JsonpController.java:83:21:83:54 | ... + ... : String | JsonpController.java:84:20:84:28 | resultStr | -| JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | -| JsonpController.java:97:21:97:54 | ... + ... : String | JsonpController.java:98:20:98:28 | resultStr | -| JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | -| JsonpController.java:108:21:108:54 | ... + ... : String | JsonpController.java:109:16:109:24 | resultStr | -| JsonpController.java:117:24:117:52 | getParameter(...) : String | JsonpController.java:118:24:118:28 | token | -| JsonpController.java:119:36:119:72 | getParameter(...) : String | JsonpController.java:122:20:122:28 | resultStr | -| JsonpController.java:121:25:121:59 | ... + ... : String | JsonpController.java:122:20:122:28 | resultStr | -| JsonpController.java:132:24:132:52 | getParameter(...) : String | JsonpController.java:133:37:133:41 | token | -| JsonpController.java:137:32:137:68 | getParameter(...) : String | JsonpController.java:140:16:140:24 | resultStr | -| JsonpController.java:139:21:139:55 | ... + ... : String | JsonpController.java:140:16:140:24 | resultStr | -| JsonpController.java:150:21:150:54 | ... + ... : String | JsonpController.java:151:16:151:24 | resultStr | -| JsonpController.java:165:21:165:54 | ... + ... : String | JsonpController.java:166:16:166:24 | resultStr | +| JsonpController.java:33:32:33:68 | getParameter(...) : String | JsonpController.java:37:16:37:24 | resultStr | +| JsonpController.java:36:21:36:54 | ... + ... : String | JsonpController.java:37:16:37:24 | resultStr | +| JsonpController.java:44:32:44:68 | getParameter(...) : String | JsonpController.java:46:16:46:24 | resultStr | +| JsonpController.java:45:21:45:80 | ... + ... : String | JsonpController.java:46:16:46:24 | resultStr | +| JsonpController.java:53:32:53:68 | getParameter(...) : String | JsonpController.java:56:16:56:24 | resultStr | +| JsonpController.java:55:21:55:55 | ... + ... : String | JsonpController.java:56:16:56:24 | resultStr | +| JsonpController.java:63:32:63:68 | getParameter(...) : String | JsonpController.java:66:16:66:24 | resultStr | +| JsonpController.java:65:21:65:54 | ... + ... : String | JsonpController.java:66:16:66:24 | resultStr | +| JsonpController.java:73:32:73:68 | getParameter(...) : String | JsonpController.java:80:20:80:28 | resultStr | +| JsonpController.java:79:21:79:54 | ... + ... : String | JsonpController.java:80:20:80:28 | resultStr | +| JsonpController.java:87:32:87:68 | getParameter(...) : String | JsonpController.java:94:20:94:28 | resultStr | +| JsonpController.java:93:21:93:54 | ... + ... : String | JsonpController.java:94:20:94:28 | resultStr | +| JsonpController.java:101:32:101:68 | getParameter(...) : String | JsonpController.java:105:16:105:24 | resultStr | +| JsonpController.java:104:21:104:54 | ... + ... : String | JsonpController.java:105:16:105:24 | resultStr | +| JsonpController.java:114:32:114:68 | getParameter(...) : String | JsonpController.java:117:16:117:24 | resultStr | +| JsonpController.java:116:21:116:55 | ... + ... : String | JsonpController.java:117:16:117:24 | resultStr | +| JsonpController.java:127:36:127:72 | getParameter(...) : String | JsonpController.java:130:20:130:28 | resultStr | +| JsonpController.java:129:25:129:59 | ... + ... : String | JsonpController.java:130:20:130:28 | resultStr | +| JsonpController.java:145:32:145:68 | getParameter(...) : String | JsonpController.java:148:16:148:24 | resultStr | +| JsonpController.java:147:21:147:55 | ... + ... : String | JsonpController.java:148:16:148:24 | resultStr | +| JsonpController.java:158:21:158:54 | ... + ... : String | JsonpController.java:159:16:159:24 | resultStr | +| JsonpController.java:173:21:173:54 | ... + ... : String | JsonpController.java:174:16:174:24 | resultStr | | JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | -| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | JsonpInjectionServlet1.java:38:39:38:45 | referer | | JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | | JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | -| RefererFilter.java:22:26:22:53 | getHeader(...) : String | RefererFilter.java:23:39:23:45 | refefer | nodes -| JsonpController.java:36:32:36:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:39:21:39:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:47:32:47:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:48:21:48:80 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:56:32:56:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:58:21:58:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:66:32:66:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:68:21:68:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:76:32:76:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:83:21:83:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:91:32:91:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:97:21:97:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:105:32:105:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:108:21:108:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:117:24:117:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:118:24:118:28 | token | semmle.label | token | -| JsonpController.java:119:36:119:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:121:25:121:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:132:24:132:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:133:37:133:41 | token | semmle.label | token | -| JsonpController.java:137:32:137:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:139:21:139:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:150:21:150:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:151:16:151:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:165:21:165:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:166:16:166:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:33:32:33:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:36:21:36:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:37:16:37:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:37:16:37:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:44:32:44:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:45:21:45:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:46:16:46:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:46:16:46:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:53:32:53:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:55:21:55:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:56:16:56:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:56:16:56:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:63:32:63:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:65:21:65:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:66:16:66:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:66:16:66:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:73:32:73:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:79:21:79:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:87:32:87:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:93:21:93:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:94:20:94:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:94:20:94:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:101:32:101:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:104:21:104:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:105:16:105:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:105:16:105:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:114:32:114:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:116:21:116:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:117:16:117:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:117:16:117:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:127:36:127:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:129:25:129:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:130:20:130:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:130:20:130:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:145:32:145:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:147:21:147:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:148:16:148:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:148:16:148:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:158:21:158:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:159:16:159:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:173:21:173:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:174:16:174:24 | resultStr | semmle.label | resultStr | | JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | semmle.label | getHeader(...) : String | -| JsonpInjectionServlet1.java:38:39:38:45 | referer | semmle.label | referer | | JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | semmle.label | ... + ... : String | | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | @@ -82,6 +78,4 @@ nodes | JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | semmle.label | ... + ... : String | | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | -| RefererFilter.java:22:26:22:53 | getHeader(...) : String | semmle.label | getHeader(...) : String | -| RefererFilter.java:23:39:23:45 | refefer | semmle.label | refefer | #select diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpController.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpController.java index e5b5e70a38d..4c60b356cfb 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpController.java +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpController.java @@ -26,9 +26,6 @@ public class JsonpController { hashMap.put("password","123456"); } - private String name = null; - - @GetMapping(value = "jsonp1") @ResponseBody public String bad1(HttpServletRequest request) { @@ -77,7 +74,6 @@ public class JsonpController { PrintWriter pw = null; Gson gson = new Gson(); String result = gson.toJson(hashMap); - String resultStr = null; pw = response.getWriter(); resultStr = jsonpCallback + "(" + result + ")"; @@ -109,13 +105,25 @@ public class JsonpController { return resultStr; } - @GetMapping(value = "jsonp8") @ResponseBody + public String bad8(HttpServletRequest request) { + String resultStr = null; + String token = request.getParameter("token"); + boolean result = verifToken(token); //Just check. + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + + @GetMapping(value = "jsonp9") + @ResponseBody public String good1(HttpServletRequest request) { String resultStr = null; - String token = request.getParameter("token"); - if (verifToken(token)){ + String referer = request.getParameter("referer"); + if (verifReferer(referer)){ String jsonpCallback = request.getParameter("jsonpCallback"); String jsonStr = getJsonStr(hashMap); resultStr = jsonpCallback + "(" + jsonStr + ")"; @@ -125,7 +133,7 @@ public class JsonpController { } - @GetMapping(value = "jsonp9") + @GetMapping(value = "jsonp10") @ResponseBody public String good2(HttpServletRequest request) { String resultStr = null; @@ -140,7 +148,7 @@ public class JsonpController { return resultStr; } - @RequestMapping(value = "jsonp10") + @RequestMapping(value = "jsonp11") @ResponseBody public String good3(HttpServletRequest request) { JSONObject parameterObj = readToJSONObect(request); @@ -151,7 +159,7 @@ public class JsonpController { return resultStr; } - @RequestMapping(value = "jsonp11") + @RequestMapping(value = "jsonp12") @ResponseBody public String good4(@RequestParam("file") MultipartFile file,HttpServletRequest request) { if(null == file){ @@ -200,4 +208,11 @@ public class JsonpController { } return true; } -} + + public static boolean verifReferer(String str){ + if (str != "xxxx"){ + return false; + } + return true; + } +} \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.expected index 91d23cebbda..2e4bc97ff97 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringController/JsonpInjection.expected @@ -1,76 +1,77 @@ edges -| JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | -| JsonpController.java:39:21:39:54 | ... + ... : String | JsonpController.java:40:16:40:24 | resultStr | -| JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | -| JsonpController.java:48:21:48:80 | ... + ... : String | JsonpController.java:49:16:49:24 | resultStr | -| JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | -| JsonpController.java:58:21:58:55 | ... + ... : String | JsonpController.java:59:16:59:24 | resultStr | -| JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | -| JsonpController.java:68:21:68:54 | ... + ... : String | JsonpController.java:69:16:69:24 | resultStr | -| JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | -| JsonpController.java:83:21:83:54 | ... + ... : String | JsonpController.java:84:20:84:28 | resultStr | -| JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | -| JsonpController.java:97:21:97:54 | ... + ... : String | JsonpController.java:98:20:98:28 | resultStr | -| JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | -| JsonpController.java:108:21:108:54 | ... + ... : String | JsonpController.java:109:16:109:24 | resultStr | -| JsonpController.java:117:24:117:52 | getParameter(...) : String | JsonpController.java:118:24:118:28 | token | -| JsonpController.java:119:36:119:72 | getParameter(...) : String | JsonpController.java:122:20:122:28 | resultStr | -| JsonpController.java:121:25:121:59 | ... + ... : String | JsonpController.java:122:20:122:28 | resultStr | -| JsonpController.java:132:24:132:52 | getParameter(...) : String | JsonpController.java:133:37:133:41 | token | -| JsonpController.java:137:32:137:68 | getParameter(...) : String | JsonpController.java:140:16:140:24 | resultStr | -| JsonpController.java:139:21:139:55 | ... + ... : String | JsonpController.java:140:16:140:24 | resultStr | -| JsonpController.java:150:21:150:54 | ... + ... : String | JsonpController.java:151:16:151:24 | resultStr | -| JsonpController.java:165:21:165:54 | ... + ... : String | JsonpController.java:166:16:166:24 | resultStr | +| JsonpController.java:33:32:33:68 | getParameter(...) : String | JsonpController.java:37:16:37:24 | resultStr | +| JsonpController.java:36:21:36:54 | ... + ... : String | JsonpController.java:37:16:37:24 | resultStr | +| JsonpController.java:44:32:44:68 | getParameter(...) : String | JsonpController.java:46:16:46:24 | resultStr | +| JsonpController.java:45:21:45:80 | ... + ... : String | JsonpController.java:46:16:46:24 | resultStr | +| JsonpController.java:53:32:53:68 | getParameter(...) : String | JsonpController.java:56:16:56:24 | resultStr | +| JsonpController.java:55:21:55:55 | ... + ... : String | JsonpController.java:56:16:56:24 | resultStr | +| JsonpController.java:63:32:63:68 | getParameter(...) : String | JsonpController.java:66:16:66:24 | resultStr | +| JsonpController.java:65:21:65:54 | ... + ... : String | JsonpController.java:66:16:66:24 | resultStr | +| JsonpController.java:73:32:73:68 | getParameter(...) : String | JsonpController.java:80:20:80:28 | resultStr | +| JsonpController.java:79:21:79:54 | ... + ... : String | JsonpController.java:80:20:80:28 | resultStr | +| JsonpController.java:87:32:87:68 | getParameter(...) : String | JsonpController.java:94:20:94:28 | resultStr | +| JsonpController.java:93:21:93:54 | ... + ... : String | JsonpController.java:94:20:94:28 | resultStr | +| JsonpController.java:101:32:101:68 | getParameter(...) : String | JsonpController.java:105:16:105:24 | resultStr | +| JsonpController.java:104:21:104:54 | ... + ... : String | JsonpController.java:105:16:105:24 | resultStr | +| JsonpController.java:114:32:114:68 | getParameter(...) : String | JsonpController.java:117:16:117:24 | resultStr | +| JsonpController.java:116:21:116:55 | ... + ... : String | JsonpController.java:117:16:117:24 | resultStr | +| JsonpController.java:127:36:127:72 | getParameter(...) : String | JsonpController.java:130:20:130:28 | resultStr | +| JsonpController.java:129:25:129:59 | ... + ... : String | JsonpController.java:130:20:130:28 | resultStr | +| JsonpController.java:145:32:145:68 | getParameter(...) : String | JsonpController.java:148:16:148:24 | resultStr | +| JsonpController.java:147:21:147:55 | ... + ... : String | JsonpController.java:148:16:148:24 | resultStr | +| JsonpController.java:158:21:158:54 | ... + ... : String | JsonpController.java:159:16:159:24 | resultStr | +| JsonpController.java:173:21:173:54 | ... + ... : String | JsonpController.java:174:16:174:24 | resultStr | nodes -| JsonpController.java:36:32:36:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:39:21:39:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:47:32:47:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:48:21:48:80 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:56:32:56:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:58:21:58:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:66:32:66:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:68:21:68:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:76:32:76:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:83:21:83:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:91:32:91:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:97:21:97:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:105:32:105:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:108:21:108:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:117:24:117:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:118:24:118:28 | token | semmle.label | token | -| JsonpController.java:119:36:119:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:121:25:121:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:132:24:132:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:133:37:133:41 | token | semmle.label | token | -| JsonpController.java:137:32:137:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:139:21:139:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:150:21:150:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:151:16:151:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:165:21:165:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:166:16:166:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:33:32:33:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:36:21:36:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:37:16:37:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:37:16:37:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:44:32:44:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:45:21:45:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:46:16:46:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:46:16:46:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:53:32:53:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:55:21:55:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:56:16:56:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:56:16:56:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:63:32:63:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:65:21:65:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:66:16:66:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:66:16:66:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:73:32:73:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:79:21:79:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:87:32:87:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:93:21:93:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:94:20:94:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:94:20:94:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:101:32:101:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:104:21:104:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:105:16:105:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:105:16:105:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:114:32:114:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:116:21:116:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:117:16:117:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:117:16:117:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:127:36:127:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:129:25:129:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:130:20:130:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:130:20:130:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:145:32:145:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:147:21:147:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:148:16:148:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:148:16:148:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:158:21:158:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:159:16:159:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:173:21:173:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:174:16:174:24 | resultStr | semmle.label | resultStr | #select -| JsonpController.java:40:16:40:24 | resultStr | JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:36:32:36:68 | getParameter(...) | this user input | -| JsonpController.java:49:16:49:24 | resultStr | JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:47:32:47:68 | getParameter(...) | this user input | -| JsonpController.java:59:16:59:24 | resultStr | JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:56:32:56:68 | getParameter(...) | this user input | -| JsonpController.java:69:16:69:24 | resultStr | JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:66:32:66:68 | getParameter(...) | this user input | -| JsonpController.java:84:20:84:28 | resultStr | JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:76:32:76:68 | getParameter(...) | this user input | -| JsonpController.java:98:20:98:28 | resultStr | JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:91:32:91:68 | getParameter(...) | this user input | -| JsonpController.java:109:16:109:24 | resultStr | JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:105:32:105:68 | getParameter(...) | this user input | +| JsonpController.java:37:16:37:24 | resultStr | JsonpController.java:33:32:33:68 | getParameter(...) : String | JsonpController.java:37:16:37:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:33:32:33:68 | getParameter(...) | this user input | +| JsonpController.java:46:16:46:24 | resultStr | JsonpController.java:44:32:44:68 | getParameter(...) : String | JsonpController.java:46:16:46:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:44:32:44:68 | getParameter(...) | this user input | +| JsonpController.java:56:16:56:24 | resultStr | JsonpController.java:53:32:53:68 | getParameter(...) : String | JsonpController.java:56:16:56:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:53:32:53:68 | getParameter(...) | this user input | +| JsonpController.java:66:16:66:24 | resultStr | JsonpController.java:63:32:63:68 | getParameter(...) : String | JsonpController.java:66:16:66:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:63:32:63:68 | getParameter(...) | this user input | +| JsonpController.java:80:20:80:28 | resultStr | JsonpController.java:73:32:73:68 | getParameter(...) : String | JsonpController.java:80:20:80:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:73:32:73:68 | getParameter(...) | this user input | +| JsonpController.java:94:20:94:28 | resultStr | JsonpController.java:87:32:87:68 | getParameter(...) : String | JsonpController.java:94:20:94:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:87:32:87:68 | getParameter(...) | this user input | +| JsonpController.java:105:16:105:24 | resultStr | JsonpController.java:101:32:101:68 | getParameter(...) : String | JsonpController.java:105:16:105:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:101:32:101:68 | getParameter(...) | this user input | +| JsonpController.java:117:16:117:24 | resultStr | JsonpController.java:114:32:114:68 | getParameter(...) : String | JsonpController.java:117:16:117:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:114:32:114:68 | getParameter(...) | this user input | diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpController.java b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpController.java index e5b5e70a38d..4c60b356cfb 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpController.java +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpController.java @@ -26,9 +26,6 @@ public class JsonpController { hashMap.put("password","123456"); } - private String name = null; - - @GetMapping(value = "jsonp1") @ResponseBody public String bad1(HttpServletRequest request) { @@ -77,7 +74,6 @@ public class JsonpController { PrintWriter pw = null; Gson gson = new Gson(); String result = gson.toJson(hashMap); - String resultStr = null; pw = response.getWriter(); resultStr = jsonpCallback + "(" + result + ")"; @@ -109,13 +105,25 @@ public class JsonpController { return resultStr; } - @GetMapping(value = "jsonp8") @ResponseBody + public String bad8(HttpServletRequest request) { + String resultStr = null; + String token = request.getParameter("token"); + boolean result = verifToken(token); //Just check. + String jsonpCallback = request.getParameter("jsonpCallback"); + String jsonStr = getJsonStr(hashMap); + resultStr = jsonpCallback + "(" + jsonStr + ")"; + return resultStr; + } + + + @GetMapping(value = "jsonp9") + @ResponseBody public String good1(HttpServletRequest request) { String resultStr = null; - String token = request.getParameter("token"); - if (verifToken(token)){ + String referer = request.getParameter("referer"); + if (verifReferer(referer)){ String jsonpCallback = request.getParameter("jsonpCallback"); String jsonStr = getJsonStr(hashMap); resultStr = jsonpCallback + "(" + jsonStr + ")"; @@ -125,7 +133,7 @@ public class JsonpController { } - @GetMapping(value = "jsonp9") + @GetMapping(value = "jsonp10") @ResponseBody public String good2(HttpServletRequest request) { String resultStr = null; @@ -140,7 +148,7 @@ public class JsonpController { return resultStr; } - @RequestMapping(value = "jsonp10") + @RequestMapping(value = "jsonp11") @ResponseBody public String good3(HttpServletRequest request) { JSONObject parameterObj = readToJSONObect(request); @@ -151,7 +159,7 @@ public class JsonpController { return resultStr; } - @RequestMapping(value = "jsonp11") + @RequestMapping(value = "jsonp12") @ResponseBody public String good4(@RequestParam("file") MultipartFile file,HttpServletRequest request) { if(null == file){ @@ -200,4 +208,11 @@ public class JsonpController { } return true; } -} + + public static boolean verifReferer(String str){ + if (str != "xxxx"){ + return false; + } + return true; + } +} \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.expected index c2bcab77d4d..d90d51ab552 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-352/JsonpInjectionWithSpringControllerAndServlet/JsonpInjection.expected @@ -1,79 +1,76 @@ edges -| JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | -| JsonpController.java:39:21:39:54 | ... + ... : String | JsonpController.java:40:16:40:24 | resultStr | -| JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | -| JsonpController.java:48:21:48:80 | ... + ... : String | JsonpController.java:49:16:49:24 | resultStr | -| JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | -| JsonpController.java:58:21:58:55 | ... + ... : String | JsonpController.java:59:16:59:24 | resultStr | -| JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | -| JsonpController.java:68:21:68:54 | ... + ... : String | JsonpController.java:69:16:69:24 | resultStr | -| JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | -| JsonpController.java:83:21:83:54 | ... + ... : String | JsonpController.java:84:20:84:28 | resultStr | -| JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | -| JsonpController.java:97:21:97:54 | ... + ... : String | JsonpController.java:98:20:98:28 | resultStr | -| JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | -| JsonpController.java:108:21:108:54 | ... + ... : String | JsonpController.java:109:16:109:24 | resultStr | -| JsonpController.java:117:24:117:52 | getParameter(...) : String | JsonpController.java:118:24:118:28 | token | -| JsonpController.java:119:36:119:72 | getParameter(...) : String | JsonpController.java:122:20:122:28 | resultStr | -| JsonpController.java:121:25:121:59 | ... + ... : String | JsonpController.java:122:20:122:28 | resultStr | -| JsonpController.java:132:24:132:52 | getParameter(...) : String | JsonpController.java:133:37:133:41 | token | -| JsonpController.java:137:32:137:68 | getParameter(...) : String | JsonpController.java:140:16:140:24 | resultStr | -| JsonpController.java:139:21:139:55 | ... + ... : String | JsonpController.java:140:16:140:24 | resultStr | -| JsonpController.java:150:21:150:54 | ... + ... : String | JsonpController.java:151:16:151:24 | resultStr | -| JsonpController.java:165:21:165:54 | ... + ... : String | JsonpController.java:166:16:166:24 | resultStr | +| JsonpController.java:33:32:33:68 | getParameter(...) : String | JsonpController.java:37:16:37:24 | resultStr | +| JsonpController.java:36:21:36:54 | ... + ... : String | JsonpController.java:37:16:37:24 | resultStr | +| JsonpController.java:44:32:44:68 | getParameter(...) : String | JsonpController.java:46:16:46:24 | resultStr | +| JsonpController.java:45:21:45:80 | ... + ... : String | JsonpController.java:46:16:46:24 | resultStr | +| JsonpController.java:53:32:53:68 | getParameter(...) : String | JsonpController.java:56:16:56:24 | resultStr | +| JsonpController.java:55:21:55:55 | ... + ... : String | JsonpController.java:56:16:56:24 | resultStr | +| JsonpController.java:63:32:63:68 | getParameter(...) : String | JsonpController.java:66:16:66:24 | resultStr | +| JsonpController.java:65:21:65:54 | ... + ... : String | JsonpController.java:66:16:66:24 | resultStr | +| JsonpController.java:73:32:73:68 | getParameter(...) : String | JsonpController.java:80:20:80:28 | resultStr | +| JsonpController.java:79:21:79:54 | ... + ... : String | JsonpController.java:80:20:80:28 | resultStr | +| JsonpController.java:87:32:87:68 | getParameter(...) : String | JsonpController.java:94:20:94:28 | resultStr | +| JsonpController.java:93:21:93:54 | ... + ... : String | JsonpController.java:94:20:94:28 | resultStr | +| JsonpController.java:101:32:101:68 | getParameter(...) : String | JsonpController.java:105:16:105:24 | resultStr | +| JsonpController.java:104:21:104:54 | ... + ... : String | JsonpController.java:105:16:105:24 | resultStr | +| JsonpController.java:114:32:114:68 | getParameter(...) : String | JsonpController.java:117:16:117:24 | resultStr | +| JsonpController.java:116:21:116:55 | ... + ... : String | JsonpController.java:117:16:117:24 | resultStr | +| JsonpController.java:127:36:127:72 | getParameter(...) : String | JsonpController.java:130:20:130:28 | resultStr | +| JsonpController.java:129:25:129:59 | ... + ... : String | JsonpController.java:130:20:130:28 | resultStr | +| JsonpController.java:145:32:145:68 | getParameter(...) : String | JsonpController.java:148:16:148:24 | resultStr | +| JsonpController.java:147:21:147:55 | ... + ... : String | JsonpController.java:148:16:148:24 | resultStr | +| JsonpController.java:158:21:158:54 | ... + ... : String | JsonpController.java:159:16:159:24 | resultStr | +| JsonpController.java:173:21:173:54 | ... + ... : String | JsonpController.java:174:16:174:24 | resultStr | | JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | -| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | JsonpInjectionServlet1.java:38:39:38:45 | referer | | JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | | JsonpInjectionServlet2.java:38:21:38:54 | ... + ... : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | nodes -| JsonpController.java:36:32:36:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:39:21:39:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:40:16:40:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:47:32:47:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:48:21:48:80 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:49:16:49:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:56:32:56:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:58:21:58:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:59:16:59:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:66:32:66:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:68:21:68:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:69:16:69:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:76:32:76:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:83:21:83:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:84:20:84:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:91:32:91:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:97:21:97:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:98:20:98:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:105:32:105:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:108:21:108:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:109:16:109:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:117:24:117:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:118:24:118:28 | token | semmle.label | token | -| JsonpController.java:119:36:119:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:121:25:121:59 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:122:20:122:28 | resultStr | semmle.label | resultStr | -| JsonpController.java:132:24:132:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:133:37:133:41 | token | semmle.label | token | -| JsonpController.java:137:32:137:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpController.java:139:21:139:55 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:140:16:140:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:150:21:150:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:151:16:151:24 | resultStr | semmle.label | resultStr | -| JsonpController.java:165:21:165:54 | ... + ... : String | semmle.label | ... + ... : String | -| JsonpController.java:166:16:166:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:33:32:33:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:36:21:36:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:37:16:37:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:37:16:37:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:44:32:44:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:45:21:45:80 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:46:16:46:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:46:16:46:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:53:32:53:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:55:21:55:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:56:16:56:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:56:16:56:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:63:32:63:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:65:21:65:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:66:16:66:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:66:16:66:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:73:32:73:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:79:21:79:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:80:20:80:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:87:32:87:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:93:21:93:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:94:20:94:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:94:20:94:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:101:32:101:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:104:21:104:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:105:16:105:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:105:16:105:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:114:32:114:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:116:21:116:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:117:16:117:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:117:16:117:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:127:36:127:72 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:129:25:129:59 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:130:20:130:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:130:20:130:28 | resultStr | semmle.label | resultStr | +| JsonpController.java:145:32:145:68 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| JsonpController.java:147:21:147:55 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:148:16:148:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:148:16:148:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:158:21:158:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:159:16:159:24 | resultStr | semmle.label | resultStr | +| JsonpController.java:173:21:173:54 | ... + ... : String | semmle.label | ... + ... : String | +| JsonpController.java:174:16:174:24 | resultStr | semmle.label | resultStr | | JsonpInjectionServlet1.java:31:32:31:64 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| JsonpInjectionServlet1.java:36:26:36:49 | getHeader(...) : String | semmle.label | getHeader(...) : String | -| JsonpInjectionServlet1.java:38:39:38:45 | referer | semmle.label | referer | | JsonpInjectionServlet1.java:44:25:44:62 | ... + ... : String | semmle.label | ... + ... : String | | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | | JsonpInjectionServlet1.java:45:24:45:32 | resultStr | semmle.label | resultStr | @@ -82,11 +79,12 @@ nodes | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | semmle.label | resultStr | #select -| JsonpController.java:40:16:40:24 | resultStr | JsonpController.java:36:32:36:68 | getParameter(...) : String | JsonpController.java:40:16:40:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:36:32:36:68 | getParameter(...) | this user input | -| JsonpController.java:49:16:49:24 | resultStr | JsonpController.java:47:32:47:68 | getParameter(...) : String | JsonpController.java:49:16:49:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:47:32:47:68 | getParameter(...) | this user input | -| JsonpController.java:59:16:59:24 | resultStr | JsonpController.java:56:32:56:68 | getParameter(...) : String | JsonpController.java:59:16:59:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:56:32:56:68 | getParameter(...) | this user input | -| JsonpController.java:69:16:69:24 | resultStr | JsonpController.java:66:32:66:68 | getParameter(...) : String | JsonpController.java:69:16:69:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:66:32:66:68 | getParameter(...) | this user input | -| JsonpController.java:84:20:84:28 | resultStr | JsonpController.java:76:32:76:68 | getParameter(...) : String | JsonpController.java:84:20:84:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:76:32:76:68 | getParameter(...) | this user input | -| JsonpController.java:98:20:98:28 | resultStr | JsonpController.java:91:32:91:68 | getParameter(...) : String | JsonpController.java:98:20:98:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:91:32:91:68 | getParameter(...) | this user input | -| JsonpController.java:109:16:109:24 | resultStr | JsonpController.java:105:32:105:68 | getParameter(...) : String | JsonpController.java:109:16:109:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:105:32:105:68 | getParameter(...) | this user input | +| JsonpController.java:37:16:37:24 | resultStr | JsonpController.java:33:32:33:68 | getParameter(...) : String | JsonpController.java:37:16:37:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:33:32:33:68 | getParameter(...) | this user input | +| JsonpController.java:46:16:46:24 | resultStr | JsonpController.java:44:32:44:68 | getParameter(...) : String | JsonpController.java:46:16:46:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:44:32:44:68 | getParameter(...) | this user input | +| JsonpController.java:56:16:56:24 | resultStr | JsonpController.java:53:32:53:68 | getParameter(...) : String | JsonpController.java:56:16:56:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:53:32:53:68 | getParameter(...) | this user input | +| JsonpController.java:66:16:66:24 | resultStr | JsonpController.java:63:32:63:68 | getParameter(...) : String | JsonpController.java:66:16:66:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:63:32:63:68 | getParameter(...) | this user input | +| JsonpController.java:80:20:80:28 | resultStr | JsonpController.java:73:32:73:68 | getParameter(...) : String | JsonpController.java:80:20:80:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:73:32:73:68 | getParameter(...) | this user input | +| JsonpController.java:94:20:94:28 | resultStr | JsonpController.java:87:32:87:68 | getParameter(...) : String | JsonpController.java:94:20:94:28 | resultStr | Jsonp response might include code from $@. | JsonpController.java:87:32:87:68 | getParameter(...) | this user input | +| JsonpController.java:105:16:105:24 | resultStr | JsonpController.java:101:32:101:68 | getParameter(...) : String | JsonpController.java:105:16:105:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:101:32:101:68 | getParameter(...) | this user input | +| JsonpController.java:117:16:117:24 | resultStr | JsonpController.java:114:32:114:68 | getParameter(...) : String | JsonpController.java:117:16:117:24 | resultStr | Jsonp response might include code from $@. | JsonpController.java:114:32:114:68 | getParameter(...) | this user input | | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) : String | JsonpInjectionServlet2.java:39:20:39:28 | resultStr | Jsonp response might include code from $@. | JsonpInjectionServlet2.java:31:32:31:64 | getParameter(...) | this user input | From ad36bea9d4ce61f804cdbfedddc5d3bebecdf1f6 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 29 Mar 2021 09:14:35 +0200 Subject: [PATCH 0109/1429] Refactor LDAP3 stuff (untested) --- .../Security/CWE-090/LDAPInjection.ql | 5 +- .../experimental/semmle/python/Concepts.qll | 6 -- .../semmle/python/frameworks/Stdlib.qll | 95 +++++++++++++++++-- .../security/injection/LDAPInjection.qll | 5 +- 4 files changed, 94 insertions(+), 17 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql index 778b771f883..7ce3a118918 100644 --- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql +++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql @@ -19,7 +19,8 @@ from LDAPQuery castedSink where config.hasFlowPath(source, sink) and - castedSink = sink.getNode() + castedSink = sink.getNode() //and +// if exists(castedSink.getAttrs()) then select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@ probably leaking $@.", sink.getNode(), "This", source.getNode(), "a user-provided value", castedSink.getLDAPNode(), - castedSink.getLDAPPart(), castedSink.getAttrList(), "this attribute(s)" + castedSink.getLDAPPart(), castedSink.getAttrs(), "this attribute(s)" diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index e2b278e40da..e0f5c1bdec3 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -15,12 +15,6 @@ private import semmle.python.dataflow.new.TaintTracking private import experimental.semmle.python.Frameworks private import semmle.python.ApiGraphs -/** - * To-Do - * - * LDAPQuery -> collect functions executing a search filter/DN - * LDAPEscape -> collect functions escaping a search filter/DN - */ module LDAPQuery { abstract class Range extends DataFlow::Node { abstract DataFlow::Node getLDAPNode(); diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll index 1ef42edbdbb..aa9013a76c0 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll @@ -30,18 +30,14 @@ private module LDAP { initCall = searchMethod.getObject().getALocalSource() and searchMethod.getAttributeName() instanceof LDAP2QueryMethods and ( + ldapNode = this.getArg(0) and + ldapPart = "DN" + or ( ldapNode = this.getArg(2) or ldapNode = this.getArgByName("filterstr") ) and ldapPart = "search_filter" - or - ldapNode = this.getArg(0) and - ldapPart = "DN" - ) and - ( // what if they're not set? - attrs = this.getArg(3) or - attrs = this.getArgByName("attrlist") ) ) } @@ -50,7 +46,9 @@ private module LDAP { override string getLDAPPart() { result = ldapPart } - override DataFlow::Node getAttrs() { result = attrs } + override DataFlow::Node getAttrs() { + result = this.getArg(3) or result = this.getArgByName("attrlist") + } } private class LDAP2EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range { @@ -63,5 +61,86 @@ private module LDAP { override DataFlow::Node getEscapeNode() { result = escapeNode } } + + private class LDAP2EscapeFilter extends DataFlow::CallCfgNode, LDAPEscape::Range { + DataFlow::Node escapeNode; + + LDAP2EscapeFilter() { + this = + API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall() and + escapeNode = this.getArg(0) + } + + override DataFlow::Node getEscapeNode() { result = escapeNode } + } + } + + private module LDAP3 { + private class LDAP3QueryMethods extends string { + // pending to dig into this although https://github.com/cannatag/ldap3/blob/21001d9087c0d24c399eec433a261c455b7bc97f/ldap3/core/connection.py#L760 + LDAP3QueryMethods() { this in ["search"] } + } + + private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range { + DataFlow::Node ldapNode; + string ldapPart; + DataFlow::Node attrs; + + LDAP3Query() { + exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode connCall | + this.getFunction() = searchMethod and + connCall = API::moduleImport("ldap3").getMember("Connection").getACall() and + connCall = searchMethod.getObject().getALocalSource() and + searchMethod.getAttributeName() instanceof LDAP3QueryMethods and + ( + ldapNode = this.getArg(0) and + ldapPart = "DN" + or + ldapNode = this.getArg(1) and + ldapPart = "search_filter" + ) + ) + } + + override DataFlow::Node getLDAPNode() { result = ldapNode } + + override string getLDAPPart() { result = ldapPart } + + override DataFlow::Node getAttrs() { + result = this.getArg(3) or result = this.getArgByName("attributes") + } + } + + private class LDAP3EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range { + DataFlow::Node escapeNode; + + LDAP3EscapeDN() { + this = + API::moduleImport("ldap3") + .getMember("utils") + .getMember("dn") + .getMember("escape_rdn") + .getACall() and + escapeNode = this.getArg(0) + } + + override DataFlow::Node getEscapeNode() { result = escapeNode } + } + + private class LDAP3EscapeFilter extends DataFlow::CallCfgNode, LDAPEscape::Range { + DataFlow::Node escapeNode; + + LDAP3EscapeFilter() { + this = + API::moduleImport("ldap3") + .getMember("utils") + .getMember("conv") + .getMember("escape_filter_chars") + .getACall() and + escapeNode = this.getArg(0) + } + + override DataFlow::Node getEscapeNode() { result = escapeNode } + } } } diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll index f5fa2306f31..db8bdda7db6 100644 --- a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll +++ b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll @@ -17,5 +17,8 @@ class LDAPInjectionFlowConfig extends TaintTracking::Configuration { override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery lQ).getLDAPNode() } - // override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof RemoteFlowSource } // any(LDAPEscape ldapEsc).getEscapeNode() } + + override predicate isSanitizer(DataFlow::Node sanitizer) { + sanitizer = any(LDAPEscape lE).getEscapeNode() + } } From 8223539f0c0988e4d1ef79710c80c82533b12901 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 29 Mar 2021 23:28:28 +0200 Subject: [PATCH 0110/1429] Add a test without attributes --- .../Security/CWE-090/unit_tests/ldap_bad.py | 10 ++++++++++ .../Security/CWE-090/unit_tests/ldap_good.py | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py index a409a38e019..011f3b82865 100644 --- a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py +++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py @@ -14,6 +14,16 @@ def normal(): unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter, ["testAttr1", "testAttr2"]) +@app.route("/normal_noAttrs") +def normal_noAttrs(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter) + + @app.route("/direct") def direct(): unsafe_dn = "dc=%s" % request.args['dc'] diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py index 1d04505fa28..eda3883da6d 100644 --- a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py +++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py @@ -19,6 +19,19 @@ def normal(): safe_dn, ldap.SCOPE_SUBTREE, safe_filter, ["testAttr1", "testAttr2"]) +@app.route("/normal_noAttrs") +def normal_noAttrs(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + safe_dn = ldap.dn.escape_dn_chars(unsafe_dn) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + user = ldap_connection.search_s( + safe_dn, ldap.SCOPE_SUBTREE, safe_filter) + + @app.route("/direct") def direct(): unsafe_dn = "dc=%s" % request.args['dc'] From 3cda2e52073b72dfc126f7863041a85005d818ae Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 29 Mar 2021 23:39:49 +0200 Subject: [PATCH 0111/1429] Polish up ldap3 tests --- .../Security/CWE-090/ldap3_bad.py | 56 ------------------ .../Security/CWE-090/ldap3_good.py | 58 ------------------- .../Security/CWE-090/unit_tests/ldap3_bad.py | 38 ++++++++++++ .../Security/CWE-090/unit_tests/ldap3_good.py | 49 ++++++++++++++++ 4 files changed, 87 insertions(+), 114 deletions(-) delete mode 100644 python/ql/src/experimental/Security/CWE-090/ldap3_bad.py delete mode 100644 python/ql/src/experimental/Security/CWE-090/ldap3_good.py create mode 100644 python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py create mode 100644 python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py diff --git a/python/ql/src/experimental/Security/CWE-090/ldap3_bad.py b/python/ql/src/experimental/Security/CWE-090/ldap3_bad.py deleted file mode 100644 index fcd00c0269c..00000000000 --- a/python/ql/src/experimental/Security/CWE-090/ldap3_bad.py +++ /dev/null @@ -1,56 +0,0 @@ -import ldap3 -from flask import request, Flask - -app = Flask(__name__) - - -@app.route("/tainted_var") -def tainted_var(): - unsanitized_dn = "dc=%s" % request.args['dc'] - unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username'] - - srv = ldap3.Server('localhost', port=1337) - conn = ldap3.Connection(srv, user=unsanitized_dn, auto_bind=True) - conn.search(unsanitized_dn, unsanitized_filter) - return conn.response - - -@app.route("/var_tainted") -def var_tainted(): - unsanitized_dn = request.args['dc'] - unsanitized_filter = request.args['username'] - - dn = "dc=%s" % unsanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter - - srv = ldap3.Server('localhost', port=1337) - conn = ldap3.Connection(srv, user=dn, auto_bind=True) - conn.search(dn, search_filter) - return conn.response - - -@app.route("/direct") -def direct(): - srv = ldap3.Server('localhost', port=1337) - conn = ldap3.Connection(srv, user="dc=%s" % - request.args['dc'], auto_bind=True) - conn.search("dc=%s" % unsanitized_dn, - "(&(objectClass=*)(uid=%s))" % request.args['username']) - return conn.response - - -@app.route("/with_") -def with_(): - unsanitized_dn = request.args['dc'] - unsanitized_filter = request.args['username'] - - dn = "dc=%s" % unsanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter - - srv = ldap3.Server('localhost', port=1337) - with ldap3.Connection(server, auto_bind=True) as conn: - conn.search(dn, search_filter) - return conn.response - -# if __name__ == "__main__": -# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/ldap3_good.py b/python/ql/src/experimental/Security/CWE-090/ldap3_good.py deleted file mode 100644 index 05bc1e24f71..00000000000 --- a/python/ql/src/experimental/Security/CWE-090/ldap3_good.py +++ /dev/null @@ -1,58 +0,0 @@ -import ldap3 -from ldap3.utils.conv import escape_filter_chars -from flask import request, Flask - -app = Flask(__name__) - - -@app.route("/tainted_var") -def tainted_var(): - sanitized_dn = "dc=%s" % request.args['dc'] - sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars( - request.args['username']) - - srv = ldap3.Server('localhost', port=1337) - conn = ldap3.Connection(srv, user=sanitized_dn, auto_bind=True) - conn.search(sanitized_dn, sanitized_filter) - return conn.response - - -@app.route("/var_tainted") -def var_tainted(): - sanitized_dn = request.args['dc'] - sanitized_filter = request.args['username'] - - dn = "dc=%s" % sanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter) - - srv = ldap3.Server('localhost', port=1337) - conn = ldap3.Connection(srv, user=dn, auto_bind=True) - conn.search(dn, search_filter) - return conn.response - - -@app.route("/direct") -def direct(): - srv = ldap3.Server('localhost', port=1337) - conn = ldap3.Connection(srv, user="dc=%s" % - request.args['dc'], auto_bind=True) - conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" % - escape_filter_chars(request.args['username'])) - return conn.response - - -@ app.route("/with_") -def with_(): - sanitized_dn = request.args['dc'] - sanitized_filter = escape_filter_chars(request.args['username']) - - dn = "dc=%s" % sanitized_dn - search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter - - srv = ldap3.Server('localhost', port=1337) - with ldap3.Connection(server, auto_bind=True) as conn: - conn.search(dn, search_filter) - return conn.response - -# if __name__ == "__main__": -# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py new file mode 100644 index 00000000000..76b7f309bd1 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py @@ -0,0 +1,38 @@ +from flask import request, Flask +import ldap3 + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + srv = ldap3.Server('ldap://127.0.0.1', port=1337) + conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True) + conn.search(unsafe_dn, unsafe_filter, attributes=[ + "testAttr1", "testAttr2"]) + + +@app.route("/normal_noAttrs") +def normal_noAttrs(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + srv = ldap3.Server('ldap://127.0.0.1', port=1337) + conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True) + conn.search(unsafe_dn, unsafe_filter) + + +@app.route("/direct") +def direct(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + srv = ldap3.Server('ldap://127.0.0.1', port=1337) + conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True).search(unsafe_dn, unsafe_filter, attributes=[ + "testAttr1", "testAttr2"]) + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py new file mode 100644 index 00000000000..c3cc1272ea7 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py @@ -0,0 +1,49 @@ +from flask import request, Flask +import ldap3 +from ldap3.utils.dn import escape_rdn +from ldap3.utils.conv import escape_filter_chars + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + safe_dn = escape_rdn(unsafe_dn) + safe_filter = escape_filter_chars(unsafe_filter) + + srv = ldap3.Server('ldap://127.0.0.1', port=1337) + conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True) + conn.search(safe_dn, safe_filter, attributes=[ + "testAttr1", "testAttr2"]) + + +@app.route("/normal_noAttrs") +def normal_noAttrs(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + safe_dn = escape_rdn(unsafe_dn) + safe_filter = escape_filter_chars(unsafe_filter) + + srv = ldap3.Server('ldap://127.0.0.1', port=1337) + conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True) + conn.search(safe_dn, safe_filter) + + +@app.route("/direct") +def direct(): + unsafe_dn = "dc=%s" % request.args['dc'] + unsafe_filter = "(user=%s)" % request.args['username'] + + safe_dn = escape_rdn(unsafe_dn) + safe_filter = escape_filter_chars(unsafe_filter) + + srv = ldap3.Server('ldap://127.0.0.1', port=1337) + conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True).search(safe_dn, safe_filter, attributes=[ + "testAttr1", "testAttr2"]) + +# if __name__ == "__main__": +# app.run(debug=True) From 937a620f4d41db65c7203201435aaf3dab6380de Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Tue, 30 Mar 2021 11:33:42 +0100 Subject: [PATCH 0112/1429] JS: Improve mysql2 model --- .../src/semmle/javascript/frameworks/SQL.qll | 32 ++++++++++++++++--- .../frameworks/SQL/Credentials.expected | 1 + .../frameworks/SQL/SqlString.expected | 8 +++++ .../frameworks/SQL/mysql2-promise.js | 32 +++++++++++++++++++ .../frameworks/SQL/mysql2-types.ts | 5 +++ .../library-tests/frameworks/SQL/mysql2tst.js | 4 +++ .../library-tests/frameworks/SQL/mysql3.js | 4 +++ 7 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 javascript/ql/test/library-tests/frameworks/SQL/mysql2-promise.js create mode 100644 javascript/ql/test/library-tests/frameworks/SQL/mysql2-types.ts diff --git a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll index 1e41f03d141..cef49ec317d 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll @@ -28,30 +28,52 @@ module SQL { * Provides classes modelling the (API compatible) `mysql` and `mysql2` packages. */ private module MySql { + private string moduleName() { result = ["mysql", "mysql2", "mysql2/promise"] } + /** Gets the package name `mysql` or `mysql2`. */ - API::Node mysql() { result = API::moduleImport(["mysql", "mysql2"]) } + API::Node mysql() { result = API::moduleImport(moduleName()) } /** Gets a reference to `mysql.createConnection`. */ - API::Node createConnection() { result = mysql().getMember("createConnection") } + API::Node createConnection() { + result = mysql().getMember(["createConnection", "createConnectionPromise"]) + } /** Gets a reference to `mysql.createPool`. */ - API::Node createPool() { result = mysql().getMember("createPool") } + API::Node createPool() { result = mysql().getMember(["createPool", "createPoolCluster"]) } /** Gets a node that contains a MySQL pool created using `mysql.createPool()`. */ - API::Node pool() { result = createPool().getReturn() } + API::Node pool() { + result = createPool().getReturn() + or + result = pool().getMember("on").getReturn() + or + result = API::Node::ofType(moduleName(), ["Pool", "PoolCluster"]) + } /** Gets a data flow node that contains a freshly created MySQL connection instance. */ API::Node connection() { result = createConnection().getReturn() or + result = createConnection().getReturn().getPromised() + or result = pool().getMember("getConnection").getParameter(0).getParameter(1) + or + result = pool().getMember("getConnection").getPromised() + or + exists(API::CallNode call | + call = pool().getMember("on").getACall() and + call.getArgument(0).getStringValue() = ["connection", "acquire", "release"] and + result = call.getParameter(1).getParameter(0) + ) + or + result = API::Node::ofType(moduleName(), ["Connection", "PoolConnection"]) } /** A call to the MySql `query` method. */ private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode { QueryCall() { exists(API::Node recv | recv = pool() or recv = connection() | - this = recv.getMember("query").getACall() + this = recv.getMember(["query", "execute"]).getACall() ) } diff --git a/javascript/ql/test/library-tests/frameworks/SQL/Credentials.expected b/javascript/ql/test/library-tests/frameworks/SQL/Credentials.expected index a7d4af58a90..190d8a51982 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/Credentials.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/Credentials.expected @@ -6,6 +6,7 @@ | mysql1.js:7:14:7:21 | 'secret' | password | | mysql1a.js:10:9:10:12 | 'me' | user name | | mysql1a.js:11:13:11:20 | 'secret' | password | +| mysql2-promise.js:8:9:8:14 | 'root' | user name | | mysql2.js:7:21:7:25 | 'bob' | user name | | mysql2.js:8:21:8:28 | 'secret' | password | | mysql2tst.js:8:9:8:14 | 'root' | user name | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected index b2e0cc4b046..69b45d70307 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected @@ -7,10 +7,18 @@ | mysql1.js:13:18:13:43 | 'SELECT ... lution' | | mysql1.js:18:18:22:1 | {\\n s ... vid']\\n} | | mysql1a.js:17:18:17:43 | 'SELECT ... lution' | +| mysql2-promise.js:14:3:14:62 | 'SELECT ... ` > 45' | +| mysql2-promise.js:23:3:23:56 | 'SELECT ... e` > ?' | +| mysql2-promise.js:31:19:31:39 | 'SELECT ... users' | +| mysql2-promise.js:32:21:32:41 | 'SELECT ... users' | +| mysql2-types.ts:4:16:4:36 | 'SELECT ... users' | | mysql2.js:12:12:12:37 | 'SELECT ... lution' | | mysql2tst.js:14:3:14:62 | 'SELECT ... ` > 45' | | mysql2tst.js:23:3:23:56 | 'SELECT ... e` > ?' | +| mysql2tst.js:31:19:31:39 | 'SELECT ... users' | +| mysql2tst.js:32:21:32:41 | 'SELECT ... users' | | mysql3.js:14:20:14:52 | 'SELECT ... etable' | +| mysql3.js:26:14:26:31 | 'SELECT something' | | mysql4.js:14:18:14:20 | sql | | mysqlImport.js:3:18:5:1 | {\\n s ... = ?',\\n} | | postgres1.js:37:21:37:24 | text | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/mysql2-promise.js b/javascript/ql/test/library-tests/frameworks/SQL/mysql2-promise.js new file mode 100644 index 00000000000..6e40c5cafbc --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/mysql2-promise.js @@ -0,0 +1,32 @@ +// Adapted from the documentation of https://github.com/sidorares/node-mysql2, +// which is licensed under the MIT license; see file node-mysql2-License. +const mysql = require('mysql2/promise'); + +// create the connection to database +const connection = await mysql.createConnection({ + host: 'localhost', + user: 'root', + database: 'test' +}); + +// simple query +connection.query( + 'SELECT * FROM `table` WHERE `name` = "Page" AND `age` > 45', + function(err, results, fields) { + console.log(results); // results contains rows returned by server + console.log(fields); // fields contains extra meta data about results, if available + } +); + +// with placeholder +connection.query( + 'SELECT * FROM `table` WHERE `name` = ? AND `age` > ?', + ['Page', 45], + function(err, results) { + console.log(results); + } +); + +const conn2 = await mysql.createConnectionPromise(); +await conn2.query('SELECT * FROM users'); +await conn2.execute('SELECT * FROM users'); diff --git a/javascript/ql/test/library-tests/frameworks/SQL/mysql2-types.ts b/javascript/ql/test/library-tests/frameworks/SQL/mysql2-types.ts new file mode 100644 index 00000000000..dba89f2ca47 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/mysql2-types.ts @@ -0,0 +1,5 @@ +import { Connection } from "mysql2"; + +export function doSomething(conn: Connection) { + conn.query('SELECT * FROM users'); +} diff --git a/javascript/ql/test/library-tests/frameworks/SQL/mysql2tst.js b/javascript/ql/test/library-tests/frameworks/SQL/mysql2tst.js index c066aa8e92e..9121096f77d 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/mysql2tst.js +++ b/javascript/ql/test/library-tests/frameworks/SQL/mysql2tst.js @@ -26,3 +26,7 @@ connection.query( console.log(results); } ); + +const conn2 = await mysql.createConnectionPromise(); +await conn2.query('SELECT * FROM users'); +await conn2.execute('SELECT * FROM users'); diff --git a/javascript/ql/test/library-tests/frameworks/SQL/mysql3.js b/javascript/ql/test/library-tests/frameworks/SQL/mysql3.js index 63ac500d067..554777abcb1 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/mysql3.js +++ b/javascript/ql/test/library-tests/frameworks/SQL/mysql3.js @@ -21,3 +21,7 @@ pool.getConnection(function(err, connection) { // Don't use the connection here, it has been returned to the pool. }); }); + +pool.on('connection', conn => { + conn.query('SELECT something'); +}); From 0b21b273edbd1ad3f1a50683ca18e956680c6233 Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Mon, 29 Mar 2021 16:41:42 +0100 Subject: [PATCH 0113/1429] JS: Improve pg model --- .../src/semmle/javascript/frameworks/SQL.qll | 23 ++++++++++++++++++- .../frameworks/SQL/SqlString.expected | 4 ++++ .../frameworks/SQL/postgres-types.ts | 5 ++++ .../library-tests/frameworks/SQL/postgres2.js | 6 +++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 javascript/ql/test/library-tests/frameworks/SQL/postgres-types.ts diff --git a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll index cef49ec317d..b3f89c82f19 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll @@ -134,7 +134,20 @@ private module Postgres { // pool.connect(function(err, client) { ... }) result = pool().getMember("connect").getParameter(0).getParameter(1) or + // await pool.connect() + result = pool().getMember("connect").getReturn().getPromised() + or result = pgpConnection().getMember("client") + or + exists(API::CallNode call | + call = pool().getMember("on").getACall() and + call.getArgument(0).getStringValue() = ["connect", "acquire"] and + result = call.getParameter(1).getParameter(0) + ) + or + result = client().getMember("on").getReturn() + or + result = API::Node::ofType("pg", ["Client", "PoolClient"]) } /** Gets a constructor that when invoked constructs a new connection pool. */ @@ -151,6 +164,10 @@ private module Postgres { result = newPool().getInstance() or result = pgpDatabase().getMember("$pool") + or + result = pool().getMember("on").getReturn() + or + result = API::Node::ofType("pg", "Pool") } /** A call to the Postgres `query` method. */ @@ -162,7 +179,11 @@ private module Postgres { /** An expression that is passed to the `query` method and hence interpreted as SQL. */ class QueryString extends SQL::SqlString { - QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() } + QueryString() { + this = any(QueryCall qc).getAQueryArgument().asExpr() + or + this = API::moduleImport("pg-cursor").getParameter(0).getARhs().asExpr() + } } /** An expression that is passed as user name or password when creating a client or a pool. */ diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected index 69b45d70307..74efed7d265 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected @@ -23,8 +23,12 @@ | mysqlImport.js:3:18:5:1 | {\\n s ... = ?',\\n} | | postgres1.js:37:21:37:24 | text | | postgres2.js:30:16:30:41 | 'SELECT ... number' | +| postgres2.js:43:15:43:26 | 'SELECT 123' | +| postgres2.js:46:15:46:47 | new Cur ... users') | +| postgres2.js:46:26:46:46 | 'SELECT ... users' | | postgres3.js:15:16:15:40 | 'SELECT ... s name' | | postgres5.js:8:21:8:25 | query | +| postgres-types.ts:4:18:4:29 | 'SELECT 123' | | postgresImport.js:4:18:4:43 | 'SELECT ... number' | | sequelize2.js:10:17:10:118 | 'SELECT ... Y name' | | sequelize.js:8:17:8:118 | 'SELECT ... Y name' | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/postgres-types.ts b/javascript/ql/test/library-tests/frameworks/SQL/postgres-types.ts new file mode 100644 index 00000000000..00817e5d63e --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/postgres-types.ts @@ -0,0 +1,5 @@ +import { Client } from "pg"; + +function submitSomething(client: Client) { + client.query('SELECT 123'); +} diff --git a/javascript/ql/test/library-tests/frameworks/SQL/postgres2.js b/javascript/ql/test/library-tests/frameworks/SQL/postgres2.js index 611f4e286af..fd449ae8765 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/postgres2.js +++ b/javascript/ql/test/library-tests/frameworks/SQL/postgres2.js @@ -38,3 +38,9 @@ pool.connect(function(err, client, done) { //output: 1 }); }); + +let client2 = await pool.connect(); +client2.query('SELECT 123'); + +const Cursor = require('pg-cursor'); +client2.query(new Cursor('SELECT * from users')); From 95937c9ac70d7f62c9ed416d041178a8d772140f Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Mon, 29 Mar 2021 22:53:47 +0100 Subject: [PATCH 0114/1429] JS: Improve sqlite3 model --- .../ql/src/semmle/javascript/frameworks/SQL.qll | 15 ++++----------- .../frameworks/SQL/SqlString.expected | 1 + .../library-tests/frameworks/SQL/sqlite-types.ts | 5 +++++ 3 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 javascript/ql/test/library-tests/frameworks/SQL/sqlite-types.ts diff --git a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll index b3f89c82f19..15dc5df808c 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll @@ -342,24 +342,17 @@ private module Sqlite { } /** Gets an expression that constructs a Sqlite database instance. */ - API::Node newDb() { + API::Node database() { // new require('sqlite3').Database() result = sqlite().getMember("Database").getInstance() + or + result = API::Node::ofType("sqlite3", "Database") } /** A call to a Sqlite query method. */ private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode { QueryCall() { - exists(string meth | - meth = "all" or - meth = "each" or - meth = "exec" or - meth = "get" or - meth = "prepare" or - meth = "run" - | - this = newDb().getMember(meth).getACall() - ) + this = database().getMember(["all", "each", "exec", "get", "prepare", "run"]).getACall() } override DataFlow::Node getAQueryArgument() { result = getArgument(0) } diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected index 74efed7d265..4b6f1de2e7d 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected @@ -54,6 +54,7 @@ | spanner.js:19:16:19:34 | { sql: "SQL code" } | | spanner.js:19:23:19:32 | "SQL code" | | spannerImport.js:4:8:4:17 | "SQL code" | +| sqlite-types.ts:4:12:4:49 | "UPDATE ... id = ?" | | sqlite.js:7:8:7:45 | "UPDATE ... id = ?" | | sqliteArray.js:6:12:6:49 | "UPDATE ... id = ?" | | sqliteImport.js:2:8:2:44 | "UPDATE ... id = ?" | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/sqlite-types.ts b/javascript/ql/test/library-tests/frameworks/SQL/sqlite-types.ts new file mode 100644 index 00000000000..9356cf0bda5 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/sqlite-types.ts @@ -0,0 +1,5 @@ +import { Database } from "sqlite3"; + +export function doSomething(db: Database) { + db.run("UPDATE tbl SET name = ? WHERE id = ?", "bar", 2); +} From 93500bd95a55b1ca91c23472d21321f6af629593 Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Tue, 30 Mar 2021 10:06:14 +0100 Subject: [PATCH 0115/1429] JS: Improve mssql model --- .../src/semmle/javascript/frameworks/SQL.qll | 31 ++++++++++++++----- .../frameworks/SQL/SqlString.expected | 2 ++ .../frameworks/SQL/mssql-types.ts | 9 ++++++ .../library-tests/frameworks/SQL/mssql1.js | 2 ++ 4 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 javascript/ql/test/library-tests/frameworks/SQL/mssql-types.ts diff --git a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll index 15dc5df808c..f006c73e544 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll @@ -371,15 +371,32 @@ private module MsSql { /** Gets a reference to the `mssql` module. */ API::Node mssql() { result = API::moduleImport("mssql") } - /** Gets an expression that creates a request object. */ - API::Node request() { - // new require('mssql').Request() - result = mssql().getMember("Request").getInstance() + /** Gets a node referring to an instance of the given class. */ + API::Node mssqlClass(string name) { + result = mssql().getMember(name).getInstance() or - // request.input(...) - result = request().getMember("input").getReturn() + result = API::Node::ofType("mssql", name) } + /** Gets an API node referring to a Request object. */ + API::Node request() { + result = mssqlClass("Request") + or + result = request().getMember(["input", "replaceInput", "output", "replaceOutput"]).getReturn() + or + result = [transaction(), pool()].getMember("request").getReturn() + } + + /** Gets an API node referring to a Transaction object. */ + API::Node transaction() { + result = mssqlClass("Transaction") + or + result = pool().getMember("transaction").getReturn() + } + + /** Gets a API node referring to a ConnectionPool object. */ + API::Node pool() { result = mssqlClass("ConnectionPool") } + /** A tagged template evaluated as a query. */ private class QueryTemplateExpr extends DatabaseAccess, DataFlow::ValueNode { override TaggedTemplateExpr astNode; @@ -395,7 +412,7 @@ private module MsSql { /** A call to a MsSql query method. */ private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode { - QueryCall() { this = request().getMember(["query", "batch"]).getACall() } + QueryCall() { this = [mssql(), request()].getMember(["query", "batch"]).getACall() } override DataFlow::Node getAQueryArgument() { result = getArgument(0) } } diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected index 4b6f1de2e7d..8720962382b 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected @@ -1,9 +1,11 @@ | mssql1.js:7:40:7:72 | select ... e id = | | mssql1.js:7:75:7:79 | value | +| mssql1.js:10:19:10:30 | 'SELECT 123' | | mssql2.js:5:15:5:34 | 'select 1 as number' | | mssql2.js:13:15:13:66 | 'create ... table' | | mssql2.js:22:24:22:43 | 'select 1 as number' | | mssql2.js:29:30:29:81 | 'create ... table' | +| mssql-types.ts:7:31:7:42 | 'SELECT 123' | | mysql1.js:13:18:13:43 | 'SELECT ... lution' | | mysql1.js:18:18:22:1 | {\\n s ... vid']\\n} | | mysql1a.js:17:18:17:43 | 'SELECT ... lution' | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/mssql-types.ts b/javascript/ql/test/library-tests/frameworks/SQL/mssql-types.ts new file mode 100644 index 00000000000..243812e799c --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/mssql-types.ts @@ -0,0 +1,9 @@ +import { ConnectionPool } from "mssql"; + +class Foo { + constructor(private pool: ConnectionPool) {} + + doSomething() { + this.pool.request().query('SELECT 123'); + } +} diff --git a/javascript/ql/test/library-tests/frameworks/SQL/mssql1.js b/javascript/ql/test/library-tests/frameworks/SQL/mssql1.js index 39a340ccf83..8f8bf1f0914 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/mssql1.js +++ b/javascript/ql/test/library-tests/frameworks/SQL/mssql1.js @@ -6,6 +6,8 @@ async () => { const pool = await sql.connect('mssql://username:password@localhost/database') const result = await sql.query`select * from mytable where id = ${value}` console.dir(result) + + sql.query('SELECT 123'); } catch (err) { // ... error checks } From 1349bf7b0ba8b83ee86d5fbdc46b207e32439cd1 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Tue, 30 Mar 2021 02:57:58 +0000 Subject: [PATCH 0116/1429] Create a .qll file to reuse the code and add check of Spring properties --- .../CWE-555/CredentialsInPropertiesFile.ql | 93 +-------------- .../CredentialsInPropertiesFile.qll | 107 ++++++++++++++++++ .../CredentialsInPropertiesFile.expected | 10 +- .../CWE-555/CredentialsInPropertiesFile.ql | 93 +-------------- .../security/CWE-555/MailConfig.java | 11 ++ .../security/CWE-555/PropertiesUtils.java | 10 -- .../query-tests/security/CWE-555/options | 1 + .../beans/factory/annotation/Value.java | 11 ++ 8 files changed, 137 insertions(+), 199 deletions(-) create mode 100644 java/ql/src/experimental/semmle/code/java/frameworks/CredentialsInPropertiesFile.qll create mode 100644 java/ql/test/experimental/query-tests/security/CWE-555/MailConfig.java create mode 100644 java/ql/test/experimental/query-tests/security/CWE-555/options create mode 100644 java/ql/test/stubs/springframework-5.2.3/org/springframework/beans/factory/annotation/Value.java diff --git a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql index 8bbc0d00a46..d4ebe902cf7 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-555/CredentialsInPropertiesFile.ql @@ -18,41 +18,7 @@ */ import java -import semmle.code.configfiles.ConfigFiles -import semmle.code.java.dataflow.FlowSources - -private string possibleSecretName() { - result = - [ - "%password%", "%passwd%", "%account%", "%accnt%", "%credential%", "%token%", "%secret%", - "%access%key%" - ] -} - -private string possibleEncryptedSecretName() { result = ["%hashed%", "%encrypted%", "%crypt%"] } - -/** Holds if the value is not cleartext credentials. */ -bindingset[value] -predicate isNotCleartextCredentials(string value) { - value = "" // Empty string - or - value.length() < 7 // Typical credentials are no less than 6 characters - or - value.matches("% %") // Sentences containing spaces - or - value.regexpMatch(".*[^a-zA-Z\\d]{3,}.*") // Contain repeated non-alphanumeric characters such as a fake password pass**** or ???? - or - value.matches("@%") // Starts with the "@" sign - or - value.regexpMatch("\\$\\{.*\\}") // Variable placeholder ${credentials} - or - value.matches("%=") // A basic check of encrypted credentials ending with padding characters - or - value.matches("ENC(%)") // Encrypted value - or - // Could be a message property for UI display or fake passwords, e.g. login.password_expired=Your current password has expired. - value.toLowerCase().matches(possibleSecretName()) -} +import experimental.semmle.code.java.frameworks.CredentialsInPropertiesFile /** * Holds if the credentials are in a non-production properties file indicated by: @@ -63,63 +29,6 @@ predicate isNonProdCredentials(CredentialsConfig cc) { cc.getFile().getAbsolutePath().matches(["%dev%", "%test%", "%sample%"]) } -/** The credentials configuration property. */ -class CredentialsConfig extends ConfigPair { - CredentialsConfig() { - this.getNameElement().getName().trim().toLowerCase().matches(possibleSecretName()) and - not this.getNameElement().getName().trim().toLowerCase().matches(possibleEncryptedSecretName()) and - not isNotCleartextCredentials(this.getValueElement().getValue().trim()) - } - - string getName() { result = this.getNameElement().getName().trim() } - - string getValue() { result = this.getValueElement().getValue().trim() } - - /** Returns a description of this vulnerability. */ - string getConfigDesc() { - exists( - LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink, MethodAccess ma - | - this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and - cc.hasFlow(source, sink) and - ma.getArgument(0) = sink.asExpr() and - result = "Plaintext credentials " + this.getName() + " are loaded in " + ma - ) - or - not exists(LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink | - this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and - cc.hasFlow(source, sink) - ) and - result = - "Plaintext credentials " + this.getName() + " have cleartext value " + this.getValue() + - " in properties file" - } -} - -/** - * A dataflow configuration tracking flow of a method that loads a credentials property. - */ -class LoadCredentialsConfiguration extends DataFlow::Configuration { - LoadCredentialsConfiguration() { this = "LoadCredentialsConfiguration" } - - override predicate isSource(DataFlow::Node source) { - exists(CredentialsConfig cc | - source.asExpr().(CompileTimeConstantExpr).getStringValue() = cc.getName() - ) - } - - override predicate isSink(DataFlow::Node sink) { - sink.asExpr() = - any(MethodAccess ma | - ma.getMethod() - .getDeclaringType() - .getASupertype*() - .hasQualifiedName("java.util", "Properties") and - ma.getMethod().getName() = "getProperty" - ).getArgument(0) - } -} - from CredentialsConfig cc where not isNonProdCredentials(cc) select cc, cc.getConfigDesc() diff --git a/java/ql/src/experimental/semmle/code/java/frameworks/CredentialsInPropertiesFile.qll b/java/ql/src/experimental/semmle/code/java/frameworks/CredentialsInPropertiesFile.qll new file mode 100644 index 00000000000..9577789ce0c --- /dev/null +++ b/java/ql/src/experimental/semmle/code/java/frameworks/CredentialsInPropertiesFile.qll @@ -0,0 +1,107 @@ +/** + * Provides classes for analyzing properties files. + */ + +import java +import semmle.code.configfiles.ConfigFiles +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.frameworks.Properties + +private string possibleSecretName() { + result = + [ + "%password%", "%passwd%", "%account%", "%accnt%", "%credential%", "%token%", "%secret%", + "%access%key%" + ] +} + +private string possibleEncryptedSecretName() { result = ["%hashed%", "%encrypted%", "%crypt%"] } + +/** Holds if the value is not cleartext credentials. */ +bindingset[value] +predicate isNotCleartextCredentials(string value) { + value = "" // Empty string + or + value.length() < 7 // Typical credentials are no less than 6 characters + or + value.matches("% %") // Sentences containing spaces + or + value.regexpMatch(".*[^a-zA-Z\\d]{3,}.*") // Contain repeated non-alphanumeric characters such as a fake password pass**** or ???? + or + value.matches("@%") // Starts with the "@" sign + or + value.regexpMatch("\\$\\{.*\\}") // Variable placeholder ${credentials} + or + value.matches("%=") // A basic check of encrypted credentials ending with padding characters + or + value.matches("ENC(%)") // Encrypted value + or + // Could be a message property for UI display or fake passwords, e.g. login.password_expired=Your current password has expired. + value.toLowerCase().matches(possibleSecretName()) +} + +/** The credentials configuration property. */ +class CredentialsConfig extends ConfigPair { + CredentialsConfig() { + this.getNameElement().getName().trim().toLowerCase().matches(possibleSecretName()) and + not this.getNameElement().getName().trim().toLowerCase().matches(possibleEncryptedSecretName()) and + not isNotCleartextCredentials(this.getValueElement().getValue().trim()) + } + + string getName() { result = this.getNameElement().getName().trim() } + + string getValue() { result = this.getValueElement().getValue().trim() } + + /** Returns a description of this vulnerability. */ + string getConfigDesc() { + exists( + // getProperty(...) + LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink, MethodAccess ma + | + this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and + cc.hasFlow(source, sink) and + ma.getArgument(0) = sink.asExpr() and + result = "Plaintext credentials " + this.getName() + " are loaded in Java Properties " + ma + ) + or + exists( + // @Value("${mail.password}") + Annotation a + | + a.getType().hasQualifiedName("org.springframework.beans.factory.annotation", "Value") and + a.getAValue().(CompileTimeConstantExpr).getStringValue() = "${" + this.getName() + "}" and + result = "Plaintext credentials " + this.getName() + " are loaded in Spring annotation " + a + ) + or + not exists(LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink | + this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and + cc.hasFlow(source, sink) + ) and + not exists(Annotation a | + a.getType().hasQualifiedName("org.springframework.beans.factory.annotation", "Value") and + a.getAValue().(CompileTimeConstantExpr).getStringValue() = "${" + this.getName() + "}" + ) and + result = + "Plaintext credentials " + this.getName() + " have cleartext value " + this.getValue() + + " in properties file" + } +} + +/** + * A dataflow configuration tracking flow of cleartext credentials stored in a properties file + * to a `Properties.getProperty(...)` method call. + */ +class LoadCredentialsConfiguration extends DataFlow::Configuration { + LoadCredentialsConfiguration() { this = "LoadCredentialsConfiguration" } + + override predicate isSource(DataFlow::Node source) { + exists(CredentialsConfig cc | + source.asExpr().(CompileTimeConstantExpr).getStringValue() = cc.getName() + ) + } + + override predicate isSink(DataFlow::Node sink) { + sink.asExpr() = + any(MethodAccess ma | ma.getMethod() instanceof PropertiesGetPropertyMethod).getArgument(0) + } +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected index 1bdc004a446..371a4765c53 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected +++ b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.expected @@ -1,5 +1,5 @@ -| configuration.properties:6:1:6:25 | ldap.password=mysecpass | Plaintext credentials ldap.password are loaded in getProperty(...) | -| configuration.properties:18:1:18:35 | datasource1.password=Passw0rd@123 | Plaintext credentials datasource1.password are loaded in getProperty(...) | -| configuration.properties:25:1:25:31 | mail.password=MysecPWxWa@1993 | Plaintext credentials mail.password are loaded in getProperty(...) | -| configuration.properties:33:1:33:50 | com.example.aws.s3.access_key=AKMAMQPBYMCD6YSAYCBA | Plaintext credentials com.example.aws.s3.access_key are loaded in getProperty(...) | -| configuration.properties:34:1:34:70 | com.example.aws.s3.secret_key=8lMPSfWzZq+wcWtck5+QPLOJDZzE783pS09/IO3k | Plaintext credentials com.example.aws.s3.secret_key are loaded in getProperty(...) | +| configuration.properties:6:1:6:25 | ldap.password=mysecpass | Plaintext credentials ldap.password are loaded in Java Properties getProperty(...) | +| configuration.properties:18:1:18:35 | datasource1.password=Passw0rd@123 | Plaintext credentials datasource1.password are loaded in Java Properties getProperty(...) | +| configuration.properties:25:1:25:31 | mail.password=MysecPWxWa@1993 | Plaintext credentials mail.password are loaded in Spring annotation Value | +| configuration.properties:33:1:33:50 | com.example.aws.s3.access_key=AKMAMQPBYMCD6YSAYCBA | Plaintext credentials com.example.aws.s3.access_key are loaded in Java Properties getProperty(...) | +| configuration.properties:34:1:34:70 | com.example.aws.s3.secret_key=8lMPSfWzZq+wcWtck5+QPLOJDZzE783pS09/IO3k | Plaintext credentials com.example.aws.s3.secret_key are loaded in Java Properties getProperty(...) | diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql index b6890e4ac9f..f085317218c 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql +++ b/java/ql/test/experimental/query-tests/security/CWE-555/CredentialsInPropertiesFile.ql @@ -18,98 +18,7 @@ */ import java -import semmle.code.configfiles.ConfigFiles -import semmle.code.java.dataflow.FlowSources - -private string possibleSecretName() { - result = - [ - "%password%", "%passwd%", "%account%", "%accnt%", "%credential%", "%token%", "%secret%", - "%access%key%" - ] -} - -private string possibleEncryptedSecretName() { result = ["%hashed%", "%encrypted%", "%crypt%"] } - -/** Holds if the value is not cleartext credentials. */ -bindingset[value] -predicate isNotCleartextCredentials(string value) { - value = "" // Empty string - or - value.length() < 7 // Typical credentials are no less than 6 characters - or - value.matches("% %") // Sentences containing spaces - or - value.regexpMatch(".*[^a-zA-Z\\d]{3,}.*") // Contain repeated non-alphanumeric characters such as a fake password pass**** or ???? - or - value.matches("@%") // Starts with the "@" sign - or - value.regexpMatch("\\$\\{.*\\}") // Variable placeholder ${credentials} - or - value.matches("%=") // A basic check of encrypted credentials ending with padding characters - or - value.matches("ENC(%)") // Encrypted value - or - // Could be a message property for UI display or fake passwords, e.g. login.password_expired=Your current password has expired. - value.toLowerCase().matches(possibleSecretName()) -} - -/** The credentials configuration property. */ -class CredentialsConfig extends ConfigPair { - CredentialsConfig() { - this.getNameElement().getName().trim().toLowerCase().matches(possibleSecretName()) and - not this.getNameElement().getName().trim().toLowerCase().matches(possibleEncryptedSecretName()) and - not isNotCleartextCredentials(this.getValueElement().getValue().trim()) - } - - string getName() { result = this.getNameElement().getName().trim() } - - string getValue() { result = this.getValueElement().getValue().trim() } - - /** Returns a description of this vulnerability. */ - string getConfigDesc() { - exists( - LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink, MethodAccess ma - | - this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and - cc.hasFlow(source, sink) and - ma.getArgument(0) = sink.asExpr() and - result = "Plaintext credentials " + this.getName() + " are loaded in " + ma - ) - or - not exists(LoadCredentialsConfiguration cc, DataFlow::Node source, DataFlow::Node sink | - this.getName() = source.asExpr().(CompileTimeConstantExpr).getStringValue() and - cc.hasFlow(source, sink) - ) and - result = - "Plaintext credentials " + this.getName() + " have cleartext value " + this.getValue() + - " in properties file" - } -} - -/** - * A dataflow configuration tracking flow of a method that loads a credentials property. - */ -class LoadCredentialsConfiguration extends DataFlow::Configuration { - LoadCredentialsConfiguration() { this = "LoadCredentialsConfiguration" } - - override predicate isSource(DataFlow::Node source) { - exists(CredentialsConfig cc | - source.asExpr().(CompileTimeConstantExpr).getStringValue() = cc.getName() - ) - } - - override predicate isSink(DataFlow::Node sink) { - sink.asExpr() = - any(MethodAccess ma | - ma.getMethod() - .getDeclaringType() - .getASupertype*() - .hasQualifiedName("java.util", "Properties") and - ma.getMethod().getName() = "getProperty" - ).getArgument(0) - } -} +import experimental.semmle.code.java.frameworks.CredentialsInPropertiesFile from CredentialsConfig cc select cc, cc.getConfigDesc() diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/MailConfig.java b/java/ql/test/experimental/query-tests/security/CWE-555/MailConfig.java new file mode 100644 index 00000000000..bef99dac3ea --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-555/MailConfig.java @@ -0,0 +1,11 @@ +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MailConfig { + @Value("${mail.password}") + private String mailPassword; + + @Value("${mail.username}") + private String mailUserName; +} diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/PropertiesUtils.java b/java/ql/test/experimental/query-tests/security/CWE-555/PropertiesUtils.java index b54995a9967..f33ef39ee07 100644 --- a/java/ql/test/experimental/query-tests/security/CWE-555/PropertiesUtils.java +++ b/java/ql/test/experimental/query-tests/security/CWE-555/PropertiesUtils.java @@ -35,16 +35,6 @@ public class PropertiesUtils { return properties.getProperty("datasource1.password"); } - /** Returns the mail account property value. */ - public static String getMailUserName() { - return properties.getProperty("mail.username"); - } - - /** Returns the mail password property value. */ - public static String getMailPassword() { - return properties.getProperty("mail.password"); - } - /** Returns the AWS Access Key property value. */ public static String getAWSAccessKey() { return properties.getProperty("com.example.aws.s3.access_key"); diff --git a/java/ql/test/experimental/query-tests/security/CWE-555/options b/java/ql/test/experimental/query-tests/security/CWE-555/options new file mode 100644 index 00000000000..cf3d60de0e5 --- /dev/null +++ b/java/ql/test/experimental/query-tests/security/CWE-555/options @@ -0,0 +1 @@ +// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/springframework-5.2.3 \ No newline at end of file diff --git a/java/ql/test/stubs/springframework-5.2.3/org/springframework/beans/factory/annotation/Value.java b/java/ql/test/stubs/springframework-5.2.3/org/springframework/beans/factory/annotation/Value.java new file mode 100644 index 00000000000..67c38cf5d20 --- /dev/null +++ b/java/ql/test/stubs/springframework-5.2.3/org/springframework/beans/factory/annotation/Value.java @@ -0,0 +1,11 @@ +package org.springframework.beans.factory.annotation; + +public @interface Value { + + /** + * The actual value expression such as #{systemProperties.myProp} + * or property placeholder such as ${my.app.myProp}. + */ + String value(); + +} \ No newline at end of file From 35f294f096c160db887b37fff31a740f32e9f3f8 Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Tue, 30 Mar 2021 10:38:13 +0100 Subject: [PATCH 0117/1429] JS: Improve sequelize model --- .../src/semmle/javascript/frameworks/SQL.qll | 24 ++++++++++++++----- .../frameworks/SQL/SqlString.expected | 4 ++++ .../frameworks/SQL/sequelize-types.ts | 9 +++++++ .../frameworks/SQL/sequelize2.js | 6 +++++ 4 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 javascript/ql/test/library-tests/frameworks/SQL/sequelize-types.ts diff --git a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll index f006c73e544..a84bde4cdd0 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll @@ -463,22 +463,34 @@ private module MsSql { * Provides classes modelling the `sequelize` package. */ private module Sequelize { - /** Gets an import of the `sequelize` module. */ - API::Node sequelize() { result = API::moduleImport("sequelize") } + /** Gets an import of the `sequelize` module or one that re-exports it. */ + API::Node sequelize() { result = API::moduleImport(["sequelize", "sequelize-typescript"]) } /** Gets an expression that creates an instance of the `Sequelize` class. */ - API::Node newSequelize() { result = sequelize().getInstance() } + API::Node instance() { + result = [sequelize(), sequelize().getMember("Sequelize")].getInstance() + or + result = API::Node::ofType(["sequelize", "sequelize-typescript"], ["Sequelize", "default"]) + } /** A call to `Sequelize.query`. */ private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode { - QueryCall() { this = newSequelize().getMember("query").getACall() } + QueryCall() { this = instance().getMember("query").getACall() } - override DataFlow::Node getAQueryArgument() { result = getArgument(0) } + override DataFlow::Node getAQueryArgument() { + result = getArgument(0) + or + result = getOptionArgument(0, "query") + } } /** An expression that is passed to `Sequelize.query` method and hence interpreted as SQL. */ class QueryString extends SQL::SqlString { - QueryString() { this = any(QueryCall qc).getAQueryArgument().asExpr() } + QueryString() { + this = any(QueryCall qc).getAQueryArgument().asExpr() + or + this = sequelize().getMember(["literal", "asIs"]).getParameter(0).getARhs().asExpr() + } } /** diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected index 8720962382b..a5e87fe45d8 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected @@ -33,6 +33,10 @@ | postgres-types.ts:4:18:4:29 | 'SELECT 123' | | postgresImport.js:4:18:4:43 | 'SELECT ... number' | | sequelize2.js:10:17:10:118 | 'SELECT ... Y name' | +| sequelize2.js:12:17:15:1 | {\\n que ... [123]\\n} | +| sequelize2.js:13:10:13:20 | 'SELECT $1' | +| sequelize2.js:17:31:17:41 | '123 + 345' | +| sequelize-types.ts:7:24:7:35 | 'SELECT 123' | | sequelize.js:8:17:8:118 | 'SELECT ... Y name' | | sequelizeImport.js:3:17:3:118 | 'SELECT ... Y name' | | spanner2.js:5:26:5:35 | "SQL code" | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/sequelize-types.ts b/javascript/ql/test/library-tests/frameworks/SQL/sequelize-types.ts new file mode 100644 index 00000000000..2591048bdb5 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/sequelize-types.ts @@ -0,0 +1,9 @@ +import Sequelize from 'sequelize'; + +export class Foo { + constructor(private seq: Sequelize) {} + + method() { + this.seq.query('SELECT 123'); + } +} diff --git a/javascript/ql/test/library-tests/frameworks/SQL/sequelize2.js b/javascript/ql/test/library-tests/frameworks/SQL/sequelize2.js index 194a921ffdd..9fa7392cad6 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/sequelize2.js +++ b/javascript/ql/test/library-tests/frameworks/SQL/sequelize2.js @@ -9,3 +9,9 @@ const sequelize = new Sequelize('database', { }); sequelize.query('SELECT * FROM Products WHERE (name LIKE \'%' + criteria + '%\') AND deletedAt IS NULL) ORDER BY name'); +sequelize.query({ + query: 'SELECT $1', + values: [123] +}); + +let value = Sequelize.literal('123 + 345'); From 9db235ac3660a894d610d49388e452317329744d Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Tue, 30 Mar 2021 11:26:44 +0100 Subject: [PATCH 0118/1429] JS: Improve @google-cloud/spanner model --- .../src/semmle/javascript/frameworks/SQL.qll | 80 ++++++++++++------- .../frameworks/SQL/SqlString.expected | 3 + .../frameworks/SQL/spanner-types.ts | 5 ++ .../library-tests/frameworks/SQL/spanner.js | 12 +++ 4 files changed, 71 insertions(+), 29 deletions(-) create mode 100644 javascript/ql/test/library-tests/frameworks/SQL/spanner-types.ts diff --git a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll index a84bde4cdd0..ce702decc96 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll @@ -543,6 +543,8 @@ private module Spanner { API::Node database() { result = spanner().getReturn().getMember("instance").getReturn().getMember("database").getReturn() + or + result = API::Node::ofType("@google-cloud/spanner", "Database") } /** @@ -550,61 +552,81 @@ private module Spanner { */ API::Node v1SpannerClient() { result = spanner().getMember("v1").getMember("SpannerClient").getInstance() + or + result = API::Node::ofType("@google-cloud/spanner", "v1.SpannerClient") } /** * Gets a node that refers to a transaction object. */ API::Node transaction() { - result = database().getMember("runTransaction").getParameter(0).getParameter(1) + result = + database() + .getMember(["runTransaction", "runTransactionAsync"]) + .getParameter([0, 1]) + .getParameter(1) + or + result = API::Node::ofType("@google-cloud/spanner", "Transaction") + } + + /** Gets an API node referring to a `BatchTransaction` object. */ + API::Node batchTransaction() { + result = database().getMember("batchTransaction").getReturn() + or + result = database().getMember("createBatchTransaction").getReturn().getPromised() + or + result = API::Node::ofType("@google-cloud/spanner", "BatchTransaction") } /** * A call to a Spanner method that executes a SQL query. */ - abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode { - /** - * Gets the position of the query argument; default is zero, which can be overridden - * by subclasses. - */ - int getQueryArgumentPosition() { result = 0 } + abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode { } + + /** + * A SQL execution that takes the input directly in the first argument or in the `sql` option. + */ + class SqlExecutionDirect extends SqlExecution { + SqlExecutionDirect() { + this = database().getMember(["run", "runPartitionedUpdate", "runStream"]).getACall() + or + this = transaction().getMember(["run", "runStream", "runUpdate"]).getACall() + or + this = batchTransaction().getMember("createQueryPartitions").getACall() + } override DataFlow::Node getAQueryArgument() { - result = getArgument(getQueryArgumentPosition()) or - result = getOptionArgument(getQueryArgumentPosition(), "sql") + result = getArgument(0) + or + result = getOptionArgument(0, "sql") } } /** - * A call to `Database.run`, `Database.runPartitionedUpdate` or `Database.runStream`. + * A SQL execution that takes an array of SQL strings or { sql: string } objects. */ - class DatabaseRunCall extends SqlExecution { - DatabaseRunCall() { - this = database().getMember(["run", "runPartitionedUpdate", "runStream"]).getACall() + class SqlExecutionBatch extends SqlExecution, API::CallNode { + SqlExecutionBatch() { this = transaction().getMember("batchUpdate").getACall() } + + override DataFlow::Node getAQueryArgument() { + // just use the whole array as the query argument, as arrays becomes tainted if one of the elements + // are tainted + result = getArgument(0) + or + result = getParameter(0).getUnknownMember().getMember("sql").getARhs() } } /** - * A call to `Transaction.run`, `Transaction.runStream` or `Transaction.runUpdate`. + * A SQL execution that only takes the input in the `sql` option, and do not accept query strings + * directly. */ - class TransactionRunCall extends SqlExecution { - TransactionRunCall() { - this = transaction().getMember(["run", "runStream", "runUpdate"]).getACall() - } - } - - /** - * A call to `v1.SpannerClient.executeSql` or `v1.SpannerClient.executeStreamingSql`. - */ - class ExecuteSqlCall extends SqlExecution { - ExecuteSqlCall() { + class SqlExecutionWithOption extends SqlExecution { + SqlExecutionWithOption() { this = v1SpannerClient().getMember(["executeSql", "executeStreamingSql"]).getACall() } - override DataFlow::Node getAQueryArgument() { - // `executeSql` and `executeStreamingSql` do not accept query strings directly - result = getOptionArgument(0, "sql") - } + override DataFlow::Node getAQueryArgument() { result = getOptionArgument(0, "sql") } } /** diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected index a5e87fe45d8..41bf9865b16 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected @@ -41,6 +41,7 @@ | sequelizeImport.js:3:17:3:118 | 'SELECT ... Y name' | | spanner2.js:5:26:5:35 | "SQL code" | | spanner2.js:7:35:7:44 | "SQL code" | +| spanner-types.ts:4:12:4:23 | 'SELECT 123' | | spanner.js:6:8:6:17 | "SQL code" | | spanner.js:7:8:7:26 | { sql: "SQL code" } | | spanner.js:7:15:7:24 | "SQL code" | @@ -59,6 +60,8 @@ | spanner.js:18:16:18:25 | "SQL code" | | spanner.js:19:16:19:34 | { sql: "SQL code" } | | spanner.js:19:23:19:32 | "SQL code" | +| spanner.js:23:12:23:23 | 'SELECT 123' | +| spanner.js:26:12:26:38 | 'UPDATE ... = @baz' | | spannerImport.js:4:8:4:17 | "SQL code" | | sqlite-types.ts:4:12:4:49 | "UPDATE ... id = ?" | | sqlite.js:7:8:7:45 | "UPDATE ... id = ?" | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/spanner-types.ts b/javascript/ql/test/library-tests/frameworks/SQL/spanner-types.ts new file mode 100644 index 00000000000..8dceca56e60 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/spanner-types.ts @@ -0,0 +1,5 @@ +import { Database } from "@google-cloud/spanner"; + +export function doSomething(db: Database) { + db.run('SELECT 123'); +} diff --git a/javascript/ql/test/library-tests/frameworks/SQL/spanner.js b/javascript/ql/test/library-tests/frameworks/SQL/spanner.js index a27c52d82fd..8396745f7e6 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/spanner.js +++ b/javascript/ql/test/library-tests/frameworks/SQL/spanner.js @@ -17,6 +17,18 @@ db.runTransaction((err, tx) => { tx.runStream({ sql: "SQL code" }); tx.runUpdate("SQL code"); tx.runUpdate({ sql: "SQL code" }); + + const queries = [ + { + sql: 'SELECT 123', + }, + { + sql: 'UPDATE foo SET bar = @baz', + params: {key: 'baz', value: '123'} + } + ]; + + tx.batchUpdate(queries, () => {}); }); exports.instance = instance; From f8bbda0cdc82aa8c155c8f4d0e76e2f6dd811ab9 Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Tue, 30 Mar 2021 11:37:45 +0100 Subject: [PATCH 0119/1429] JS: Change note --- javascript/change-notes/2021-03-30-sql-models.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 javascript/change-notes/2021-03-30-sql-models.md diff --git a/javascript/change-notes/2021-03-30-sql-models.md b/javascript/change-notes/2021-03-30-sql-models.md new file mode 100644 index 00000000000..2ceaf8dc7a7 --- /dev/null +++ b/javascript/change-notes/2021-03-30-sql-models.md @@ -0,0 +1,3 @@ +lgtm,codescanning +* The SQL library models for `mysql`, `mysql2`, `mssql`, `pg`, `sqlite3`, `sequelize`, and `@google-cloud/spanner` have improved, + leading to more SQL injection sinks. From 8faafb6961d21e652ff3c7223dab498cec9f8332 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 30 Mar 2021 16:58:02 +0200 Subject: [PATCH 0120/1429] Update Sink --- .../Security/CWE-090/LDAPInjection.ql | 9 +++---- .../semmle/python/frameworks/Stdlib.qll | 6 ++--- .../security/injection/LDAPInjection.qll | 26 +++++++++++++++++-- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql index 7ce3a118918..a803b39260e 100644 --- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql +++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql @@ -16,11 +16,10 @@ import DataFlow::PathGraph from LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink, - LDAPQuery castedSink + LDAPInjectionSink castedSink where config.hasFlowPath(source, sink) and - castedSink = sink.getNode() //and + castedSink.getLDAPNode() = sink.getNode() //and // if exists(castedSink.getAttrs()) then -select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@ probably leaking $@.", - sink.getNode(), "This", source.getNode(), "a user-provided value", castedSink.getLDAPNode(), - castedSink.getLDAPPart(), castedSink.getAttrs(), "this attribute(s)" +select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@.", castedSink, "This", + source.getNode(), "a user-provided value", castedSink.getLDAPNode(), castedSink.getLDAPPart() //, castedSink.getAttrs(), "probably leaking this attribute(s)" diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll index aa9013a76c0..348367d0370 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll @@ -95,10 +95,10 @@ private module LDAP { ( ldapNode = this.getArg(0) and ldapPart = "DN" - or - ldapNode = this.getArg(1) and - ldapPart = "search_filter" ) + or + ldapNode = this.getArg(1) and + ldapPart = "search_filter" ) } diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll index db8bdda7db6..4814097ff8e 100644 --- a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll +++ b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll @@ -8,6 +8,26 @@ import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking import semmle.python.dataflow.new.RemoteFlowSources +class LDAPInjectionSink extends DataFlow::Node { + // DataFlow::Node attrs; + DataFlow::Node ldapNode; + string ldapPart; + + LDAPInjectionSink() { + exists(LDAPQuery ldapQuery | + this = ldapQuery and + ldapNode = ldapQuery.getLDAPNode() and + ldapPart = ldapQuery.getLDAPPart() // and + // if exists(ldapQuery.getAttrs()) then attrs = ldapQuery.getAttrs() + ) + } + + DataFlow::Node getLDAPNode() { result = ldapNode } + + string getLDAPPart() { result = ldapPart } + // DataFlow::Node getAttrs() { result = attrs } +} + /** * A taint-tracking configuration for detecting regular expression injections. */ @@ -16,9 +36,11 @@ class LDAPInjectionFlowConfig extends TaintTracking::Configuration { override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery lQ).getLDAPNode() } + override predicate isSink(DataFlow::Node sink) { + sink = any(LDAPInjectionSink ldapInjSink).getLDAPNode() + } override predicate isSanitizer(DataFlow::Node sanitizer) { - sanitizer = any(LDAPEscape lE).getEscapeNode() + sanitizer = any(LDAPEscape ldapEsc).getEscapeNode() } } From 57784dc7461f3f4624966b9c9e0356fe0018ee5b Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Wed, 31 Mar 2021 09:23:47 +0100 Subject: [PATCH 0121/1429] JS: Update test output --- .../ql/test/library-tests/frameworks/SQL/SqlString.expected | 1 + 1 file changed, 1 insertion(+) diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected index 41bf9865b16..81338e00140 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected @@ -62,6 +62,7 @@ | spanner.js:19:23:19:32 | "SQL code" | | spanner.js:23:12:23:23 | 'SELECT 123' | | spanner.js:26:12:26:38 | 'UPDATE ... = @baz' | +| spanner.js:31:18:31:24 | queries | | spannerImport.js:4:8:4:17 | "SQL code" | | sqlite-types.ts:4:12:4:49 | "UPDATE ... id = ?" | | sqlite.js:7:8:7:45 | "UPDATE ... id = ?" | From 8159098dc0e47627a2386d8682e36f3d0445ca3e Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 31 Mar 2021 11:32:01 +0200 Subject: [PATCH 0122/1429] C++: Add test from issue #5190. --- .../dataflow/taint-tests/smart_pointer.cpp | 10 ++++++++++ cpp/ql/test/library-tests/dataflow/taint-tests/stl.h | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/smart_pointer.cpp b/cpp/ql/test/library-tests/dataflow/taint-tests/smart_pointer.cpp index f45260228e8..1f222cbae3e 100644 --- a/cpp/ql/test/library-tests/dataflow/taint-tests/smart_pointer.cpp +++ b/cpp/ql/test/library-tests/dataflow/taint-tests/smart_pointer.cpp @@ -65,4 +65,14 @@ void test_shared_field_member() { std::unique_ptr p = std::make_unique(source(), 0); sink(p->x); // $ MISSING: ast,ir sink(p->y); // not tainted +} + +void getNumber(std::shared_ptr ptr) { + *ptr = source(); +} + +int test_from_issue_5190() { + std::shared_ptr p(new int); + getNumber(p); + sink(*p); // $ MISSING: ast,ir } \ No newline at end of file diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/stl.h b/cpp/ql/test/library-tests/dataflow/taint-tests/stl.h index 09e77c5a3b6..1382552bdca 100644 --- a/cpp/ql/test/library-tests/dataflow/taint-tests/stl.h +++ b/cpp/ql/test/library-tests/dataflow/taint-tests/stl.h @@ -348,7 +348,7 @@ namespace std { class shared_ptr { public: shared_ptr() noexcept; - explicit shared_ptr(T*); + explicit shared_ptr(T*); shared_ptr(const shared_ptr&) noexcept; template shared_ptr(const shared_ptr&) noexcept; template shared_ptr(shared_ptr&&) noexcept; From 9ff894bf839a098aad951f8d5df23b6f339da14b Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 31 Mar 2021 11:42:09 +0200 Subject: [PATCH 0123/1429] C++: Add support for AST dataflow out of functions that take a smart pointer by value. --- .../cpp/dataflow/internal/DataFlowUtil.qll | 14 ++++ .../code/cpp/dataflow/internal/FlowVar.qll | 72 ++++++++++++++++++- .../dataflow/internal/TaintTrackingUtil.qll | 6 +- cpp/ql/src/semmle/code/cpp/exprs/Call.qll | 1 + .../dataflow/taint-tests/localTaint.expected | 22 ++++++ .../dataflow/taint-tests/smart_pointer.cpp | 10 +-- 6 files changed, 118 insertions(+), 7 deletions(-) diff --git a/cpp/ql/src/semmle/code/cpp/dataflow/internal/DataFlowUtil.qll b/cpp/ql/src/semmle/code/cpp/dataflow/internal/DataFlowUtil.qll index 0a6d459ec79..baf5f72b75b 100644 --- a/cpp/ql/src/semmle/code/cpp/dataflow/internal/DataFlowUtil.qll +++ b/cpp/ql/src/semmle/code/cpp/dataflow/internal/DataFlowUtil.qll @@ -199,6 +199,8 @@ class DefinitionByReferenceOrIteratorNode extends PartialDefinitionNode { this.getPartialDefinition() instanceof DefinitionByReference or this.getPartialDefinition() instanceof DefinitionByIterator + or + this.getPartialDefinition() instanceof DefinitionBySmartPointer ) } @@ -330,6 +332,18 @@ class IteratorPartialDefinitionNode extends PartialDefinitionNode { override Node getPreUpdateNode() { pd.definesExpressions(_, result.asExpr()) } } +/** + * INTERNAL: do not use. + * + * A synthetic data flow node used for flow into a collection when a smart pointer + * write occurs in a callee. + */ +class SmartPointerPartialDefinitionNode extends PartialDefinitionNode { + override SmartPointerPartialDefinition pd; + + override Node getPreUpdateNode() { pd.definesExpressions(_, result.asExpr()) } +} + /** * A post-update node on the `e->f` in `f(&e->f)` (and other forms). */ diff --git a/cpp/ql/src/semmle/code/cpp/dataflow/internal/FlowVar.qll b/cpp/ql/src/semmle/code/cpp/dataflow/internal/FlowVar.qll index f3aa94a7992..21ba4dff36d 100644 --- a/cpp/ql/src/semmle/code/cpp/dataflow/internal/FlowVar.qll +++ b/cpp/ql/src/semmle/code/cpp/dataflow/internal/FlowVar.qll @@ -7,6 +7,7 @@ private import semmle.code.cpp.controlflow.SSA private import semmle.code.cpp.dataflow.internal.SubBasicBlocks private import semmle.code.cpp.dataflow.internal.AddressFlow private import semmle.code.cpp.models.implementations.Iterator +private import semmle.code.cpp.models.interfaces.PointerWrapper /** * A conceptual variable that is assigned only once, like an SSA variable. This @@ -243,6 +244,54 @@ private module PartialDefinitions { } } + class SmartPointerPartialDefinition extends PartialDefinition { + Variable pointer; + Expr innerDefinedExpr; + + SmartPointerPartialDefinition() { + exists(Expr convertedInner | + not this instanceof Conversion and + valueToUpdate(convertedInner, this.getFullyConverted(), node) and + innerDefinedExpr = convertedInner.getUnconverted() and + innerDefinedExpr = getAPointerWrapperAccess(pointer) + ) + or + // iterators passed by value without a copy constructor + exists(Call call | + call = node and + call.getAnArgument() = innerDefinedExpr and + innerDefinedExpr = this and + this = getAPointerWrapperAccess(pointer) and + not call instanceof OverloadedPointerDereferenceExpr + ) + or + // iterators passed by value with a copy constructor + exists(Call call, ConstructorCall copy | + copy.getTarget() instanceof CopyConstructor and + call = node and + call.getAnArgument() = copy and + copy.getArgument(0) = getAPointerWrapperAccess(pointer) and + innerDefinedExpr = this and + this = copy and + not call instanceof OverloadedPointerDereferenceExpr + ) + } + + deprecated override predicate partiallyDefines(Variable v) { v = pointer } + + deprecated override predicate partiallyDefinesThis(ThisExpr e) { none() } + + override predicate definesExpressions(Expr inner, Expr outer) { + inner = innerDefinedExpr and + outer = this + } + + override predicate partiallyDefinesVariableAt(Variable v, ControlFlowNode cfn) { + v = pointer and + cfn = node + } + } + /** * A partial definition that's a definition via an output iterator. */ @@ -256,6 +305,15 @@ private module PartialDefinitions { class DefinitionByReference extends VariablePartialDefinition { DefinitionByReference() { exists(Call c | this = c.getAnArgument() or this = c.getQualifier()) } } + + /** + * A partial definition that's a definition via a smart pointer being passed into a function. + */ + class DefinitionBySmartPointer extends SmartPointerPartialDefinition { + DefinitionBySmartPointer() { + exists(Call c | this = c.getAnArgument() or this = c.getQualifier()) + } + } } import PartialDefinitions @@ -296,7 +354,8 @@ module FlowVar_internal { // treating them as immutable, but for data flow it gives better results in // practice to make the variable synonymous with its contents. not v.getUnspecifiedType() instanceof ReferenceType and - not v instanceof IteratorParameter + not v instanceof IteratorParameter and + not v instanceof PointerWrapperParameter } /** @@ -648,6 +707,8 @@ module FlowVar_internal { ) or p instanceof IteratorParameter + or + p instanceof PointerWrapperParameter } /** @@ -832,10 +893,19 @@ module FlowVar_internal { ) } + Call getAPointerWrapperAccess(Variable pointer) { + pointer.getUnspecifiedType() instanceof PointerWrapper and + [result.getQualifier(), result.getAnArgument()] = pointer.getAnAccess() + } + class IteratorParameter extends Parameter { IteratorParameter() { this.getUnspecifiedType() instanceof Iterator } } + class PointerWrapperParameter extends Parameter { + PointerWrapperParameter() { this.getUnspecifiedType() instanceof PointerWrapper } + } + /** * Holds if `v` is initialized to have value `assignedExpr`. */ diff --git a/cpp/ql/src/semmle/code/cpp/dataflow/internal/TaintTrackingUtil.qll b/cpp/ql/src/semmle/code/cpp/dataflow/internal/TaintTrackingUtil.qll index 1ef340c4f21..591f461c8eb 100644 --- a/cpp/ql/src/semmle/code/cpp/dataflow/internal/TaintTrackingUtil.qll +++ b/cpp/ql/src/semmle/code/cpp/dataflow/internal/TaintTrackingUtil.qll @@ -11,6 +11,7 @@ private import semmle.code.cpp.models.interfaces.DataFlow private import semmle.code.cpp.models.interfaces.Taint private import semmle.code.cpp.models.interfaces.Iterator +private import semmle.code.cpp.models.interfaces.PointerWrapper private module DataFlow { import semmle.code.cpp.dataflow.internal.DataFlowUtil @@ -141,7 +142,10 @@ private predicate noFlowFromChildExpr(Expr e) { or e instanceof LogicalOrExpr or - e instanceof Call + // Allow taint from `operator*` on smart pointers. + exists(Call call | e = call | + not call.getTarget() = any(PointerWrapper wrapper).getAnUnwrapperFunction() + ) or e instanceof SizeofOperator or diff --git a/cpp/ql/src/semmle/code/cpp/exprs/Call.qll b/cpp/ql/src/semmle/code/cpp/exprs/Call.qll index 349efdcee10..6f6f710ac4b 100644 --- a/cpp/ql/src/semmle/code/cpp/exprs/Call.qll +++ b/cpp/ql/src/semmle/code/cpp/exprs/Call.qll @@ -314,6 +314,7 @@ class OverloadedPointerDereferenceFunction extends Function { * T1 operator*(const T2 &); * T1 a; T2 b; * a = *b; + * ``` */ class OverloadedPointerDereferenceExpr extends FunctionCall { OverloadedPointerDereferenceExpr() { diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected b/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected index 4444ea13267..9b1ad0d4f10 100644 --- a/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected +++ b/cpp/ql/test/library-tests/dataflow/taint-tests/localTaint.expected @@ -3223,24 +3223,30 @@ | smart_pointer.cpp:11:30:11:50 | call to make_shared | smart_pointer.cpp:12:11:12:11 | p | | | smart_pointer.cpp:11:30:11:50 | call to make_shared | smart_pointer.cpp:13:10:13:10 | p | | | smart_pointer.cpp:11:52:11:57 | call to source | smart_pointer.cpp:11:30:11:50 | call to make_shared | TAINT | +| smart_pointer.cpp:12:10:12:10 | call to operator* [post update] | smart_pointer.cpp:13:10:13:10 | p | | | smart_pointer.cpp:12:11:12:11 | p | smart_pointer.cpp:12:10:12:10 | call to operator* | TAINT | | smart_pointer.cpp:12:11:12:11 | ref arg p | smart_pointer.cpp:13:10:13:10 | p | | | smart_pointer.cpp:17:32:17:54 | call to make_shared | smart_pointer.cpp:18:11:18:11 | p | | | smart_pointer.cpp:17:32:17:54 | call to make_shared | smart_pointer.cpp:19:10:19:10 | p | | +| smart_pointer.cpp:18:10:18:10 | ref arg call to operator* | smart_pointer.cpp:19:10:19:10 | p | | | smart_pointer.cpp:18:11:18:11 | p | smart_pointer.cpp:18:10:18:10 | call to operator* | TAINT | | smart_pointer.cpp:18:11:18:11 | ref arg p | smart_pointer.cpp:19:10:19:10 | p | | | smart_pointer.cpp:23:30:23:50 | call to make_unique | smart_pointer.cpp:24:11:24:11 | p | | | smart_pointer.cpp:23:30:23:50 | call to make_unique | smart_pointer.cpp:25:10:25:10 | p | | | smart_pointer.cpp:23:52:23:57 | call to source | smart_pointer.cpp:23:30:23:50 | call to make_unique | TAINT | +| smart_pointer.cpp:24:10:24:10 | call to operator* [post update] | smart_pointer.cpp:25:10:25:10 | p | | | smart_pointer.cpp:24:11:24:11 | p | smart_pointer.cpp:24:10:24:10 | call to operator* | TAINT | | smart_pointer.cpp:24:11:24:11 | ref arg p | smart_pointer.cpp:25:10:25:10 | p | | | smart_pointer.cpp:29:32:29:54 | call to make_unique | smart_pointer.cpp:30:11:30:11 | p | | | smart_pointer.cpp:29:32:29:54 | call to make_unique | smart_pointer.cpp:31:10:31:10 | p | | +| smart_pointer.cpp:30:10:30:10 | ref arg call to operator* | smart_pointer.cpp:31:10:31:10 | p | | | smart_pointer.cpp:30:11:30:11 | p | smart_pointer.cpp:30:10:30:10 | call to operator* | TAINT | | smart_pointer.cpp:30:11:30:11 | ref arg p | smart_pointer.cpp:31:10:31:10 | p | | | smart_pointer.cpp:35:30:35:50 | call to make_shared | smart_pointer.cpp:37:6:37:6 | p | | | smart_pointer.cpp:35:30:35:50 | call to make_shared | smart_pointer.cpp:38:10:38:10 | p | | | smart_pointer.cpp:35:30:35:50 | call to make_shared | smart_pointer.cpp:39:11:39:11 | p | | +| smart_pointer.cpp:37:5:37:5 | call to operator* [post update] | smart_pointer.cpp:38:10:38:10 | p | | +| smart_pointer.cpp:37:5:37:5 | call to operator* [post update] | smart_pointer.cpp:39:11:39:11 | p | | | smart_pointer.cpp:37:5:37:17 | ... = ... | smart_pointer.cpp:37:5:37:5 | call to operator* [post update] | | | smart_pointer.cpp:37:6:37:6 | p | smart_pointer.cpp:37:5:37:5 | call to operator* | TAINT | | smart_pointer.cpp:37:6:37:6 | ref arg p | smart_pointer.cpp:38:10:38:10 | p | | @@ -3251,6 +3257,8 @@ | smart_pointer.cpp:43:29:43:51 | call to unique_ptr | smart_pointer.cpp:45:6:45:6 | p | | | smart_pointer.cpp:43:29:43:51 | call to unique_ptr | smart_pointer.cpp:46:10:46:10 | p | | | smart_pointer.cpp:43:29:43:51 | call to unique_ptr | smart_pointer.cpp:47:11:47:11 | p | | +| smart_pointer.cpp:45:5:45:5 | call to operator* [post update] | smart_pointer.cpp:46:10:46:10 | p | | +| smart_pointer.cpp:45:5:45:5 | call to operator* [post update] | smart_pointer.cpp:47:11:47:11 | p | | | smart_pointer.cpp:45:5:45:17 | ... = ... | smart_pointer.cpp:45:5:45:5 | call to operator* [post update] | | | smart_pointer.cpp:45:6:45:6 | p | smart_pointer.cpp:45:5:45:5 | call to operator* | TAINT | | smart_pointer.cpp:45:6:45:6 | ref arg p | smart_pointer.cpp:46:10:46:10 | p | | @@ -3268,7 +3276,21 @@ | smart_pointer.cpp:65:28:65:46 | call to make_unique | smart_pointer.cpp:67:10:67:10 | p | | | smart_pointer.cpp:65:48:65:53 | call to source | smart_pointer.cpp:65:28:65:46 | call to make_unique | TAINT | | smart_pointer.cpp:65:58:65:58 | 0 | smart_pointer.cpp:65:28:65:46 | call to make_unique | TAINT | +| smart_pointer.cpp:66:10:66:10 | p | smart_pointer.cpp:66:11:66:11 | call to operator-> | TAINT | | smart_pointer.cpp:66:10:66:10 | ref arg p | smart_pointer.cpp:67:10:67:10 | p | | +| smart_pointer.cpp:67:10:67:10 | p | smart_pointer.cpp:67:11:67:11 | call to operator-> | TAINT | +| smart_pointer.cpp:70:37:70:39 | ptr | smart_pointer.cpp:70:37:70:39 | ptr | | +| smart_pointer.cpp:70:37:70:39 | ptr | smart_pointer.cpp:71:4:71:6 | ptr | | +| smart_pointer.cpp:71:3:71:3 | call to operator* [post update] | smart_pointer.cpp:70:37:70:39 | ptr | | +| smart_pointer.cpp:71:3:71:17 | ... = ... | smart_pointer.cpp:71:3:71:3 | call to operator* [post update] | | +| smart_pointer.cpp:71:4:71:6 | ptr | smart_pointer.cpp:71:3:71:3 | call to operator* | TAINT | +| smart_pointer.cpp:71:4:71:6 | ref arg ptr | smart_pointer.cpp:70:37:70:39 | ptr | | +| smart_pointer.cpp:71:10:71:15 | call to source | smart_pointer.cpp:71:3:71:17 | ... = ... | | +| smart_pointer.cpp:75:26:75:33 | call to shared_ptr | smart_pointer.cpp:76:13:76:13 | p | | +| smart_pointer.cpp:75:26:75:33 | call to shared_ptr | smart_pointer.cpp:77:9:77:9 | p | | +| smart_pointer.cpp:76:13:76:13 | call to shared_ptr [post update] | smart_pointer.cpp:77:9:77:9 | p | | +| smart_pointer.cpp:76:13:76:13 | p | smart_pointer.cpp:76:13:76:13 | call to shared_ptr | | +| smart_pointer.cpp:77:9:77:9 | p | smart_pointer.cpp:77:8:77:8 | call to operator* | TAINT | | standalone_iterators.cpp:39:45:39:51 | source1 | standalone_iterators.cpp:39:45:39:51 | source1 | | | standalone_iterators.cpp:39:45:39:51 | source1 | standalone_iterators.cpp:40:11:40:17 | source1 | | | standalone_iterators.cpp:39:45:39:51 | source1 | standalone_iterators.cpp:41:12:41:18 | source1 | | diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/smart_pointer.cpp b/cpp/ql/test/library-tests/dataflow/taint-tests/smart_pointer.cpp index 1f222cbae3e..e5d835d3426 100644 --- a/cpp/ql/test/library-tests/dataflow/taint-tests/smart_pointer.cpp +++ b/cpp/ql/test/library-tests/dataflow/taint-tests/smart_pointer.cpp @@ -35,16 +35,16 @@ void test_reverse_taint_shared() { std::shared_ptr p = std::make_shared(); *p = source(); - sink(p); // $ MISSING: ast,ir - sink(*p); // $ MISSING: ast,ir + sink(p); // $ ast MISSING: ir + sink(*p); // $ ast MISSING: ir } void test_reverse_taint_unique() { std::unique_ptr p = std::unique_ptr(); *p = source(); - sink(p); // $ MISSING: ast,ir - sink(*p); // $ MISSING: ast,ir + sink(p); // $ ast MISSING: ir + sink(*p); // $ ast MISSING: ir } void test_shared_get() { @@ -74,5 +74,5 @@ void getNumber(std::shared_ptr ptr) { int test_from_issue_5190() { std::shared_ptr p(new int); getNumber(p); - sink(*p); // $ MISSING: ast,ir + sink(*p); // $ ast MISSING: ir } \ No newline at end of file From 43306f4700e1e330abdaeb7c02f80d7288c11670 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Wed, 31 Mar 2021 17:17:58 +0200 Subject: [PATCH 0124/1429] Python: Add tests for Module.declaredInAll --- .../modules/__all__/DeclaredInAll.expected | 3 + .../modules/__all__/DeclaredInAll.ql | 5 ++ .../modules/__all__/all_dynamic.py | 8 ++ .../library-tests/modules/__all__/all_list.py | 6 ++ .../library-tests/modules/__all__/all_set.py | 6 ++ .../modules/__all__/all_tuple.py | 6 ++ .../library-tests/modules/__all__/main.py | 74 +++++++++++++++++++ .../library-tests/modules/__all__/no_all.py | 3 + 8 files changed, 111 insertions(+) create mode 100644 python/ql/test/library-tests/modules/__all__/DeclaredInAll.expected create mode 100644 python/ql/test/library-tests/modules/__all__/DeclaredInAll.ql create mode 100644 python/ql/test/library-tests/modules/__all__/all_dynamic.py create mode 100644 python/ql/test/library-tests/modules/__all__/all_list.py create mode 100644 python/ql/test/library-tests/modules/__all__/all_set.py create mode 100644 python/ql/test/library-tests/modules/__all__/all_tuple.py create mode 100644 python/ql/test/library-tests/modules/__all__/main.py create mode 100644 python/ql/test/library-tests/modules/__all__/no_all.py diff --git a/python/ql/test/library-tests/modules/__all__/DeclaredInAll.expected b/python/ql/test/library-tests/modules/__all__/DeclaredInAll.expected new file mode 100644 index 00000000000..5d90821921b --- /dev/null +++ b/python/ql/test/library-tests/modules/__all__/DeclaredInAll.expected @@ -0,0 +1,3 @@ +| all_dynamic.py:0:0:0:0 | Module all_dynamic | foo | +| all_list.py:0:0:0:0 | Module all_list | bar | +| all_list.py:0:0:0:0 | Module all_list | foo | diff --git a/python/ql/test/library-tests/modules/__all__/DeclaredInAll.ql b/python/ql/test/library-tests/modules/__all__/DeclaredInAll.ql new file mode 100644 index 00000000000..69f9d32f856 --- /dev/null +++ b/python/ql/test/library-tests/modules/__all__/DeclaredInAll.ql @@ -0,0 +1,5 @@ +import python + +from Module mod, string name +where mod.declaredInAll(name) +select mod, name diff --git a/python/ql/test/library-tests/modules/__all__/all_dynamic.py b/python/ql/test/library-tests/modules/__all__/all_dynamic.py new file mode 100644 index 00000000000..5213418328d --- /dev/null +++ b/python/ql/test/library-tests/modules/__all__/all_dynamic.py @@ -0,0 +1,8 @@ +foo = "foo" +bar = "bar" +baz = "baz" + + +__all__ = ["foo"] + +__all__ += ["bar"] diff --git a/python/ql/test/library-tests/modules/__all__/all_list.py b/python/ql/test/library-tests/modules/__all__/all_list.py new file mode 100644 index 00000000000..d2ecda63db4 --- /dev/null +++ b/python/ql/test/library-tests/modules/__all__/all_list.py @@ -0,0 +1,6 @@ +foo = "foo" +bar = "bar" +baz = "baz" + + +__all__ = ["foo", "bar"] diff --git a/python/ql/test/library-tests/modules/__all__/all_set.py b/python/ql/test/library-tests/modules/__all__/all_set.py new file mode 100644 index 00000000000..bbd7d0d4c59 --- /dev/null +++ b/python/ql/test/library-tests/modules/__all__/all_set.py @@ -0,0 +1,6 @@ +foo = "foo" +bar = "bar" +baz = "baz" + + +__all__ = {"foo", "bar"} diff --git a/python/ql/test/library-tests/modules/__all__/all_tuple.py b/python/ql/test/library-tests/modules/__all__/all_tuple.py new file mode 100644 index 00000000000..b06def5ddb7 --- /dev/null +++ b/python/ql/test/library-tests/modules/__all__/all_tuple.py @@ -0,0 +1,6 @@ +foo = "foo" +bar = "bar" +baz = "baz" + + +__all__ = ("foo", "bar") diff --git a/python/ql/test/library-tests/modules/__all__/main.py b/python/ql/test/library-tests/modules/__all__/main.py new file mode 100644 index 00000000000..40b63287638 --- /dev/null +++ b/python/ql/test/library-tests/modules/__all__/main.py @@ -0,0 +1,74 @@ +# This file showcases how imports work with `__all__`. +# +# TL;DR; in `from import *`, if `__all__` is defined in ``, then only +# the names in `__all__` will be imported -- otherwise all names that doesn't begin with +# an underscore will be imported. +# +# If `__all__` is defined, `__all__` must be a sequence for `from import *` to work. +# +# https://docs.python.org/3/reference/simple_stmts.html#the-import-statement +# https://docs.python.org/3/glossary.html#term-sequence + +print("import *") +print("---") + +from no_all import * +print("no_all.py") +print(" foo={!r}".format(foo)) +print(" bar={!r}".format(bar)) +print(" baz={!r}".format(baz)) +del foo, bar, baz + +from all_list import * +print("all_list.py") +print(" foo={!r}".format(foo)) +print(" bar={!r}".format(bar)) +try: + print(" baz={!r}".format(baz)) +except NameError: + print(" baz not imported") +del foo, bar + +from all_tuple import * +print("all_tuple.py") +print(" foo={!r}".format(foo)) +print(" bar={!r}".format(bar)) +try: + print(" baz={!r}".format(baz)) +except NameError: + print(" baz not imported") +del foo, bar + +from all_dynamic import * +print("all_dynamic.py") +print(" foo={!r}".format(foo)) +print(" bar={!r}".format(bar)) +try: + print(" baz={!r}".format(baz)) +except NameError: + print(" baz not imported") +del foo, bar + + +# Example of wrong definition of `__all__`, where it is not a sequence. +try: + from all_set import * +except TypeError as e: + assert str(e) == "'set' object does not support indexing" + print("from all_set import * could not be imported:", e) + +print("") +print("Direct reference on module") +print("---") +# Direct reference always works, no matter how `__all__` is set. +import no_all +import all_list +import all_tuple +import all_dynamic +import all_set + +for mod in [no_all, all_list, all_tuple, all_dynamic, all_set]: + print("{}.py".format(mod.__name__)) + print(" foo={!r}".format(mod.foo)) + print(" bar={!r}".format(mod.bar)) + print(" baz={!r}".format(mod.baz)) diff --git a/python/ql/test/library-tests/modules/__all__/no_all.py b/python/ql/test/library-tests/modules/__all__/no_all.py new file mode 100644 index 00000000000..ca059c60687 --- /dev/null +++ b/python/ql/test/library-tests/modules/__all__/no_all.py @@ -0,0 +1,3 @@ +foo = "foo" +bar = "bar" +baz = "baz" From ab3edf37d70d9e69b7e7226de3ea7ce8a8f973ae Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Wed, 31 Mar 2021 17:25:19 +0200 Subject: [PATCH 0125/1429] Python: Handle __all__ assigned to a tuple Examples where this is used in real code: - https://github.com/django/django/blob/76c0b32f826469320c59709d31e2f2126dd7c505/django/core/files/temp.py#L24 - https://github.com/django/django/blob/76c0b32f826469320c59709d31e2f2126dd7c505/django/contrib/gis/gdal/__init__.py#L44-L49 --- python/ql/src/semmle/python/Module.qll | 6 +++++- .../library-tests/modules/__all__/DeclaredInAll.expected | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/python/ql/src/semmle/python/Module.qll b/python/ql/src/semmle/python/Module.qll index fcf1c0b2925..bc100e262f4 100644 --- a/python/ql/src/semmle/python/Module.qll +++ b/python/ql/src/semmle/python/Module.qll @@ -129,7 +129,11 @@ class Module extends Module_, Scope, AstNode { a.defines(all) and a.getScope() = this and all.getId() = "__all__" and - a.getValue().(List).getAnElt().(StrConst).getText() = name + ( + a.getValue().(List).getAnElt().(StrConst).getText() = name + or + a.getValue().(Tuple).getAnElt().(StrConst).getText() = name + ) ) } diff --git a/python/ql/test/library-tests/modules/__all__/DeclaredInAll.expected b/python/ql/test/library-tests/modules/__all__/DeclaredInAll.expected index 5d90821921b..86591566217 100644 --- a/python/ql/test/library-tests/modules/__all__/DeclaredInAll.expected +++ b/python/ql/test/library-tests/modules/__all__/DeclaredInAll.expected @@ -1,3 +1,5 @@ | all_dynamic.py:0:0:0:0 | Module all_dynamic | foo | | all_list.py:0:0:0:0 | Module all_list | bar | | all_list.py:0:0:0:0 | Module all_list | foo | +| all_tuple.py:0:0:0:0 | Module all_tuple | bar | +| all_tuple.py:0:0:0:0 | Module all_tuple | foo | From 95ac2c8edda7f845bfa60df47d77ecc9fbec9d93 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Wed, 31 Mar 2021 17:31:55 +0200 Subject: [PATCH 0126/1429] Python: Add another dynamic __all__ test --- .../library-tests/modules/__all__/all_dynamic2.py | 7 +++++++ python/ql/test/library-tests/modules/__all__/main.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 python/ql/test/library-tests/modules/__all__/all_dynamic2.py diff --git a/python/ql/test/library-tests/modules/__all__/all_dynamic2.py b/python/ql/test/library-tests/modules/__all__/all_dynamic2.py new file mode 100644 index 00000000000..88c0ddaeacc --- /dev/null +++ b/python/ql/test/library-tests/modules/__all__/all_dynamic2.py @@ -0,0 +1,7 @@ +foo = "foo" +bar = "bar" +baz = "baz" + + +temp = ["foo", "bar"] +__all__ = temp diff --git a/python/ql/test/library-tests/modules/__all__/main.py b/python/ql/test/library-tests/modules/__all__/main.py index 40b63287638..a36afae0ed4 100644 --- a/python/ql/test/library-tests/modules/__all__/main.py +++ b/python/ql/test/library-tests/modules/__all__/main.py @@ -49,6 +49,15 @@ except NameError: print(" baz not imported") del foo, bar +from all_dynamic2 import * +print("all_dynamic2.py") +print(" foo={!r}".format(foo)) +print(" bar={!r}".format(bar)) +try: + print(" baz={!r}".format(baz)) +except NameError: + print(" baz not imported") +del foo, bar # Example of wrong definition of `__all__`, where it is not a sequence. try: @@ -65,9 +74,10 @@ import no_all import all_list import all_tuple import all_dynamic +import all_dynamic2 import all_set -for mod in [no_all, all_list, all_tuple, all_dynamic, all_set]: +for mod in [no_all, all_list, all_tuple, all_dynamic, all_dynamic2, all_set]: print("{}.py".format(mod.__name__)) print(" foo={!r}".format(mod.foo)) print(" bar={!r}".format(mod.bar)) From ecbce88ec78710f67c0160f7ae58e58c027fb73c Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 31 Mar 2021 22:23:50 +0200 Subject: [PATCH 0127/1429] C++: Fix comment. --- cpp/ql/src/semmle/code/cpp/dataflow/internal/FlowVar.qll | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/ql/src/semmle/code/cpp/dataflow/internal/FlowVar.qll b/cpp/ql/src/semmle/code/cpp/dataflow/internal/FlowVar.qll index 21ba4dff36d..29b6cd691a4 100644 --- a/cpp/ql/src/semmle/code/cpp/dataflow/internal/FlowVar.qll +++ b/cpp/ql/src/semmle/code/cpp/dataflow/internal/FlowVar.qll @@ -256,7 +256,7 @@ private module PartialDefinitions { innerDefinedExpr = getAPointerWrapperAccess(pointer) ) or - // iterators passed by value without a copy constructor + // pointer wrappers passed by value without a copy constructor exists(Call call | call = node and call.getAnArgument() = innerDefinedExpr and @@ -265,7 +265,7 @@ private module PartialDefinitions { not call instanceof OverloadedPointerDereferenceExpr ) or - // iterators passed by value with a copy constructor + // pointer wrappers passed by value with a copy constructor exists(Call call, ConstructorCall copy | copy.getTarget() instanceof CopyConstructor and call = node and From 4328ff398121d12f800a67a3edc0cd72ea2bf77e Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 31 Mar 2021 22:26:08 +0200 Subject: [PATCH 0128/1429] Remove attrs feature --- .../experimental/Security/CWE-090/LDAPInjection.ql | 5 ++--- python/ql/src/experimental/semmle/python/Concepts.qll | 4 ---- .../experimental/semmle/python/frameworks/Stdlib.qll | 11 ----------- .../python/security/injection/LDAPInjection.qll | 5 +---- 4 files changed, 3 insertions(+), 22 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql index a803b39260e..8d01fc173d4 100644 --- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql +++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql @@ -19,7 +19,6 @@ from LDAPInjectionSink castedSink where config.hasFlowPath(source, sink) and - castedSink.getLDAPNode() = sink.getNode() //and -// if exists(castedSink.getAttrs()) then + castedSink.getLDAPNode() = sink.getNode() select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@.", castedSink, "This", - source.getNode(), "a user-provided value", castedSink.getLDAPNode(), castedSink.getLDAPPart() //, castedSink.getAttrs(), "probably leaking this attribute(s)" + source.getNode(), "a user-provided value", castedSink.getLDAPNode(), castedSink.getLDAPPart() diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index e0f5c1bdec3..e62d9680665 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -20,8 +20,6 @@ module LDAPQuery { abstract DataFlow::Node getLDAPNode(); abstract string getLDAPPart(); - - abstract DataFlow::Node getAttrs(); } } @@ -33,8 +31,6 @@ class LDAPQuery extends DataFlow::Node { DataFlow::Node getLDAPNode() { result = range.getLDAPNode() } string getLDAPPart() { result = range.getLDAPPart() } - - DataFlow::Node getAttrs() { result = range.getAttrs() } } module LDAPEscape { diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll index 348367d0370..f56a6603ec7 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll @@ -21,7 +21,6 @@ private module LDAP { private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range { DataFlow::Node ldapNode; string ldapPart; - DataFlow::Node attrs; LDAP2Query() { exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode initCall | @@ -45,10 +44,6 @@ private module LDAP { override DataFlow::Node getLDAPNode() { result = ldapNode } override string getLDAPPart() { result = ldapPart } - - override DataFlow::Node getAttrs() { - result = this.getArg(3) or result = this.getArgByName("attrlist") - } } private class LDAP2EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range { @@ -77,14 +72,12 @@ private module LDAP { private module LDAP3 { private class LDAP3QueryMethods extends string { - // pending to dig into this although https://github.com/cannatag/ldap3/blob/21001d9087c0d24c399eec433a261c455b7bc97f/ldap3/core/connection.py#L760 LDAP3QueryMethods() { this in ["search"] } } private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range { DataFlow::Node ldapNode; string ldapPart; - DataFlow::Node attrs; LDAP3Query() { exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode connCall | @@ -105,10 +98,6 @@ private module LDAP { override DataFlow::Node getLDAPNode() { result = ldapNode } override string getLDAPPart() { result = ldapPart } - - override DataFlow::Node getAttrs() { - result = this.getArg(3) or result = this.getArgByName("attributes") - } } private class LDAP3EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range { diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll index 4814097ff8e..da71f38457a 100644 --- a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll +++ b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll @@ -9,7 +9,6 @@ import semmle.python.dataflow.new.TaintTracking import semmle.python.dataflow.new.RemoteFlowSources class LDAPInjectionSink extends DataFlow::Node { - // DataFlow::Node attrs; DataFlow::Node ldapNode; string ldapPart; @@ -17,15 +16,13 @@ class LDAPInjectionSink extends DataFlow::Node { exists(LDAPQuery ldapQuery | this = ldapQuery and ldapNode = ldapQuery.getLDAPNode() and - ldapPart = ldapQuery.getLDAPPart() // and - // if exists(ldapQuery.getAttrs()) then attrs = ldapQuery.getAttrs() + ldapPart = ldapQuery.getLDAPPart() ) } DataFlow::Node getLDAPNode() { result = ldapNode } string getLDAPPart() { result = ldapPart } - // DataFlow::Node getAttrs() { result = attrs } } /** From 9b430310b4ecd9535191262249d889d0b9306025 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 31 Mar 2021 23:19:56 +0200 Subject: [PATCH 0129/1429] Improve Sanitizer calls --- .../semmle/python/frameworks/Stdlib.qll | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll index f56a6603ec7..cfd02b8b5a5 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll @@ -46,27 +46,21 @@ private module LDAP { override string getLDAPPart() { result = ldapPart } } - private class LDAP2EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range { - DataFlow::Node escapeNode; - - LDAP2EscapeDN() { - this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall() and - escapeNode = this.getArg(0) + private class LDAP2EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range { + LDAP2EscapeDNCall() { + this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall() } - override DataFlow::Node getEscapeNode() { result = escapeNode } + override DataFlow::Node getEscapeNode() { result = this.getArg(0) } } - private class LDAP2EscapeFilter extends DataFlow::CallCfgNode, LDAPEscape::Range { - DataFlow::Node escapeNode; - - LDAP2EscapeFilter() { + private class LDAP2EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range { + LDAP2EscapeFilterCall() { this = - API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall() and - escapeNode = this.getArg(0) + API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall() } - override DataFlow::Node getEscapeNode() { result = escapeNode } + override DataFlow::Node getEscapeNode() { result = this.getArg(0) } } } @@ -100,36 +94,30 @@ private module LDAP { override string getLDAPPart() { result = ldapPart } } - private class LDAP3EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range { - DataFlow::Node escapeNode; - - LDAP3EscapeDN() { + private class LDAP3EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range { + LDAP3EscapeDNCall() { this = API::moduleImport("ldap3") .getMember("utils") .getMember("dn") .getMember("escape_rdn") - .getACall() and - escapeNode = this.getArg(0) + .getACall() } - override DataFlow::Node getEscapeNode() { result = escapeNode } + override DataFlow::Node getEscapeNode() { result = this.getArg(0) } } - private class LDAP3EscapeFilter extends DataFlow::CallCfgNode, LDAPEscape::Range { - DataFlow::Node escapeNode; - - LDAP3EscapeFilter() { + private class LDAP3EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range { + LDAP3EscapeFilterCall() { this = API::moduleImport("ldap3") .getMember("utils") .getMember("conv") .getMember("escape_filter_chars") - .getACall() and - escapeNode = this.getArg(0) + .getACall() } - override DataFlow::Node getEscapeNode() { result = escapeNode } + override DataFlow::Node getEscapeNode() { result = this.getArg(0) } } } } From 480ce39618efaa47d7a51b43a80a044aca7d6532 Mon Sep 17 00:00:00 2001 From: Luke Cartey <5377966+lcartey@users.noreply.github.com> Date: Thu, 1 Apr 2021 11:23:31 +0100 Subject: [PATCH 0130/1429] C#: Exclude jump-to-def information for elements with too many locations In databases which include multiple duplicated files, we can get an explosion of definition locations that can cause this query to produce too many results for the CodeQL toolchain. This commit restricts the definitions.ql query to producing definition/uses for definitions with fewer than 10 locations. This replicates the logic used in the C++ definitions.qll library which faces similar problems. --- csharp/ql/src/definitions.qll | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/csharp/ql/src/definitions.qll b/csharp/ql/src/definitions.qll index 2004ad0d218..c1b456f4dbf 100644 --- a/csharp/ql/src/definitions.qll +++ b/csharp/ql/src/definitions.qll @@ -187,5 +187,11 @@ cached Declaration definitionOf(Use use, string kind) { result = use.getDefinition() and result.fromSource() and - kind = use.getUseType() + kind = use.getUseType() and + // Some entities have many locations. This can arise for files that + // are duplicated multiple times in the database at different + // locations. Rather than letting the result set explode, we just + // exclude results that are "too ambiguous" -- we could also arbitrarily + // pick one location later on. + strictcount(result.getLocation()) < 10 } From a3421e7ab2e81e155fe211ca78ad25c1171cd5eb Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Thu, 12 Nov 2020 12:43:37 +0000 Subject: [PATCH 0131/1429] JS: Add getALocalUse --- javascript/ql/src/semmle/javascript/dataflow/Sources.qll | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/javascript/ql/src/semmle/javascript/dataflow/Sources.qll b/javascript/ql/src/semmle/javascript/dataflow/Sources.qll index efbac19b95d..e6d76898d26 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/Sources.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/Sources.qll @@ -52,6 +52,11 @@ class SourceNode extends DataFlow::Node { */ predicate flowsToExpr(Expr sink) { flowsTo(DataFlow::valueNode(sink)) } + /** + * Gets a node into which data may flow from this node in zero or more local steps. + */ + DataFlow::Node getALocalUse() { flowsTo(result) } + /** * Gets a reference (read or write) of property `propName` on this node. */ From 125d1465c8952c1cbddc0d005a6c9a34e37ec097 Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Mon, 25 Jan 2021 13:10:07 +0000 Subject: [PATCH 0132/1429] JS: Add DataFlow::functionForwardingStep --- .../semmle/javascript/dataflow/DataFlow.qll | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll b/javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll index 8835b14af05..8474598fef9 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll @@ -1683,4 +1683,59 @@ module DataFlow { import TypeTracking predicate localTaintStep = TaintTracking::localTaintStep/2; + + /** + * Holds if the function in `succ` forwards all its arguments to a call to `pred` and returns + * its result. This can thus be seen as a step `pred -> succ` used for tracking function values + * through "wrapper functions", since the `succ` function partially replicates behavior of `pred`. + * + * Examples: + * ```js + * function f(x) { + * return g(x); // step: g -> f + * } + * + * function doExec(x) { + * console.log(x); + * return exec(x); // step: exec -> doExec + * } + * + * function doEither(x, y) { + * if (x > y) { + * return foo(x, y); // step: foo -> doEither + * } else { + * return bar(x, y); // step: bar -> doEither + * } + * } + * + * function wrapWithLogging(f) { + * return (x) => { + * console.log(x); + * return f(x); // step: f -> anonymous function + * } + * } + * wrapWithLogging(g); // step: g -> wrapWithLogging(g) + * ``` + */ + predicate functionForwardingStep(DataFlow::Node pred, DataFlow::Node succ) { + exists(DataFlow::FunctionNode function, DataFlow::CallNode call | + call.flowsTo(function.getReturnNode()) and + forall(int i | exists([call.getArgument(i), function.getParameter(i)]) | + function.getParameter(i).flowsTo(call.getArgument(i)) + ) and + pred = call.getCalleeNode() and + succ = function + ) + or + // Given a generic wrapper function like, + // + // function wrap(f) { return (x, y) => f(x, y) }; + // + // add steps through calls to that function: `g -> wrap(g)` + exists(DataFlow::FunctionNode wrapperFunction, SourceNode param, Node paramUse | + FlowSteps::argumentPassing(succ, pred, wrapperFunction.getFunction(), param) and + param.flowsTo(paramUse) and + functionForwardingStep(paramUse, wrapperFunction.getReturnNode().getALocalSource()) + ) + } } From c1651ad30c1f7dfcec201ee75ec49d8975bb791a Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Mon, 25 Jan 2021 13:16:15 +0000 Subject: [PATCH 0133/1429] JS: Factor out Unit type --- javascript/ql/src/semmle/javascript/Unit.qll | 10 ++++++++++ .../src/semmle/javascript/dataflow/Configuration.qll | 2 +- .../src/semmle/javascript/dataflow/TaintTracking.qll | 2 +- .../javascript/dataflow/internal/PreCallGraphStep.qll | 7 +------ .../src/semmle/javascript/dataflow/internal/Unit.qll | 9 --------- 5 files changed, 13 insertions(+), 17 deletions(-) create mode 100644 javascript/ql/src/semmle/javascript/Unit.qll delete mode 100644 javascript/ql/src/semmle/javascript/dataflow/internal/Unit.qll diff --git a/javascript/ql/src/semmle/javascript/Unit.qll b/javascript/ql/src/semmle/javascript/Unit.qll new file mode 100644 index 00000000000..dbc59f541e6 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/Unit.qll @@ -0,0 +1,10 @@ +/** Provides the `Unit` class. */ + +/** The unit type. */ +private newtype TUnit = TMkUnit() + +/** The trivial type with a single element. */ +class Unit extends TUnit { + /** Gets a textual representation of this element. */ + string toString() { result = "Unit" } +} diff --git a/javascript/ql/src/semmle/javascript/dataflow/Configuration.qll b/javascript/ql/src/semmle/javascript/dataflow/Configuration.qll index 24f767540e1..b4842026e24 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/Configuration.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/Configuration.qll @@ -72,7 +72,7 @@ private import javascript private import internal.FlowSteps private import internal.AccessPaths private import internal.CallGraphs -private import internal.Unit +private import semmle.javascript.Unit private import semmle.javascript.internal.CachedStages /** diff --git a/javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll b/javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll index 6c6734b84d6..251a691501e 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll @@ -15,7 +15,7 @@ import javascript private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps -private import semmle.javascript.dataflow.internal.Unit +private import semmle.javascript.Unit private import semmle.javascript.dataflow.InferredTypes private import semmle.javascript.internal.CachedStages diff --git a/javascript/ql/src/semmle/javascript/dataflow/internal/PreCallGraphStep.qll b/javascript/ql/src/semmle/javascript/dataflow/internal/PreCallGraphStep.qll index 7020353ca0b..18db549300a 100644 --- a/javascript/ql/src/semmle/javascript/dataflow/internal/PreCallGraphStep.qll +++ b/javascript/ql/src/semmle/javascript/dataflow/internal/PreCallGraphStep.qll @@ -4,14 +4,9 @@ */ private import javascript +private import semmle.javascript.Unit private import semmle.javascript.internal.CachedStages -private newtype TUnit = MkUnit() - -private class Unit extends TUnit { - string toString() { result = "unit" } -} - /** * Internal extension point for adding flow edges prior to call graph construction * and type tracking. diff --git a/javascript/ql/src/semmle/javascript/dataflow/internal/Unit.qll b/javascript/ql/src/semmle/javascript/dataflow/internal/Unit.qll deleted file mode 100644 index 21018c2f60c..00000000000 --- a/javascript/ql/src/semmle/javascript/dataflow/internal/Unit.qll +++ /dev/null @@ -1,9 +0,0 @@ -private newtype TUnit = MkUnit() - -/** - * A class with only one instance. - */ -class Unit extends TUnit { - /** Gets a textual representation of this element. */ - final string toString() { result = "Unit" } -} From 314839fc097f0755ea8d5b1371f8b8fcd89ec65d Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Mon, 25 Jan 2021 13:16:32 +0000 Subject: [PATCH 0134/1429] JS: Add @reduxjs/toolkit to composed functions --- .../ql/src/semmle/javascript/frameworks/ComposedFunctions.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/ql/src/semmle/javascript/frameworks/ComposedFunctions.qll b/javascript/ql/src/semmle/javascript/frameworks/ComposedFunctions.qll index 9e4de6cd4dc..8da2286ad79 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/ComposedFunctions.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/ComposedFunctions.qll @@ -88,7 +88,7 @@ module FunctionCompositionCall { RightToLeft() { this = DataFlow::moduleImport(["compose-function"]).getACall() or - this = DataFlow::moduleMember(["redux", "ramda"], "compose").getACall() + this = DataFlow::moduleMember(["redux", "ramda", "@reduxjs/toolkit"], "compose").getACall() or this = LodashUnderscore::member("flowRight").getACall() } From 8fa3fb05612f6660e1464be31ff9396143917543 Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Mon, 25 Jan 2021 13:00:27 +0000 Subject: [PATCH 0135/1429] JS: Redux model --- javascript/ql/src/javascript.qll | 1 + .../semmle/javascript/frameworks/Redux.qll | 1285 +++++++++++++++++ .../frameworks/Redux/exportedReducer.js | 13 + .../frameworks/Redux/react-redux.jsx | 79 + .../frameworks/Redux/test.expected | 154 ++ .../library-tests/frameworks/Redux/test.ql | 61 + .../library-tests/frameworks/Redux/trivial.js | 137 ++ 7 files changed, 1730 insertions(+) create mode 100644 javascript/ql/src/semmle/javascript/frameworks/Redux.qll create mode 100644 javascript/ql/test/library-tests/frameworks/Redux/exportedReducer.js create mode 100644 javascript/ql/test/library-tests/frameworks/Redux/react-redux.jsx create mode 100644 javascript/ql/test/library-tests/frameworks/Redux/test.expected create mode 100644 javascript/ql/test/library-tests/frameworks/Redux/test.ql create mode 100644 javascript/ql/test/library-tests/frameworks/Redux/trivial.js diff --git a/javascript/ql/src/javascript.qll b/javascript/ql/src/javascript.qll index 66dccd8cddd..b565a7cd21f 100644 --- a/javascript/ql/src/javascript.qll +++ b/javascript/ql/src/javascript.qll @@ -105,6 +105,7 @@ import semmle.javascript.frameworks.PropertyProjection import semmle.javascript.frameworks.Puppeteer import semmle.javascript.frameworks.React import semmle.javascript.frameworks.ReactNative +import semmle.javascript.frameworks.Redux import semmle.javascript.frameworks.Request import semmle.javascript.frameworks.RxJS import semmle.javascript.frameworks.ServerLess diff --git a/javascript/ql/src/semmle/javascript/frameworks/Redux.qll b/javascript/ql/src/semmle/javascript/frameworks/Redux.qll new file mode 100644 index 00000000000..7f22c3638e2 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/frameworks/Redux.qll @@ -0,0 +1,1285 @@ +/** + * Provides classes and predicates for reasoning about data flow through the redux package. + */ + +import javascript +private import semmle.javascript.dataflow.internal.PreCallGraphStep +private import semmle.javascript.Unit + +/** + * Provides classes and predicates for reasoning about data flow through the redux package. + */ +module Redux { + /** + * To avoid mixing up the state between independent Redux apps that live in a monorepo, + * we do a heuristic program slicing based on `package.json` files. For most projects this has no effect. + */ + private module ProgramSlicing { + /** Gets the innermost `package.json` file in a directory containing the given file. */ + private PackageJSON getPackageJson(Container f) { + f = result.getFile().getParentContainer() + or + not exists(f.getFile("package.json")) and + result = getPackageJson(f.getParentContainer()) + } + + private predicate packageDependsOn(PackageJSON importer, PackageJSON dependency) { + importer.getADependenciesObject("").getADependency(dependency.getPackageName(), _) + } + + /** A package that can be considered an entry point for a Redux app. */ + private PackageJSON entryPointPackage() { + result = getPackageJson(any(StoreCreation c).getFile()) + or + // Any package that imports a store-creating package is considered a potential entry point. + packageDependsOn(result, entryPointPackage()) + } + + pragma[nomagic] + private predicate arePackagesInSameReduxApp(PackageJSON a, PackageJSON b) { + exists(PackageJSON entry | + entry = entryPointPackage() and + packageDependsOn*(entry, a) and + packageDependsOn*(entry, b) + ) + } + + /** Holds if the two files are considered to be part of the same Redux app. */ + pragma[inline] + predicate areFilesInSameReduxApp(File a, File b) { + not exists(PackageJSON pkg) + or + arePackagesInSameReduxApp(getPackageJson(a), getPackageJson(b)) + } + } + + /** + * Creation of a redux store, usually via a call to `createStore`. + */ + class StoreCreation extends DataFlow::SourceNode { + StoreCreation::Range range; + + StoreCreation() { this = range } + + /** Gets a reference to the store. */ + DataFlow::SourceNode ref() { + // We happen to know that all store-creation sources have API nodes, so just reuse the API node type tracking + exists(API::Node apiNode | + apiNode.getAnImmediateUse() = this and + result = apiNode.getAUse() + ) + } + + /** Gets the data flow node holding the root reducer for this store. */ + DataFlow::Node getReducerArg() { result = range.getReducerArg() } + + /** Gets a data flow node referring to the root reducer. */ + DataFlow::SourceNode getAReducerSource() { result = getReducerArg().(ReducerArg).getASource() } + } + + /** Companion module to the `StoreCreation` class. */ + module StoreCreation { + /** + * Creation of a redux store. Additional `StoreCreation` instances can be generated by subclassing this class. + */ + abstract class Range extends DataFlow::SourceNode { + /** Gets the data flow node holding the root reducer for this store. */ + abstract DataFlow::Node getReducerArg(); + } + + private class CreateStore extends DataFlow::CallNode, Range { + CreateStore() { + this = API::moduleImport(["redux", "@reduxjs/toolkit"]).getMember("createStore").getACall() + } + + override DataFlow::Node getReducerArg() { result = getArgument(0) } + } + + private class ToolkitStore extends API::CallNode, Range { + ToolkitStore() { + this = API::moduleImport("@reduxjs/toolkit").getMember("configureStore").getACall() + } + + override DataFlow::Node getReducerArg() { + result = getParameter(0).getMember("reducer").getARhs() + } + } + } + + /** An API node that is a source of the Redux root state. */ + abstract private class RootStateSource extends API::Node { } + + /** Gets an API node referring to the Redux root state. */ + private API::Node rootState() { + result instanceof RootStateSource + or + stateStep(rootState().getAUse(), result.getAnImmediateUse()) + } + + /** + * Gets an API node referring to the given (non-empty) access path within the Redux state. + */ + private API::Node rootStateAccessPath(string accessPath) { + result = rootState().getMember(accessPath) + or + exists(string base, string prop | + result = rootStateAccessPath(base).getMember(prop) and + accessPath = joinAccessPaths(base, prop) + ) + or + stateStep(rootStateAccessPath(accessPath).getAUse(), result.getAnImmediateUse()) + } + + /** + * Combines two state access paths, while disallowing unbounded growth of access paths. + */ + bindingset[base, prop] + private string joinAccessPaths(string base, string prop) { + result = base + "." + prop and + // Allow at most two occurrences of a given property name in the path + // (one in the base, plus the one we're appending now). + count(base.indexOf("." + prop + ".")) <= 1 + } + + /** + * Creation of a reducer function that delegates to one or more other reducer functions. + * + * Delegating reducers can delegate specific parts of the state object (`getStateHandlerArg`), + * actions of a specific type (`getActionHandlerArg`), or everything (`getAPlainHandlerArg`). + */ + abstract class DelegatingReducer extends DataFlow::SourceNode { + /** + * Gets a data flow node holding a reducer to which handling of `state.prop` is delegated. + * + * For example, gets the `fn` in `combineReducers({foo: fn})` with `prop` bound to `foo`. + * + * The delegating reducer should behave as a function of this form: + * ```js + * function outer(state, action) { + * return { + * prop: inner(state.prop, action), + * ... + * } + * } + * ``` + */ + DataFlow::Node getStateHandlerArg(string prop) { none() } + + /** + * Gets a data flow node holding a reducer to which actions of the given type are delegated. + * + * For example, gets the `fn` in `handleAction(a, fn)` with `actionType` bound to `a`. + * + * The `actionType` node may refer an action creator or a string value corresponding to `action.type`. + */ + DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) { none() } + + /** + * Gets a data flow node holding a reducer to which every request is forwarded (for the + * purpose of this model). + * + * For example, gets the `fn` in `persistReducer(config, fn)`. + */ + DataFlow::Node getAPlainHandlerArg() { none() } + + /** Gets the use site of this reducer. */ + final ReducerArg getUseSite() { result.getASource() = this } + } + + private module DelegatingReducer { + private API::Node combineReducers() { + result = + API::moduleImport(["redux", "redux-immutable", "@reduxjs/toolkit"]) + .getMember("combineReducers") + } + + /** + * A call to `combineReducers`, which delegates properties of `state` to individual sub-reducers. + */ + private class CombineReducers extends API::CallNode, DelegatingReducer { + CombineReducers() { this = combineReducers().getACall() } + + override DataFlow::Node getStateHandlerArg(string prop) { + result = getParameter(0).getMember(prop).getARhs() + } + } + + /** + * An object literal flowing into a nested property in a `combineReducers` object, such as the `{ bar }` object in: + * ```js + * combineReducers({ foo: { bar } }) + * ``` + * + * Although the object itself is clearly not a function, we use the object to model the corresponding reducer function created by `combineReducers`. + */ + private class NestedCombineReducers extends DelegatingReducer, DataFlow::ObjectLiteralNode { + NestedCombineReducers() { + this = combineReducers().getParameter(0).getAMember+().getAValueReachingRhs() + } + + override DataFlow::Node getStateHandlerArg(string prop) { + result = getAPropertyWrite(prop).getRhs() + } + } + + /** + * A call to `handleActions`, creating a reducer function that dispatched based on the action type: + * + * ```js + * let reducer = handleActions({ + * actionType1: (state, action) => { ... }, + * actionType2: (state, action) => { ... }, + * }) + * ``` + */ + private class HandleActions extends API::CallNode, DelegatingReducer { + HandleActions() { + this = + API::moduleImport(["redux-actions", "redux-ts-utils"]) + .getMember("handleActions") + .getACall() + } + + override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) { + exists(DataFlow::PropWrite write | + result = getParameter(0).getAMember().getARhs() and + write.getRhs() = result and + actionType = write.getPropertyNameExpr().flow() + ) + } + } + + /** + * A call to `handleAction`, creating a reducer function that only handles a given action type: + * + * ```js + * let reducer = handleAction('actionType', (state, action) => { ... }); + * ``` + */ + private class HandleAction extends API::CallNode, DelegatingReducer { + HandleAction() { + this = + API::moduleImport(["redux-actions", "redux-ts-utils"]) + .getMember("handleAction") + .getACall() + } + + override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) { + actionType = getArgument(0) and + result = getArgument(1) + } + } + + /** + * A call to `persistReducer`, which we model as a plain wrapper around another reducer. + */ + private class PersistReducer extends DataFlow::CallNode, DelegatingReducer { + PersistReducer() { + this = API::moduleImport("redux-persist").getMember("persistReducer").getACall() + } + + override DataFlow::Node getAPlainHandlerArg() { result = getArgument(1) } + } + + /** + * A call to `immer` or `immer.produce`, which we model as a plain wrapper around another reducer. + */ + private class ImmerProduce extends DataFlow::CallNode, DelegatingReducer { + ImmerProduce() { + this = API::moduleImport("immer").getACall() + or + this = API::moduleImport("immer").getMember("produce").getACall() + } + + override DataFlow::Node getAPlainHandlerArg() { result = getArgument(0) } + } + + /** + * A call to `reduce-reducers`, modelled as a reducer that dispatches to an arbitrary subreducer. + * + * In reality, this function chains together all of the reducers, but in practice it is only used + * when the reducers handle a disjoint set of action types, which makes it behave as if it + * dispatched to just one of them. + * + * For example: + * ```js + * let reducer = reduceReducers([ + * handleAction('action1', (state, action) => { ... }), + * handleAction('action2', (state, action) => { ... }), + * ]); + * ``` + */ + private class ReduceReducers extends DataFlow::CallNode, DelegatingReducer { + ReduceReducers() { + this = API::moduleImport("reduce-reducers").getACall() or + this = + API::moduleImport(["redux-actions", "redux-ts-utils"]) + .getMember("reduceReducers") + .getACall() + } + + override DataFlow::Node getAPlainHandlerArg() { + result = getAnArgument() + or + result = getArgument(0).getALocalSource().(DataFlow::ArrayCreationNode).getAnElement() + } + } + + /** + * A call to `createReducer`, for example: + * + * ```js + * let reducer = createReducer(initialState, (builder) => { + * builder + * .addCase(actionType1, (state, action) => { ... }) + * .addCase(actionType2, (state, action) => { ... }); + * }); + * ``` + */ + private class CreateReducer extends API::CallNode, DelegatingReducer { + CreateReducer() { + this = API::moduleImport("@reduxjs/toolkit").getMember("createReducer").getACall() + } + + private API::Node getABuilderRef() { + result = getParameter(1).getParameter(0) + or + result = getABuilderRef().getAMember().getReturn() + } + + override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) { + exists(API::CallNode addCase | + addCase = getABuilderRef().getMember("addCase").getACall() and + actionType = addCase.getArgument(0) and + result = addCase.getArgument(1) + ) + } + } + + /** + * A reducer created by a call to `createSlice`. Note that `createSlice` creates both + * reducers and actions; this class models the reducers only. + * + * For example: + * ```js + * let slice = createSlice({ + * name: 'mySlice', + * reducers: { + * actionType1: (state, action) => { ... }, + * actionType2: (state, action) => { ... }, + * }, + * extraReducers: (builder) => { + * builder.addCase('actionType3', (state, action) => { ... }) + * } + * }); + * export default slice.reducer; + * ``` + */ + private class CreateSliceReducer extends DelegatingReducer { + API::CallNode call; + + CreateSliceReducer() { + call = API::moduleImport("@reduxjs/toolkit").getMember("createSlice").getACall() and + this = call.getReturn().getMember("reducer").getAnImmediateUse() + } + + private API::Node getABuilderRef() { + result = call.getParameter(0).getMember("extraReducers").getParameter(0) + or + result = getABuilderRef().getAMember().getReturn() + } + + override DataFlow::Node getActionHandlerArg(DataFlow::Node actionType) { + exists(string name | + result = call.getParameter(0).getMember("reducers").getMember(name).getARhs() and + actionType = call.getReturn().getMember("actions").getMember(name).getAnImmediateUse() + ) + or + // Properties of 'extraReducers': + // { extraReducers: { [action]: reducer }} + exists(DataFlow::PropWrite write | + result = call.getParameter(0).getMember("extraReducers").getAMember().getARhs() and + write.getRhs() = result and + actionType = write.getPropertyNameExpr().flow() + ) + or + // Builder callback to 'extraReducers': + // extraReducers: builder => builder.addCase(action, reducer) + exists(API::CallNode addCase | + addCase = getABuilderRef().getMember("addCase").getACall() and + actionType = addCase.getArgument(0) and + result = addCase.getArgument(1) + ) + } + } + } + + /** + * A function for creating and dispatching action objects of shape `{type, payload}`. + * + * In the simplest case, an action creator is a function, which, for some string `T` behaves as the function `x => {type: T, payload: x}`. + * + * An action creator may have a middleware function `f`, which makes it behave as the function `x => {type: T, payload: f(x)}` (that is, + * the function `f` converts the argument into the actual payload). + * + * Some action creators dispatch the action to a store, while for others, the value is returned and it is simply assumed to be dispatched + * at some point. We model all action creators as if they dispatch the action they create. + */ + class ActionCreator extends DataFlow::SourceNode { + ActionCreator::Range range; + + ActionCreator() { this = range } + + /** Gets the `type` property of actions created by this action creator, if it is known. */ + string getTypeTag() { result = range.getTypeTag() } + + /** + * Gets the middleware function that transforms arguments passed to this function into the + * action payload. + * + * Not every action creator has a middleware function; in such cases the first argument is + * treated as the action payload. + * + * If `async` is true, the middlware function returns a promise whose value eventually becomes + * the action payload. Otherwise, the return value is the payload itself. + */ + DataFlow::FunctionNode getMiddlewareFunction(boolean async) { + result = range.getMiddlewareFunction(async) + } + + /** Gets a data flow node referring to this action creator. */ + private DataFlow::SourceNode ref(DataFlow::TypeTracker t) { + t.start() and + result = this + or + // x -> bindActionCreators({ x, ... }) + exists(BindActionCreatorsCall bind, string prop | + ref(t.continue()).flowsTo(bind.getParameter(0).getMember(prop).getARhs()) and + result = bind.getReturn().getMember(prop).getAnImmediateUse() + ) + or + // x -> combineActions(x, ...) + exists(API::CallNode combiner | + combiner = + API::moduleImport(["redux-actions", "redux-ts-utils"]) + .getMember("combineActions") + .getACall() and + ref(t.continue()).flowsTo(combiner.getAnArgument()) and + result = combiner + ) + or + // x -> x.fulfilled, for async action creators + result = ref(t.continue()).getAPropertyRead("fulfilled") + or + // follow flow through mapDispatchToProps + ReactRedux::dispatchToPropsStep(ref(t.continue()).getALocalUse(), result) + or + exists(DataFlow::TypeTracker t2 | result = ref(t2).track(t2, t)) + } + + /** Gets a data flow node referring to this action creator. */ + DataFlow::SourceNode ref() { result = ref(DataFlow::TypeTracker::end()) } + + /** + * Holds if `successBlock` is executed when a check has determined that `action` originated from this action creator. + */ + private ReachableBasicBlock getASuccessfulTypeCheckBlock(DataFlow::SourceNode action) { + action = getAnUntypedActionInReducer() and + result = getASuccessfulTypeCheckBlock(action, getTypeTag()) + or + // and ProgramSlicing::areFilesInSameReduxApp(result.getFile(), this.getFile()) -- TODO delete? + // some action creators implement a .match method for this purpose + exists(ConditionGuardNode guard, DataFlow::CallNode call | + call = ref().getAMethodCall("match") and + guard.getTest() = call.asExpr() and + action.flowsTo(call.getArgument(0)) and + guard.getOutcome() = true and + result = guard.getBasicBlock() + ) + } + + /** + * Gets a reducer that handles the type of action created by this action creator, for example: + * ```js + * handleAction(TYPE, (state, action) => { ... action.payload ... }) + * ``` + * + * Does not include reducers that perform their own action type checking. + */ + DataFlow::FunctionNode getAReducerFunction() { + exists(ReducerArg reducer | + reducer.isTypeTagHandler(getTypeTag()) + or + reducer.isActionTypeHandler(ref().getALocalUse()) + | + result = reducer.getASource() + ) + } + + /** Gets a data flow node referring a payload of this action (usually in the reducer function). */ + DataFlow::SourceNode getAPayloadReference() { + // `if (action.type === TYPE) { ... action.payload ... }` + exists(DataFlow::SourceNode actionSrc | + actionSrc = getAnUntypedActionInReducer() and + result = actionSrc.getAPropertyRead("payload") and + getASuccessfulTypeCheckBlock(actionSrc).dominates(result.getBasicBlock()) + ) + or + result = getAReducerFunction().getParameter(1).getAPropertyRead("payload") + } + + /** Gets a data flow node referring to the first argument of the action creator invocation. */ + DataFlow::SourceNode getAMetaArgReference() { + exists(ReducerArg reducer | + reducer.isActionTypeHandler(ref().getAPropertyRead(["fulfilled", "rejected", "pending"])) and + result = + reducer + .getASource() + .(DataFlow::FunctionNode) + .getParameter(1) + .getAPropertyRead("meta") + .getAPropertyRead("arg") + ) + } + } + + /** Companion module to the `ActionCreator` class. */ + module ActionCreator { + /** A function for creating and dispatching action objects of shape `{type, payload}`. */ + abstract class Range extends DataFlow::SourceNode { + /** Gets the `type` property of actions created by this action creator */ + abstract string getTypeTag(); + + /** Gets the function transforming arguments into the action payload. */ + DataFlow::FunctionNode getMiddlewareFunction(boolean async) { none() } + } + + /** + * An action creator made using `createAction`: + * ```js + * let action1 = createAction('action1'); + * let action2 = createAction('action2', (x,y) => { x, y }); + * ``` + */ + private class SingleAction extends Range, API::CallNode { + SingleAction() { + this = + API::moduleImport(["@reduxjs/toolkit", "redux-actions", "redux-ts-utils"]) + .getMember("createAction") + .getACall() + } + + override string getTypeTag() { getArgument(0).mayHaveStringValue(result) } + + override DataFlow::FunctionNode getMiddlewareFunction(boolean async) { + result = getCallback(1) and async = false + } + } + + /** + * One of the action creators made by a call to `createActions`: + * ```js + * let { actionOne, actionTwo } = createActions({ + * ACTION_ONE: (x, y) => { x, y }, + * ACTION_TWO: (x, y) => { x, y }, + * }) + * ``` + */ + class MultiAction extends Range { + API::CallNode createActions; + string name; + + MultiAction() { + createActions = API::moduleImport("redux-actions").getMember("createActions").getACall() and + this = createActions.getReturn().getMember(name).getAnImmediateUse() + } + + override DataFlow::FunctionNode getMiddlewareFunction(boolean async) { + result.flowsTo(createActions.getParameter(0).getMember(getTypeTag()).getARhs()) and + async = false + } + + override string getTypeTag() { + result = name.regexpReplaceAll("([a-z])([A-Z])", "$1_$2").toUpperCase() + } + } + + /** + * An action creator made by a call to `createSlice`. Note that `createSlice` creates both + * reducers and actions; this class models the action creators. + * + * ```js + * let slice = createSlice({ + * name: 'mySlice', + * reducers: { + * actionType1: (state, action) => { ... }, + * actionType2: (state, action) => { ... }, + * }, + * }); + * export const { actionType1, actionType2 } = slice.actions; + * ``` + */ + private class CreateSliceAction extends Range { + API::CallNode call; + string actionName; + + CreateSliceAction() { + call = API::moduleImport("@reduxjs/toolkit").getMember("createSlice").getACall() and + this = call.getReturn().getMember("actions").getMember(actionName).getAnImmediateUse() + } + + override string getTypeTag() { + exists(string prefix | + call.getParameter(0).getMember("name").getARhs().mayHaveStringValue(prefix) and + result = prefix + "/" + actionName + ) + } + } + + /** + * An action creator made by a call to `createAsyncThunk`: + * ```js + * const fetchUserId = createAsyncThunk('fetchUserId', async (id) => { + * return (await fetchUserId(id)).data; + * }); + * ``` + */ + private class CreateAsyncThunk extends Range, API::CallNode { + CreateAsyncThunk() { + this = API::moduleImport("@reduxjs/toolkit").getMember("createAsyncThunk").getACall() + } + + override DataFlow::FunctionNode getMiddlewareFunction(boolean async) { + async = true and + result = getParameter(1).getAValueReachingRhs() + } + + override string getTypeTag() { getArgument(0).mayHaveStringValue(result) } + } + } + + /** + * Gets the type tag of an action creator reaching `node`. + */ + private string getAnActionTypeTag(DataFlow::SourceNode node) { + exists(ActionCreator action | + node = action.ref() and + result = action.getTypeTag() + ) + } + + /** Gets the type tag of an action reaching `node`, or the string value of `node`. */ + // Inlined to avoid duplicating `mayHaveStringValue` + pragma[inline] + private string getATypeTagFromNode(DataFlow::Node node) { + node.mayHaveStringValue(result) + or + node.asExpr().(Label).getName() = result + or + result = getAnActionTypeTag(node.getALocalSource()) + } + + /** A data flow node that is used as a reducer. */ + class ReducerArg extends DataFlow::Node { + ReducerArg() { + this = any(StoreCreation c).getReducerArg() + or + this = any(DelegatingReducer r).getStateHandlerArg(_) + or + this = any(DelegatingReducer r).getActionHandlerArg(_) + } + + /** Gets a data flow node that flows to this reducer argument. */ + DataFlow::SourceNode getASource(DataFlow::TypeBackTracker t) { + t.start() and + result = getALocalSource() + or + // Step through forwarding functions + DataFlow::functionForwardingStep(result.getALocalUse(), getASource(t.continue())) + or + // Step through library functions like `redux-persist` + result.getALocalUse() = getASource(t.continue()).(DelegatingReducer).getAPlainHandlerArg() + or + // Step through function composition (usually composed with various state "enhancer" functions) + exists(FunctionCompositionCall compose, DataFlow::CallNode call | + getASource(t.continue()) = call and + call = compose.getACall() and + result.getALocalUse() = [compose.getAnOperandNode(), call.getAnArgument()] + ) + or + exists(DataFlow::TypeBackTracker t2 | result = getASource(t2).backtrack(t2, t)) + } + + /** Gets a data flow node that flows to this reducer argument. */ + DataFlow::SourceNode getASource() { result = getASource(DataFlow::TypeBackTracker::end()) } + + /** + * Holds if the actions dispatched to this reducer have the given type, that is, + * it is created by an action creator that flows to `actionType`, or has `action.type` set to + * the string value of `actionType`. + */ + predicate isActionTypeHandler(DataFlow::Node actionType) { + exists(DelegatingReducer r | + this = r.getActionHandlerArg(actionType) + or + this = r.getStateHandlerArg(_) and + r.getUseSite().isActionTypeHandler(actionType) + ) + } + + /** + * Holds if the actions dispatched to this reducer have the given `action.type` value. + */ + predicate isTypeTagHandler(string actionType) { + exists(DataFlow::Node node | + isActionTypeHandler(node) and + actionType = getATypeTagFromNode(node) + ) + } + + /** + * Holds if this reducer operates on the root state, as opposed to some access path within the state. + */ + predicate isRootStateHandler() { + this = any(StoreCreation c).getReducerArg() + or + exists(DelegatingReducer r | + this = r.getActionHandlerArg(_) and + r.getUseSite().isRootStateHandler() + ) + } + } + + /** + * A source of the `dispatch` function, used as starting point for `getADispatchFunctionReference`. + */ + abstract private class DispatchFunctionSource extends DataFlow::SourceNode { } + + /** + * A value that is dispatched, that is, flows to the first argument of `dispatch` + * (but where the call to `dispatch` is not necessarily explicit in the code). + * + * Used as starting point for `getADispatchedValueSource`. + */ + abstract private class DispatchedValueSink extends DataFlow::Node { } + + private class StoreDispatchSource extends DispatchFunctionSource { + StoreDispatchSource() { this = any(StoreCreation c).ref().getAPropertyRead("dispatch") } + } + + /** Gets a data flow node referring to the `dispatch` function. */ + private DataFlow::SourceNode getADispatchFunctionReference(DataFlow::TypeTracker t) { + t.start() and + result instanceof DispatchFunctionSource + or + // When using the redux-thunk middleware, dispatching a function value results in that + // function being invoked with (dispatch, getState). + // We simply assume redux-thunk middleware is always installed. + t.start() and + result = getADispatchedValueSource().(DataFlow::FunctionNode).getParameter(0) + or + exists(DataFlow::TypeTracker t2 | result = getADispatchFunctionReference(t2).track(t2, t)) + } + + /** Gets a data flow node referring to the `dispatch` function. */ + DataFlow::SourceNode getADispatchFunctionReference() { + result = getADispatchFunctionReference(DataFlow::TypeTracker::end()) + } + + /** Gets a data flow node that is dispatched as an action. */ + private DataFlow::SourceNode getADispatchedValueSource(DataFlow::TypeBackTracker t) { + t.start() and + result = any(DispatchedValueSink d).getALocalSource() + or + t.start() and + result = getADispatchFunctionReference().getACall().getArgument(0).getALocalSource() + or + exists(DataFlow::TypeBackTracker t2 | result = getADispatchedValueSource(t2).backtrack(t2, t)) + } + + /** + * Gets a data flow node that is dispatched as an action, that is, it flows to the first argument of `dispatch`. + */ + DataFlow::SourceNode getADispatchedValueSource() { + result = getADispatchedValueSource(DataFlow::TypeBackTracker::end()) + } + + /** Gets the `action` parameter of a reducer that isn't behind an implied type guard. */ + DataFlow::SourceNode getAnUntypedActionInReducer() { + exists(ReducerArg reducer | + not reducer.isTypeTagHandler(_) and + result = reducer.getASource().(DataFlow::FunctionNode).getParameter(1) + ) + } + + /** A call to `bindActionCreators` */ + private class BindActionCreatorsCall extends API::CallNode { + BindActionCreatorsCall() { + this = + API::moduleImport(["redux", "@reduxjs/toolkit"]).getMember("bindActionCreators").getACall() + } + } + + /** The return value of a function flowing into `bindActionCreators`, seen as a value that is dispatched. */ + private class BindActionDispatchSink extends DispatchedValueSink { + BindActionDispatchSink() { + this = any(BindActionCreatorsCall c).getParameter(0).getAMember().getReturn().getARhs() + } + } + + /** + * Holds if `pred -> succ` is step from an action creation to its use in a reducer function. + */ + predicate actionToReducerStep(DataFlow::Node pred, DataFlow::SourceNode succ) { + // Actions created by an action creator library + exists(ActionCreator action | + exists(DataFlow::CallNode call | call = action.ref().getACall() | + exists(int i | + pred = call.getArgument(i) and + succ = action.getMiddlewareFunction(_).getParameter(i) + ) + or + not exists(action.getMiddlewareFunction(_)) and + pred = call.getArgument(0) and + succ = action.getAPayloadReference() + or + pred = call.getArgument(0) and + succ = action.getAMetaArgReference() + ) + or + pred = action.getMiddlewareFunction(false).getReturnNode() and + succ = action.getAPayloadReference() + ) + or + // Manually created and dispatched actions + exists(string actionType, string prop, DataFlow::SourceNode actionSrc | + actionSrc = getAnUntypedActionInReducer() and + pred = getAManuallyDispatchedValue(actionType).getAPropertyWrite(prop).getRhs() and + succ = actionSrc.getAPropertyRead(prop) + | + getASuccessfulTypeCheckBlock(actionSrc, actionType).dominates(succ.getBasicBlock()) + or + exists(ReducerArg reducer | + reducer.isTypeTagHandler(actionType) and + actionSrc = reducer.getASource().(DataFlow::FunctionNode).getParameter(1) + ) + ) + } + + /** Holds if `pred -> succ` is a step from the promise of an action payload to its use in a reducer function. */ + predicate actionToReducerPromiseStep(DataFlow::Node pred, DataFlow::SourceNode succ) { + exists(ActionCreator action | + pred = action.getMiddlewareFunction(true).getReturnNode() and + succ = action.getAPayloadReference() + ) + } + + private class ActionToReducerStep extends DataFlow::AdditionalFlowStep { + ActionToReducerStep() { + actionToReducerStep(_, this) + or + actionToReducerPromiseStep(_, this) + } + + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + actionToReducerStep(pred, succ) and succ = this + } + + override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) { + actionToReducerPromiseStep(pred, succ) and succ = this and prop = Promises::valueProp() + } + } + + /** Gets the access path which `reducer` operates on. */ + string getAffectedStateAccessPath(ReducerArg reducer) { + exists(DelegatingReducer r | + exists(string prop | reducer = r.getStateHandlerArg(prop) | + result = joinAccessPaths(getAffectedStateAccessPath(r.getUseSite()), prop) + or + r.getUseSite().isRootStateHandler() and + result = prop + ) + or + reducer = r.getActionHandlerArg(_) and + result = getAffectedStateAccessPath(r.getUseSite()) + ) + } + + /** + * Holds if `pred -> succ` should be a step from a reducer to a state access affected by the reducer. + */ + predicate reducerToStateStep(DataFlow::Node pred, DataFlow::Node succ) { + reducerToStateStepAux(pred, succ) and + ProgramSlicing::areFilesInSameReduxApp(pred.getFile(), succ.getFile()) + } + + /** + * Holds if `pred -> succ` should be a step from a reducer to a state access affected by the reducer. + * + * This is a helper predicate for `reducerToStateStep` without the program-slicing check. + */ + pragma[nomagic] + private predicate reducerToStateStepAux(DataFlow::Node pred, DataFlow::SourceNode succ) { + exists(ReducerArg reducer, DataFlow::FunctionNode function, string accessPath | + function = reducer.getASource() and + accessPath = getAffectedStateAccessPath(reducer) + | + pred = function.getReturnNode() and + succ = rootStateAccessPath(accessPath).getAnImmediateUse() + or + exists(string suffix, DataFlow::SourceNode base | + base = [function.getParameter(0), function.getReturnNode().getALocalSource()] and + pred = AccessPath::getAnAssignmentTo(base, suffix) and + succ = rootStateAccessPath(accessPath + "." + suffix).getAnImmediateUse() + ) + ) + or + exists( + ReducerArg reducer, DataFlow::FunctionNode function, string suffix, DataFlow::SourceNode base + | + function = reducer.getASource() and + reducer.isRootStateHandler() and + base = [function.getParameter(0), function.getReturnNode().getALocalSource()] and + pred = AccessPath::getAnAssignmentTo(base, suffix) and + succ = rootStateAccessPath(suffix).getAnImmediateUse() + ) + } + + private class ReducerToStateStep extends DataFlow::AdditionalFlowStep { + ReducerToStateStep() { reducerToStateStep(_, this) } + + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + reducerToStateStep(pred, succ) and succ = this + } + } + + /** + * Gets a dispatched object literal with a property `type: actionType`. + */ + private DataFlow::ObjectLiteralNode getAManuallyDispatchedValue(string actionType) { + result.getAPropertyWrite("type").getRhs().mayHaveStringValue(actionType) and + result = getADispatchedValueSource() + } + + /** + * Gets the block to be executed after a check has determined that `action.type` is `actionType`, + * or the entry block of a closure dominated by such a check. + */ + private ReachableBasicBlock getASuccessfulTypeCheckBlock( + DataFlow::SourceNode action, string actionType + ) { + action = getAnUntypedActionInReducer() and + ( + exists(MembershipCandidate candidate, ConditionGuardNode guard | + action.getAPropertyRead("type").flowsTo(candidate) and + candidate.getAMemberString() = actionType and + guard.getTest() = candidate.getTest().asExpr() and + guard.getOutcome() = candidate.getTestPolarity() and + result = guard.getBasicBlock() + ) + or + exists(SwitchStmt switch, SwitchCase case | + action.getAPropertyRead("type").flowsTo(switch.getExpr().flow()) and + case = switch.getACase() and + case.getExpr().mayHaveStringValue(actionType) and + result = getCaseBlock(case) + ) + ) + or + exists(Function f | + getASuccessfulTypeCheckBlock(action, actionType) + .dominates(f.(ControlFlowNode).getBasicBlock()) and + result = f.getEntryBB() + ) + } + + /** Gets the block to execute when `case` matches sucessfully. */ + private BasicBlock getCaseBlock(SwitchCase case) { + result = case.getBodyStmt(0).getBasicBlock() + or + not exists(case.getABodyStmt()) and + exists(SwitchStmt stmt, int i | + stmt.getCase(i) = case and + result = getCaseBlock(stmt.getCase(i + 1)) + ) + } + + /** + * Defines a flow step to be used for propagating tracking access to `state`. + * + * An `AdditionalFlowStep` is generated for these steps as well. + * It is distinct from `AdditionalFlowStep` to avoid recursion between that and the propagation of `state`. + */ + private class StateStep extends Unit { + abstract predicate step(DataFlow::Node pred, DataFlow::Node succ); + } + + private predicate stateStep(DataFlow::Node pred, DataFlow::Node succ) { + any(StateStep s).step(pred, succ) + } + + private class StateStepAsFlowStep extends DataFlow::AdditionalFlowStep { + StateStepAsFlowStep() { stateStep(_, this) } + + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + stateStep(pred, succ) and succ = this + } + } + + /** + * Model of the `react-redux` package. + */ + private module ReactRedux { + /** Gets an API node referring to the `useSelector` function. */ + API::Node useSelector() { result = API::moduleImport("react-redux").getMember("useSelector") } + + /** + * Step out of a `useSelector` call, such as from `state.x` to the result of `useSelector(state => state.x)`. + */ + class UseSelectorStep extends StateStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + exists(API::CallNode call | + call = useSelector().getACall() and + pred = call.getParameter(0).getReturn().getARhs() and + succ = call + ) + } + } + + /** The argument to a `useSelector` callback, seen as a root state reference. */ + class UseSelectorStateSource extends RootStateSource { + UseSelectorStateSource() { this = useSelector().getParameter(0).getParameter(0) } + } + + /** A call to `useDispatch`, as a source of the `dispatch` function. */ + private class UseDispatchFunctionSource extends DispatchFunctionSource { + UseDispatchFunctionSource() { + this = + API::moduleImport("react-redux").getMember("useDispatch").getReturn().getAnImmediateUse() + } + } + + /** + * A call to `connect()`, typically as part of a code pattern like the following: + * ```js + * let withConnect = connect(mapStateToProps, mapDispatchToProps); + * let MyAwesomeComponent = compose(withConnect, otherStuff)(MyComponent); + * ``` + */ + abstract private class ConnectCall extends API::CallNode { + /** Gets the API node corresponding to the `mapStateToProps` argument. */ + abstract API::Node getMapStateToProps(); + + /** Gets the API node corresponding to the `mapDispatchToProps` argument. */ + abstract API::Node getMapDispatchToProps(); + + /** + * Gets a function whose first argument becomes the React component to connect. + */ + DataFlow::SourceNode getAComponentTransformer() { + result = this + or + exists(FunctionCompositionCall compose | + getAComponentTransformer().flowsTo(compose.getAnOperandNode()) and + result = compose + ) + } + + /** + * Gets a data-flow node that should flow to `props.name` via the `mapDispatchToProps` function. + */ + DataFlow::Node getDispatchPropNode(string name) { + // Implicitly bound by bindActionCreators: + // + // const mapDispatchToProps = { foo } + // + result = getMapDispatchToProps().getMember(name).getARhs() + or + // + // const mapDispatchToProps = dispatch => ( { foo } ) + // + result = getMapDispatchToProps().getReturn().getMember(name).getARhs() + or + // Explicitly bound by bindActionCreators: + // + // const mapDispatchToProps = dispatch => bindActionCreators({ foo }, dispatch); + // + exists(BindActionCreatorsCall bind | + bind.flowsTo(getMapDispatchToProps().getReturn().getARhs()) and + result = bind.getOptionArgument(0, name) + ) + } + + /** + * Gets the React component decorated by this call, if one can be determined. + */ + ReactComponent getReactComponent() { + exists(DataFlow::SourceNode component | component = result.getAComponentCreatorReference() | + component.flowsTo(getAComponentTransformer().getACall().getArgument(0)) + or + component.(DataFlow::ClassNode).getADecorator() = getAComponentTransformer() + ) + } + } + + /** A call to `connect`. */ + private class RealConnectFunction extends ConnectCall { + RealConnectFunction() { + this = API::moduleImport("react-redux").getMember("connect").getACall() + } + + override API::Node getMapStateToProps() { result = getParameter(0) } + + override API::Node getMapDispatchToProps() { result = getParameter(1) } + } + + /** + * An entry point in the API graphs corresponding to functions named `mapDispatchToProps`, + * used to catch cases where the call to `connect` was not found (usually because of it being + * wrapped in another function, which API graphs won't look through). + */ + private class HeuristicConnectEntryPoint extends API::EntryPoint { + HeuristicConnectEntryPoint() { this = "react-redux-connect" } + + override DataFlow::Node getARhs() { none() } + + override DataFlow::SourceNode getAUse() { + exists(DataFlow::CallNode call | + call.getAnArgument().asExpr().(Identifier).getName() = + ["mapStateToProps", "mapDispatchToProps"] and + // exclude genuine calls to avoid duplication + not call = DataFlow::moduleMember("react-redux", "connect").getACall() and + result = call.getCalleeNode().getALocalSource() + ) + } + } + + /** A heuristic call to `connect`, recognized by it taking arguments named `mapStateToProps` and `mapDispatchToProps`. */ + private class HeuristicConnectFunction extends ConnectCall { + HeuristicConnectFunction() { + this = API::root().getASuccessor(any(HeuristicConnectEntryPoint e)).getACall() + } + + override API::Node getMapStateToProps() { + result = getAParameter() and + result.getARhs().asExpr().(Identifier).getName() = "mapStateToProps" + } + + override API::Node getMapDispatchToProps() { + result = getAParameter() and + result.getARhs().asExpr().(Identifier).getName() = "mapDispatchToProps" + } + } + + /** + * A step from the return value of `mapStateToProps` to a `props` access. + */ + private class StateToPropsStep extends StateStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + exists(ConnectCall call | + pred = call.getMapStateToProps().getReturn().getARhs() and + succ = call.getReactComponent().getADirectPropsAccess() + ) + } + } + + /** + * Holds if `pred -> succ` is a step from `mapDispatchToProps` to a `props` property access. + */ + predicate dispatchToPropsStep(DataFlow::Node pred, DataFlow::Node succ) { + exists(ConnectCall call, string member | + pred = call.getDispatchPropNode(member) and + succ = call.getReactComponent().getAPropRead(member) + ) + } + + /** The first argument to `mapDispatchToProps` as a source of the `dispatch` function */ + private class MapDispatchToPropsArg extends DispatchFunctionSource { + MapDispatchToPropsArg() { + this = any(ConnectCall c).getMapDispatchToProps().getParameter(0).getAnImmediateUse() + } + } + + /** If `mapDispatchToProps` is an object, each method's return value is dispatched. */ + private class MapDispatchToPropsMember extends DispatchedValueSink { + MapDispatchToPropsMember() { + this = any(ConnectCall c).getMapDispatchToProps().getAMember().getReturn().getARhs() + } + } + + /** The first argument to `mapStateToProps` as an access to the root state. */ + private class MapStateToPropsStateSource extends RootStateSource { + MapStateToPropsStateSource() { + this = any(ConnectCall c).getMapStateToProps().getParameter(0) + } + } + } + + private module Reselect { + /** + * A call to `createSelector`. + * + * Such calls have two forms. The single-argument version is simply a memoized function wrapper: + * + * ```js + * createSelector(state => state.foo) + * ``` + * + * If multiple arguments are used, each callback independently maps over the state, and last + * callback collects all the intermediate results into the final result: + * + * ```js + * creatorSelector( + * state => state.foo, + * state => state.bar, + * ([foo, bar]) => {...} + * ) + * ``` + * + * Although selectors can work on any data, not just the Redux state, they are in practice only used + * with the state. + */ + class CreateSelectorCall extends API::CallNode { + CreateSelectorCall() { + this = + API::moduleImport(["reselect", "@reduxjs/toolkit"]).getMember("createSelector").getACall() + } + + /** Gets the `i`th selector callback, that is, a callback other than the result function. */ + API::Node getSelectorFunction(int i) { + // When there are multiple callbacks, exclude the last one + result = getParameter(i) and + (i = 0 or i < getNumArgument() - 1) + or + // Selector functions may be given as an array + exists(DataFlow::ArrayCreationNode array | + array.flowsTo(getArgument(0)) and + result.getAUse() = array.getElement(i) + ) + } + } + + /** The state argument to a selector */ + private class SelectorStateArg extends RootStateSource { + SelectorStateArg() { this = any(CreateSelectorCall c).getSelectorFunction(_).getParameter(0) } + } + + /** A flow step between the callbacks of `createSelector` or out of its final selector. */ + private class CreateSelectorStep extends StateStep { + override predicate step(DataFlow::Node pred, DataFlow::Node succ) { + // Return value of `i`th callback flows to the `i`th parameter of the last callback. + exists(CreateSelectorCall call, int index | + call.getNumArgument() > 1 and + pred = call.getSelectorFunction(index).getReturn().getARhs() and + succ = call.getLastParameter().getParameter(index).getAnImmediateUse() + ) + or + // The result of the last callback is the final result + exists(CreateSelectorCall call | + pred = call.getLastParameter().getReturn().getARhs() and + succ = call + ) + } + } + } +} diff --git a/javascript/ql/test/library-tests/frameworks/Redux/exportedReducer.js b/javascript/ql/test/library-tests/frameworks/Redux/exportedReducer.js new file mode 100644 index 00000000000..4331d0f2157 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Redux/exportedReducer.js @@ -0,0 +1,13 @@ +import { combineReducers } from 'redux'; + +export default (state, action) => { + return state; +}; + +export function notAReducer(notState, notAction) { + console.log(notState, notAction); +} + +export const nestedReducer = combineReducers({ + inner: (state, action) => state +}); diff --git a/javascript/ql/test/library-tests/frameworks/Redux/react-redux.jsx b/javascript/ql/test/library-tests/frameworks/Redux/react-redux.jsx new file mode 100644 index 00000000000..abb9729d4bf --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Redux/react-redux.jsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { connect, useDispatch } from 'react-redux'; +import * as rt from '@reduxjs/toolkit'; + +const toolkitAction = rt.createAction('toolkitAction', (x) => { + return { + toolkitValue: x + } +}); +const toolkitReducer = rt.createReducer({}, builder => { + builder + .addCase(toolkitAction, (state, action) => { + return { + value: action.payload.toolkitValue, + ...state + }; + }) + .addCase(asyncAction.fulfilled, (state, action) => { + return { + asyncValue: action.payload.x, + ...state + }; + }); +}); + +function manualAction(x) { + return { + type: 'manualAction', + payload: x + } +} +function manualReducer(state, action) { + switch (action.type) { + case 'manualAction': { + return { ...state, manualValue: action.payload }; + } + } + return state; +} +const asyncAction = rt.createAsyncThunk('asyncAction', (x) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ x }); + }, 10) + }); +}); + +const store = rt.createStore(rt.combineReducers({ + toolkit: toolkitReducer, + manual: manualReducer, +})); + +function MyComponent(props) { + let dispatch = useDispatch(); + const clickHandler = React.useCallback(() => { + props.toolkitAction(source()); + props.manualAction(source()); // not currently propagated as functions are not type-tracked + dispatch(manualAction(source())); + dispatch(asyncAction(source())); + }); + + sink(props.propFromToolkitAction); // NOT OK + sink(props.propFromManualAction); // NOT OK + sink(props.propFromAsync); // NOT OK + + return