|
| 1 | +import python |
| 2 | +import experimental.semmle.python.Concepts |
| 3 | +import semmle.python.dataflow.new.DataFlow |
| 4 | +import semmle.python.ApiGraphs |
| 5 | +import semmle.python.dataflow.new.TaintTracking |
| 6 | +import WebAppConstantSecretKeySource |
| 7 | + |
| 8 | +/** |
| 9 | + * with using flask-session package, there is no jwt exists in cookies in user side |
| 10 | + * ```python |
| 11 | + *import os |
| 12 | + *from flask import Flask, session |
| 13 | + *app = Flask(__name__) |
| 14 | + * ``` |
| 15 | + */ |
| 16 | +module FlaskConstantSecretKeyConfig { |
| 17 | + /** |
| 18 | + * `flask.Flask()` |
| 19 | + */ |
| 20 | + API::Node flaskInstance() { |
| 21 | + result = API::moduleImport("flask").getMember("Flask").getASubclass*() |
| 22 | + } |
| 23 | + |
| 24 | + /** |
| 25 | + * Sources are Constants that without any Tainting reach the Sinks. |
| 26 | + * Also Sources can be the default value of getenv or similar methods |
| 27 | + * in a case that no value is assigned to Desired SECRET_KEY environment variable |
| 28 | + */ |
| 29 | + predicate isSource(DataFlow::Node source) { source instanceof WebAppConstantSecretKeySource } |
| 30 | + |
| 31 | + /** |
| 32 | + * Sinks are one of the following kinds, some of them are directly connected to a flask Instance like |
| 33 | + * ```python |
| 34 | + * app.config['SECRET_KEY'] = 'CHANGEME1' |
| 35 | + * app.secret_key = 'CHANGEME2' |
| 36 | + * app.config.update(SECRET_KEY="CHANGEME3") |
| 37 | + * app.config.from_mapping(SECRET_KEY="CHANGEME4") |
| 38 | + * ``` |
| 39 | + * other Sinks are SECRET_KEY Constants Variables that are defined in separate files or a class in those files like: |
| 40 | + * ```python |
| 41 | + * app.config.from_pyfile("config.py") |
| 42 | + * app.config.from_object('config.Config') |
| 43 | + *``` |
| 44 | + * we find these files with `FromObjectFileName` DataFlow Configuration |
| 45 | + * note that "JWT_SECRET_KEY" is same as "SECRET_KEY" but it is belong to popular flask-jwt-extended library |
| 46 | + */ |
| 47 | + predicate isSink(DataFlow::Node sink) { |
| 48 | + ( |
| 49 | + exists(API::Node n | n = flaskInstance().getReturn() | |
| 50 | + sink = |
| 51 | + [ |
| 52 | + n.getMember("config").getSubscript(["SECRET_KEY", "JWT_SECRET_KEY"]).asSink(), |
| 53 | + n.getMember("config") |
| 54 | + .getMember(["update", "from_mapping"]) |
| 55 | + .getACall() |
| 56 | + .getArgByName(["SECRET_KEY", "JWT_SECRET_KEY"]) |
| 57 | + ] |
| 58 | + ) |
| 59 | + or |
| 60 | + exists(DataFlow::AttrWrite attr | |
| 61 | + attr.getObject().getALocalSource() = flaskInstance().getACall() and |
| 62 | + attr.getAttributeName() = ["secret_key", "jwt_secret_key"] and |
| 63 | + sink = attr.getValue() |
| 64 | + ) |
| 65 | + or |
| 66 | + exists(SecretKeyAssignStmt e | sink.asExpr() = e.getValue()) |
| 67 | + ) and |
| 68 | + exists(sink.getScope().getLocation().getFile().getRelativePath()) and |
| 69 | + not sink.getScope().getLocation().getFile().inStdlib() |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * An Assignments like `SECRET_KEY = ConstantValue` |
| 74 | + * and `SECRET_KEY` file must be the Location that is specified in argument of `from_object` or `from_pyfile` methods |
| 75 | + */ |
| 76 | + class SecretKeyAssignStmt extends AssignStmt { |
| 77 | + SecretKeyAssignStmt() { |
| 78 | + exists(string configFileName, string fileNamehelper, DataFlow::Node n1, File file | |
| 79 | + fileNamehelper = [flaskConfiFileName(n1), flaskConfiFileName2(n1)] and |
| 80 | + // because of `from_object` we want first part of `Config.AClassName` which `Config` is a python file name |
| 81 | + configFileName = fileNamehelper.splitAt(".") and |
| 82 | + file = this.getLocation().getFile() |
| 83 | + | |
| 84 | + ( |
| 85 | + if fileNamehelper = "__name__" |
| 86 | + then |
| 87 | + file.getShortName() = flaskInstance().asSource().getLocation().getFile().getShortName() |
| 88 | + else ( |
| 89 | + fileNamehelper.matches("%.py") and |
| 90 | + file.getShortName().matches("%" + configFileName + "%") and |
| 91 | + // after spliting, don't look at %py% pattern |
| 92 | + configFileName != ".py" |
| 93 | + or |
| 94 | + // in case of referencing to a directory which then we must look for __init__.py file |
| 95 | + not fileNamehelper.matches("%.py") and |
| 96 | + file.getRelativePath() |
| 97 | + .matches("%" + fileNamehelper.replaceAll(".", "/") + "/__init__.py") |
| 98 | + ) |
| 99 | + ) and |
| 100 | + this.getTarget(0).(Name).getId() = ["SECRET_KEY", "JWT_SECRET_KEY"] |
| 101 | + ) and |
| 102 | + exists(this.getScope().getLocation().getFile().getRelativePath()) and |
| 103 | + not this.getScope().getLocation().getFile().inStdlib() |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + /** |
| 108 | + * Holds if there is a helper predicate that specify where the Flask `SECRET_KEY` variable location is defined. |
| 109 | + * In Flask we have config files that specify the location of `SECRET_KEY` variable initialization |
| 110 | + * and the name of these files are determined by |
| 111 | + * `app.config.from_pyfile("configFileName.py")` |
| 112 | + * or |
| 113 | + * `app.config.from_object("configFileName.ClassName")` |
| 114 | + */ |
| 115 | + string flaskConfiFileName(API::CallNode cn) { |
| 116 | + cn = |
| 117 | + flaskInstance() |
| 118 | + .getReturn() |
| 119 | + .getMember("config") |
| 120 | + .getMember(["from_object", "from_pyfile"]) |
| 121 | + .getACall() and |
| 122 | + result = |
| 123 | + [ |
| 124 | + cn.getParameter(0).getAValueReachingSink().asExpr().(StrConst).getText(), |
| 125 | + cn.getParameter(0).asSink().asExpr().(Name).getId() |
| 126 | + ] |
| 127 | + } |
| 128 | + |
| 129 | + string flaskConfiFileName2(API::CallNode cn) { |
| 130 | + cn = |
| 131 | + API::moduleImport("flask") |
| 132 | + .getMember("Flask") |
| 133 | + .getASubclass*() |
| 134 | + .getASuccessor*() |
| 135 | + .getMember("from_object") |
| 136 | + .getACall() and |
| 137 | + result = cn.getParameter(0).asSink().asExpr().(StrConst).getText() |
| 138 | + } |
| 139 | +} |
0 commit comments