Skip to content

Python: Add JWT security-related queries #5588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5edb3b1
Query upload
jorgectf Apr 1, 2021
ee70eb7
Remove old comment
jorgectf Apr 1, 2021
513055c
Change old comments
jorgectf Apr 1, 2021
7ed7809
Use LocalSourceNode and flowsTo() for better performance
jorgectf Apr 2, 2021
198f8dc
Improve predicates
jorgectf Apr 3, 2021
d22da88
Fix verifiesSignature()
jorgectf Apr 4, 2021
6565680
Finish query
jorgectf Jun 18, 2021
058ade4
Merge remote-tracking branch 'upstream/main' into jorgectf/python/jwt…
jorgectf Jun 18, 2021
07422a1
Move tests under `test/`
jorgectf Jul 1, 2021
4079e53
Add `JWT` framework to `Frameworks.qll`
jorgectf Jul 1, 2021
a1f48db
Make `verifiesSignature()` a predicate
jorgectf Jul 1, 2021
7fb4447
Add `.expected` results
jorgectf Jul 1, 2021
3d2b6f7
Delete outdated comment
jorgectf Jul 1, 2021
f1b3c70
Divide JWT libraries
jorgectf Jul 21, 2021
e14b103
Add indeterminate test to pyjwt
jorgectf Jul 21, 2021
ce507be
Add `Authlib` modeling and tests
jorgectf Jul 21, 2021
8d84d63
Add `Python-Jose` modeling and tests
jorgectf Jul 21, 2021
68f79f0
Update `.expected`
jorgectf Jul 21, 2021
f9b244e
Polish documentation
jorgectf Jul 23, 2021
350cbb4
Polish qhelp and libraries
jorgectf Oct 27, 2021
7069f45
Polish documentation
jorgectf Oct 28, 2021
3dec222
Merge remote-tracking branch 'origin/main' into jorgectf/python/jwt-q…
jorgectf Oct 28, 2021
ef4a27f
Apply code review suggestions
jorgectf Oct 28, 2021
f4d63cc
Apply suggestions from code review
jorgectf Oct 28, 2021
a6c285a
Apply `getItem(_)` and extend `verifiesSignature` readability
jorgectf Oct 28, 2021
b3ec82c
Merge branch 'jorgectf/python/jwt-queries' of https://github.com/jorg…
jorgectf Oct 28, 2021
47b14f1
Polish `Concepts.qll` qldocs
jorgectf Oct 28, 2021
a722631
Apply suggestions from code review
jorgectf Nov 16, 2021
3fe2a08
Update `.expected` file
jorgectf Nov 16, 2021
9ad8a85
Delete redundant checks in `verifiesSignature()`
jorgectf Nov 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import jwt

# algorithm set to None
jwt.encode(payload, "somekey", None)

# empty key
jwt.encode(payload, key="", algorithm="HS256")
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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 + "."
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import jwt

# unverified decoding
jwt.decode(payload, key="somekey", verify=False)
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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."
138 changes: 138 additions & 0 deletions python/ql/src/experimental/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
4 changes: 4 additions & 0 deletions python/ql/src/experimental/semmle/python/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 23 additions & 0 deletions python/ql/src/experimental/semmle/python/frameworks/JWT.qll
Original file line number Diff line number Diff line change
@@ -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)
}
87 changes: 87 additions & 0 deletions python/ql/src/experimental/semmle/python/libraries/Authlib.qll
Original file line number Diff line number Diff line change
@@ -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() }
}
}
Loading