From dc58f6fa8748fe947f6a039d5dc332e4cc48cadf Mon Sep 17 00:00:00 2001 From: dilanbhalla Date: Thu, 25 Jun 2020 11:39:09 -0700 Subject: [PATCH 001/336] 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 002/336] 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 003/336] 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 004/336] *)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 005/336] *)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 006/336] *)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 007/336] 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 008/336] 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 009/336] 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 010/336] 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 011/336] 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 c5577cb09a073c2fb4f6feeca887c437d5fbe2fb Mon Sep 17 00:00:00 2001 From: haby0 Date: Thu, 4 Mar 2021 19:54:49 +0800 Subject: [PATCH 012/336] 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 013/336] 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 014/336] 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 015/336] 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 016/336] 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 31eaa80f5b881531bc66551e568fed1ffc380325 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Sat, 6 Mar 2021 00:56:15 +0000 Subject: [PATCH 017/336] 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 018/336] 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 019/336] 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 020/336] 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 021/336] 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 022/336] 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 023/336] 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 024/336] 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 025/336] 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 98204a15a6ad8c8dd5dfb42269bfdd153cfc1827 Mon Sep 17 00:00:00 2001 From: haby0 Date: Wed, 17 Mar 2021 15:28:04 +0800 Subject: [PATCH 026/336] 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 027/336] 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 028/336] 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 1385b2264234bc6fa3deaaeb7537ee47d89c47fe Mon Sep 17 00:00:00 2001 From: Dilan Date: Fri, 19 Mar 2021 16:43:29 -0700 Subject: [PATCH 029/336] 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 030/336] 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 031/336] 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 032/336] 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 033/336] 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 034/336] 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 035/336] 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 036/336] 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 037/336] 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 038/336] 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 039/336] 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 08c3bf26d558d1bfaf0504fa1a0ba6837910829a Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Tue, 16 Mar 2021 03:18:26 +0000 Subject: [PATCH 040/336] 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 041/336] 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 042/336] 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 2ca95166d916cc89bf2d7086aa664ef3757c95e4 Mon Sep 17 00:00:00 2001 From: Porcuiney Hairs Date: Wed, 13 Jan 2021 01:32:54 +0530 Subject: [PATCH 043/336] 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 d33b04cd965de99180434c998f96a15f98c1f5fa Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 26 Mar 2021 02:33:40 +0000 Subject: [PATCH 044/336] 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 a72b1340eb6bb3bdbea9cabd6bdd942a9365b848 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Fri, 26 Mar 2021 16:51:43 +0000 Subject: [PATCH 045/336] 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 a53cbc1631c2cfc994f74d4c5ce9e39f6c38abbc Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Sat, 27 Mar 2021 00:11:01 +0000 Subject: [PATCH 046/336] 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 5ce3f9d6ff6eb740d8c6add31fd7c95fa79f6453 Mon Sep 17 00:00:00 2001 From: luchua-bc Date: Sun, 28 Mar 2021 03:15:06 +0000 Subject: [PATCH 047/336] 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 093c63ea3b15cd33b08b8bd1d1058ffc3c6dd075 Mon Sep 17 00:00:00 2001 From: ihsinme Date: Sun, 28 Mar 2021 23:42:36 +0300 Subject: [PATCH 048/336] 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 049/336] 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 050/336] 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 937a620f4d41db65c7203201435aaf3dab6380de Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Tue, 30 Mar 2021 11:33:42 +0100 Subject: [PATCH 051/336] 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 052/336] 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 053/336] 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 054/336] 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 055/336] 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 056/336] 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 057/336] 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 058/336] 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 57784dc7461f3f4624966b9c9e0356fe0018ee5b Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Wed, 31 Mar 2021 09:23:47 +0100 Subject: [PATCH 059/336] 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 060/336] 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 061/336] 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 ecbce88ec78710f67c0160f7ae58e58c027fb73c Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Wed, 31 Mar 2021 22:23:50 +0200 Subject: [PATCH 062/336] 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 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 063/336] 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 064/336] 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 065/336] 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 066/336] 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 067/336] 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 068/336] 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