Merge pull request #5588 from jorgectf/jorgectf/python/jwt-queries

Python: Add JWT security-related queries
This commit is contained in:
Taus
2021-11-16 15:40:45 +01:00
committed by GitHub
19 changed files with 659 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import jwt
# algorithm set to None
jwt.encode(payload, "somekey", None)
# empty key
jwt.encode(payload, key="", algorithm="HS256")

View File

@@ -0,0 +1,30 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Applications encoding a JSON Web Token (JWT) may be vulnerable when the applied key or algorithm
is empty or <code>None</code>.</p>
</overview>
<recommendation>
<p>Use non-empty nor <code>None</code> values while encoding JWT payloads.</p>
</recommendation>
<example>
<p>This example shows two PyJWT encoding calls.
In the first place, the encoding process use a None algorithm whereas the second example uses an
empty key. Both examples leave the payload insecurely encoded.
</p>
<sample src="JWTEmptyKeyOrAlgorithm.py" />
</example>
<references>
<li>PyJWT: <a href="https://pyjwt.readthedocs.io/en/stable/">Documentation</a>.</li>
<li>Authlib JWT: <a href="https://docs.authlib.org/en/latest/specs/rfc7519.html">Documentation</a>.</li>
<li>Python-Jose: <a href="https://github.com/mpdavis/python-jose">Documentation</a>.</li>
<li>Auth0 Blog: <a href="https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/#Meet-the--None--Algorithm">Meet the "None" Algorithm</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,22 @@
/**
* @name JWT encoding using empty key or algorithm
* @description The application uses an empty secret or algorithm while encoding a JWT Token.
* @kind problem
* @problem.severity warning
* @id py/jwt-empty-secret-or-algorithm
* @tags security
*/
// determine precision above
import python
import experimental.semmle.python.Concepts
import experimental.semmle.python.frameworks.JWT
from JWTEncoding jwtEncoding, string affectedComponent
where
affectedComponent = "algorithm" and
isEmptyOrNone(jwtEncoding.getAlgorithm())
or
affectedComponent = "key" and
isEmptyOrNone(jwtEncoding.getKey())
select jwtEncoding, "This JWT encoding has an empty " + affectedComponent + "."

View File

@@ -0,0 +1,4 @@
import jwt
# unverified decoding
jwt.decode(payload, key="somekey", verify=False)

View File

@@ -0,0 +1,30 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Applications decoding a JSON Web Token (JWT) may be vulnerable when the
key isn't verified in the process.
</p>
</overview>
<recommendation>
<p>Set the <code>verify</code> argument to <code>True</code> or use
a framework that does it by default.
</p>
</recommendation>
<example>
<p>This example shows a PyJWT encoding call with the <code>verify</code>
argument set to <code>False</code>.
</p>
<sample src="JWTMissingSecretOrPublicKeyVerification.py" />
</example>
<references>
<li>PyJWT: <a href="https://pyjwt.readthedocs.io/en/stable/">Documentation</a>.</li>
<li>Authlib JWT: <a href="https://docs.authlib.org/en/latest/specs/rfc7519.html">Documentation</a>.</li>
<li>Python-Jose: <a href="https://github.com/mpdavis/python-jose">Documentation</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,17 @@
/**
* @name JWT missing secret or public key verification
* @description The application does not verify the JWT payload with a cryptographic secret or public key.
* @kind problem
* @problem.severity warning
* @id py/jwt-missing-verification
* @tags security
* external/cwe/cwe-347
*/
// determine precision above
import python
import experimental.semmle.python.Concepts
from JWTDecoding jwtDecoding
where not jwtDecoding.verifiesSignature()
select jwtDecoding.getPayload(), "is not verified with a cryptographic secret or public key."

View File

@@ -296,3 +296,141 @@ class HeaderDeclaration extends DataFlow::Node {
*/
DataFlow::Node getValueArg() { result = range.getValueArg() }
}
/** Provides classes for modeling JWT encoding-related APIs. */
module JWTEncoding {
/**
* A data-flow node that collects methods encoding a JWT token.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `JWTEncoding` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the argument containing the encoding payload.
*/
abstract DataFlow::Node getPayload();
/**
* Gets the argument containing the encoding key.
*/
abstract DataFlow::Node getKey();
/**
* Gets the argument for the algorithm used in the encoding.
*/
abstract DataFlow::Node getAlgorithm();
/**
* Gets a string representation of the algorithm used in the encoding.
*/
abstract string getAlgorithmString();
}
}
/**
* A data-flow node that collects methods encoding a JWT token.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `JWTEncoding::Range` instead.
*/
class JWTEncoding extends DataFlow::Node instanceof JWTEncoding::Range {
/**
* Gets the argument containing the payload.
*/
DataFlow::Node getPayload() { result = super.getPayload() }
/**
* Gets the argument containing the encoding key.
*/
DataFlow::Node getKey() { result = super.getKey() }
/**
* Gets the argument for the algorithm used in the encoding.
*/
DataFlow::Node getAlgorithm() { result = super.getAlgorithm() }
/**
* Gets a string representation of the algorithm used in the encoding.
*/
string getAlgorithmString() { result = super.getAlgorithmString() }
}
/** Provides classes for modeling JWT decoding-related APIs. */
module JWTDecoding {
/**
* A data-flow node that collects methods decoding a JWT token.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `JWTDecoding` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the argument containing the encoding payload.
*/
abstract DataFlow::Node getPayload();
/**
* Gets the argument containing the encoding key.
*/
abstract DataFlow::Node getKey();
/**
* Gets the argument for the algorithm used in the encoding.
*/
abstract DataFlow::Node getAlgorithm();
/**
* Gets a string representation of the algorithm used in the encoding.
*/
abstract string getAlgorithmString();
/**
* Gets the options Node used in the encoding.
*/
abstract DataFlow::Node getOptions();
/**
* Checks if the signature gets verified while decoding.
*/
abstract predicate verifiesSignature();
}
}
/**
* A data-flow node that collects methods encoding a JWT token.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `JWTDecoding::Range` instead.
*/
class JWTDecoding extends DataFlow::Node instanceof JWTDecoding::Range {
/**
* Gets the argument containing the payload.
*/
DataFlow::Node getPayload() { result = super.getPayload() }
/**
* Gets the argument containing the encoding key.
*/
DataFlow::Node getKey() { result = super.getKey() }
/**
* Gets the argument for the algorithm used in the encoding.
*/
DataFlow::Node getAlgorithm() { result = super.getAlgorithm() }
/**
* Gets a string representation of the algorithm used in the encoding.
*/
string getAlgorithmString() { result = super.getAlgorithmString() }
/**
* Gets the options Node used in the encoding.
*/
DataFlow::Node getOptions() { result = super.getOptions() }
/**
* Checks if the signature gets verified while decoding.
*/
predicate verifiesSignature() { super.verifiesSignature() }
}

View File

@@ -9,3 +9,7 @@ private import experimental.semmle.python.frameworks.Werkzeug
private import experimental.semmle.python.frameworks.LDAP
private import experimental.semmle.python.frameworks.NoSQL
private import experimental.semmle.python.frameworks.Log
private import experimental.semmle.python.frameworks.JWT
private import experimental.semmle.python.libraries.PyJWT
private import experimental.semmle.python.libraries.Authlib
private import experimental.semmle.python.libraries.PythonJose

View File

@@ -0,0 +1,23 @@
private import python
private import semmle.python.ApiGraphs
/** Checks if the argument is empty or none. */
predicate isEmptyOrNone(DataFlow::Node arg) { isEmpty(arg) or isNone(arg) }
/** Checks if an empty string `""` flows to `arg` */
predicate isEmpty(DataFlow::Node arg) {
exists(StrConst emptyString |
emptyString.getText() = "" and
DataFlow::exprNode(emptyString).(DataFlow::LocalSourceNode).flowsTo(arg)
)
}
/** Checks if `None` flows to `arg` */
predicate isNone(DataFlow::Node arg) {
DataFlow::exprNode(any(None no)).(DataFlow::LocalSourceNode).flowsTo(arg)
}
/** Checks if `False` flows to `arg` */
predicate isFalse(DataFlow::Node arg) {
DataFlow::exprNode(any(False falseExpr)).(DataFlow::LocalSourceNode).flowsTo(arg)
}

View File

@@ -0,0 +1,87 @@
private import python
private import experimental.semmle.python.Concepts
private import semmle.python.ApiGraphs
private import experimental.semmle.python.frameworks.JWT
private module Authlib {
/** Gets a reference to `authlib.jose.(jwt|JsonWebToken)` */
private API::Node authlibJWT() {
result in [
API::moduleImport("authlib").getMember("jose").getMember("jwt"),
API::moduleImport("authlib").getMember("jose").getMember("JsonWebToken").getReturn()
]
}
/** Gets a reference to `jwt.encode` */
private API::Node authlibJWTEncode() { result = authlibJWT().getMember("encode") }
/** Gets a reference to `jwt.decode` */
private API::Node authlibJWTDecode() { result = authlibJWT().getMember("decode") }
/**
* Gets a call to `authlib.jose.(jwt|JsonWebToken).encode`.
*
* Given the following example:
*
* ```py
* jwt.encode({"alg": "HS256"}, token, "key")
* ```
*
* * `this` would be `jwt.encode({"alg": "HS256"}, token, "key")`.
* * `getPayload()`'s result would be `token`.
* * `getKey()`'s result would be `"key"`.
* * `getAlgorithm()`'s result would be `"HS256"`.
* * `getAlgorithmstring()`'s result would be `HS256`.
*/
private class AuthlibJWTEncodeCall extends DataFlow::CallCfgNode, JWTEncoding::Range {
AuthlibJWTEncodeCall() { this = authlibJWTEncode().getACall() }
override DataFlow::Node getPayload() { result = this.getArg(1) }
override DataFlow::Node getKey() { result = this.getArg(2) }
override DataFlow::Node getAlgorithm() {
exists(KeyValuePair headerDict |
headerDict = this.getArg(0).asExpr().(Dict).getItem(_) and
headerDict.getKey().(Str_).getS().matches("alg") and
result.asExpr() = headerDict.getValue()
)
}
override string getAlgorithmString() {
exists(StrConst str |
DataFlow::exprNode(str).(DataFlow::LocalSourceNode).flowsTo(getAlgorithm()) and
result = str.getText()
)
}
}
/**
* Gets a call to `authlib.jose.(jwt|JsonWebToken).decode`
*
* Given the following example:
*
* ```py
* jwt.decode(token, key)
* ```
*
* * `this` would be `jwt.decode(token, key)`.
* * `getPayload()`'s result would be `token`.
* * `getKey()`'s result would be `key`.
*/
private class AuthlibJWTDecodeCall extends DataFlow::CallCfgNode, JWTDecoding::Range {
AuthlibJWTDecodeCall() { this = authlibJWTDecode().getACall() }
override DataFlow::Node getPayload() { result = this.getArg(0) }
override DataFlow::Node getKey() { result = this.getArg(1) }
override DataFlow::Node getAlgorithm() { none() }
override string getAlgorithmString() { none() }
override DataFlow::Node getOptions() { none() }
override predicate verifiesSignature() { any() }
}
}

View File

@@ -0,0 +1,108 @@
private import python
private import experimental.semmle.python.Concepts
private import semmle.python.ApiGraphs
private import experimental.semmle.python.frameworks.JWT
private module PyJWT {
/** Gets a reference to `jwt.encode` */
private API::Node pyjwtEncode() { result = API::moduleImport("jwt").getMember("encode") }
/** Gets a reference to `jwt.decode` */
private API::Node pyjwtDecode() { result = API::moduleImport("jwt").getMember("decode") }
/**
* Gets a call to `jwt.encode`.
*
* Given the following example:
*
* ```py
* jwt.encode(token, "key", "HS256")
* ```
*
* * `this` would be `jwt.encode(token, "key", "HS256")`.
* * `getPayload()`'s result would be `token`.
* * `getKey()`'s result would be `"key"`.
* * `getAlgorithm()`'s result would be `"HS256"`.
* * `getAlgorithmstring()`'s result would be `HS256`.
*/
private class PyJWTEncodeCall extends DataFlow::CallCfgNode, JWTEncoding::Range {
PyJWTEncodeCall() { this = pyjwtEncode().getACall() }
override DataFlow::Node getPayload() {
result in [this.getArg(0), this.getArgByName("payload")]
}
override DataFlow::Node getKey() { result in [this.getArg(1), this.getArgByName("key")] }
override DataFlow::Node getAlgorithm() {
result in [this.getArg(2), this.getArgByName("algorithm")]
}
override string getAlgorithmString() {
exists(StrConst str |
DataFlow::exprNode(str).(DataFlow::LocalSourceNode).flowsTo(getAlgorithm()) and
result = str.getText()
)
}
}
/**
* Gets a call to `jwt.decode`.
*
* Given the following example:
*
* ```py
* jwt.decode(token, key, "HS256", options={"verify_signature": True})
* ```
*
* * `this` would be `jwt.decode(token, key, options={"verify_signature": True})`.
* * `getPayload()`'s result would be `token`.
* * `getKey()`'s result would be `key`.
* * `getAlgorithm()`'s result would be `"HS256"`.
* * `getAlgorithmstring()`'s result would be `HS256`.
* * `getOptions()`'s result would be `{"verify_signature": True}`.
* * `verifiesSignature()` predicate would succeed.
*/
private class PyJWTDecodeCall extends DataFlow::CallCfgNode, JWTDecoding::Range {
PyJWTDecodeCall() { this = pyjwtDecode().getACall() }
override DataFlow::Node getPayload() { result in [this.getArg(0), this.getArgByName("jwt")] }
override DataFlow::Node getKey() { result in [this.getArg(1), this.getArgByName("key")] }
override DataFlow::Node getAlgorithm() {
result in [this.getArg(2), this.getArgByName("algorithms")]
}
override string getAlgorithmString() {
exists(StrConst str |
DataFlow::exprNode(str).(DataFlow::LocalSourceNode).flowsTo(getAlgorithm()) and
result = str.getText()
)
}
override DataFlow::Node getOptions() {
result in [this.getArg(3), this.getArgByName("options")]
}
override predicate verifiesSignature() {
not this.hasVerifySetToFalse() and
not this.hasVerifySignatureSetToFalse()
}
predicate hasNoVerifyArgumentOrOptions() {
not exists(this.getArgByName("verify")) and not exists(this.getOptions())
}
predicate hasVerifySetToFalse() { isFalse(this.getArgByName("verify")) }
predicate hasVerifySignatureSetToFalse() {
exists(KeyValuePair optionsDict, NameConstant falseName |
falseName.getId() = "False" and
optionsDict = this.getOptions().asExpr().(Dict).getItem(_) and
optionsDict.getKey().(Str_).getS().matches("%verify%") and
falseName = optionsDict.getValue()
)
}
}
}

View File

@@ -0,0 +1,105 @@
private import python
private import experimental.semmle.python.Concepts
private import semmle.python.ApiGraphs
private import experimental.semmle.python.frameworks.JWT
private module PythonJose {
/** Gets a reference to `jwt` */
private API::Node joseJWT() { result = API::moduleImport("jose").getMember("jwt") }
/** Gets a reference to `jwt.encode` */
private API::Node joseJWTEncode() { result = joseJWT().getMember("encode") }
/** Gets a reference to `jwt.decode` */
private API::Node joseJWTDecode() { result = joseJWT().getMember("decode") }
/**
* Gets a call to `jwt.encode`.
*
* Given the following example:
*
* ```py
* jwt.encode(token, key="key", algorithm="HS256")
* ```
*
* * `this` would be `jwt.encode(token, key="key", algorithm="HS256")`.
* * `getPayload()`'s result would be `token`.
* * `getKey()`'s result would be `"key"`.
* * `getAlgorithm()`'s result would be `"HS256"`.
* * `getAlgorithmstring()`'s result would be `HS256`.
*/
private class JoseJWTEncodeCall extends DataFlow::CallCfgNode, JWTEncoding::Range {
JoseJWTEncodeCall() { this = joseJWTEncode().getACall() }
override DataFlow::Node getPayload() { result = this.getArg(0) }
override DataFlow::Node getKey() { result in [this.getArg(1), this.getArgByName("key")] }
override DataFlow::Node getAlgorithm() {
result in [this.getArg(2), this.getArgByName("algorithm")]
}
override string getAlgorithmString() {
exists(StrConst str |
DataFlow::exprNode(str).(DataFlow::LocalSourceNode).flowsTo(getAlgorithm()) and
result = str.getText()
)
}
}
/**
* Gets a call to `jwt.decode`.
*
* Given the following example:
*
* ```py
* jwt.decode(token, "key", "HS256")
* ```
*
* * `this` would be `jwt.decode(token, "key", "HS256")`.
* * `getPayload()`'s result would be `token`.
* * `getKey()`'s result would be `"key"`.
* * `getAlgorithm()`'s result would be `"HS256"`.
* * `getAlgorithmstring()`'s result would be `HS256`.
* * `getOptions()`'s result would be none.
* * `verifiesSignature()` predicate would succeed.
*/
private class JoseJWTDecodeCall extends DataFlow::CallCfgNode, JWTDecoding::Range {
JoseJWTDecodeCall() { this = joseJWTDecode().getACall() }
override DataFlow::Node getPayload() { result = this.getArg(0) }
override DataFlow::Node getKey() { result in [this.getArg(1), this.getArgByName("key")] }
override DataFlow::Node getAlgorithm() {
result in [this.getArg(2), this.getArgByName("algorithms")]
}
override string getAlgorithmString() {
exists(StrConst str |
DataFlow::exprNode(str).(DataFlow::LocalSourceNode).flowsTo(getAlgorithm()) and
result = str.getText()
)
}
override DataFlow::Node getOptions() {
result in [this.getArg(3), this.getArgByName("options")]
}
override predicate verifiesSignature() {
// jwt.decode(token, key, options={"verify_signature": False})
not this.hasVerifySignatureSetToFalse()
}
predicate hasNoOptions() { not exists(this.getOptions()) }
predicate hasVerifySignatureSetToFalse() {
exists(KeyValuePair optionsDict, NameConstant falseName |
falseName.getId() = "False" and
optionsDict = this.getOptions().asExpr().(Dict).getItem(_) and
optionsDict.getKey().(Str_).getS().matches("%verify%") and
falseName = optionsDict.getValue()
)
}
}
}

View File

@@ -0,0 +1,8 @@
| authlib.py:11:1:11:39 | ControlFlowNode for Attribute() | This JWT encoding has an empty key. |
| authlib.py:12:1:12:50 | ControlFlowNode for Attribute() | This JWT encoding has an empty key. |
| pyjwt.py:10:1:10:29 | ControlFlowNode for Attribute() | This JWT encoding has an empty algorithm. |
| pyjwt.py:10:1:10:29 | ControlFlowNode for Attribute() | This JWT encoding has an empty key. |
| pyjwt.py:13:1:13:40 | ControlFlowNode for Attribute() | This JWT encoding has an empty key. |
| pyjwt.py:14:1:14:44 | ControlFlowNode for Attribute() | This JWT encoding has an empty key. |
| python_jose.py:10:1:10:40 | ControlFlowNode for Attribute() | This JWT encoding has an empty key. |
| python_jose.py:11:1:11:44 | ControlFlowNode for Attribute() | This JWT encoding has an empty key. |

View File

@@ -0,0 +1 @@
experimental/Security/CWE-347/JWTEmptyKeyOrAlgorithm.ql

View File

@@ -0,0 +1,3 @@
| pyjwt.py:22:12:22:16 | ControlFlowNode for token | is not verified with a cryptographic secret or public key. |
| pyjwt.py:23:12:23:16 | ControlFlowNode for token | is not verified with a cryptographic secret or public key. |
| python_jose.py:19:12:19:16 | ControlFlowNode for token | is not verified with a cryptographic secret or public key. |

View File

@@ -0,0 +1 @@
experimental/Security/CWE-347/JWTMissingSecretOrPublicKeyVerification.ql

View File

@@ -0,0 +1,18 @@
from authlib.jose import jwt # It is already a JsonWebToken object
from authlib.jose import JsonWebToken
# Encoding
# good - key and algorithm supplied
jwt.encode({"alg": "HS256"}, token, "key")
JsonWebToken().encode({"alg": "HS256"}, token, "key")
# bad - empty key
jwt.encode({"alg": "HS256"}, token, "")
JsonWebToken().encode({"alg": "HS256"}, token, "")
# Decoding
# good - "it will raise BadSignatureError when signature doesnt match"
jwt.decode(token, key)
JsonWebToken().decode(token, key)

View File

@@ -0,0 +1,31 @@
import jwt
# Encoding
# good - key and algorithm supplied
jwt.encode(token, "key", "HS256")
jwt.encode(token, key="key", algorithm="HS256")
# bad - both key and algorithm set to None
jwt.encode(token, None, None)
# bad - empty key
jwt.encode(token, "", algorithm="HS256")
jwt.encode(token, key="", algorithm="HS256")
# Decoding
# good
jwt.decode(token, "key", "HS256")
# bad - unverified decoding
jwt.decode(token, verify=False)
jwt.decode(token, key, options={"verify_signature": False})
# good - verified decoding
jwt.decode(token, verify=True)
jwt.decode(token, key, options={"verify_signature": True})
def indeterminate(verify):
jwt.decode(token, key, verify)

View File

@@ -0,0 +1,22 @@
from jose import jwt
# Encoding
# good - key and algorithm supplied
jwt.encode(token, "key", "HS256")
jwt.encode(token, key="key", algorithm="HS256")
# bad - empty key
jwt.encode(token, "", algorithm="HS256")
jwt.encode(token, key="", algorithm="HS256")
# Decoding
# good
jwt.decode(token, "key", "HS256")
# bad - unverified decoding
jwt.decode(token, key, options={"verify_signature": False})
# good - verified decoding
jwt.decode(token, key, options={"verify_signature": True})