Skip to content

Commit eed98bd

Browse files
authored
Merge pull request #5588 from jorgectf/jorgectf/python/jwt-queries
Python: Add JWT security-related queries
2 parents 8d22db8 + 9ad8a85 commit eed98bd

19 files changed

+659
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import jwt
2+
3+
# algorithm set to None
4+
jwt.encode(payload, "somekey", None)
5+
6+
# empty key
7+
jwt.encode(payload, key="", algorithm="HS256")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Applications encoding a JSON Web Token (JWT) may be vulnerable when the applied key or algorithm
7+
is empty or <code>None</code>.</p>
8+
</overview>
9+
10+
<recommendation>
11+
<p>Use non-empty nor <code>None</code> values while encoding JWT payloads.</p>
12+
</recommendation>
13+
14+
<example>
15+
<p>This example shows two PyJWT encoding calls.
16+
17+
In the first place, the encoding process use a None algorithm whereas the second example uses an
18+
empty key. Both examples leave the payload insecurely encoded.
19+
</p>
20+
21+
<sample src="JWTEmptyKeyOrAlgorithm.py" />
22+
</example>
23+
24+
<references>
25+
<li>PyJWT: <a href="https://pyjwt.readthedocs.io/en/stable/">Documentation</a>.</li>
26+
<li>Authlib JWT: <a href="https://docs.authlib.org/en/latest/specs/rfc7519.html">Documentation</a>.</li>
27+
<li>Python-Jose: <a href="https://github.com/mpdavis/python-jose">Documentation</a>.</li>
28+
<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>
29+
</references>
30+
</qhelp>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @name JWT encoding using empty key or algorithm
3+
* @description The application uses an empty secret or algorithm while encoding a JWT Token.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id py/jwt-empty-secret-or-algorithm
7+
* @tags security
8+
*/
9+
10+
// determine precision above
11+
import python
12+
import experimental.semmle.python.Concepts
13+
import experimental.semmle.python.frameworks.JWT
14+
15+
from JWTEncoding jwtEncoding, string affectedComponent
16+
where
17+
affectedComponent = "algorithm" and
18+
isEmptyOrNone(jwtEncoding.getAlgorithm())
19+
or
20+
affectedComponent = "key" and
21+
isEmptyOrNone(jwtEncoding.getKey())
22+
select jwtEncoding, "This JWT encoding has an empty " + affectedComponent + "."
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import jwt
2+
3+
# unverified decoding
4+
jwt.decode(payload, key="somekey", verify=False)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Applications decoding a JSON Web Token (JWT) may be vulnerable when the
7+
key isn't verified in the process.
8+
</p>
9+
</overview>
10+
11+
<recommendation>
12+
<p>Set the <code>verify</code> argument to <code>True</code> or use
13+
a framework that does it by default.
14+
</p>
15+
</recommendation>
16+
17+
<example>
18+
<p>This example shows a PyJWT encoding call with the <code>verify</code>
19+
argument set to <code>False</code>.
20+
</p>
21+
22+
<sample src="JWTMissingSecretOrPublicKeyVerification.py" />
23+
</example>
24+
25+
<references>
26+
<li>PyJWT: <a href="https://pyjwt.readthedocs.io/en/stable/">Documentation</a>.</li>
27+
<li>Authlib JWT: <a href="https://docs.authlib.org/en/latest/specs/rfc7519.html">Documentation</a>.</li>
28+
<li>Python-Jose: <a href="https://github.com/mpdavis/python-jose">Documentation</a>.</li>
29+
</references>
30+
</qhelp>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @name JWT missing secret or public key verification
3+
* @description The application does not verify the JWT payload with a cryptographic secret or public key.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id py/jwt-missing-verification
7+
* @tags security
8+
* external/cwe/cwe-347
9+
*/
10+
11+
// determine precision above
12+
import python
13+
import experimental.semmle.python.Concepts
14+
15+
from JWTDecoding jwtDecoding
16+
where not jwtDecoding.verifiesSignature()
17+
select jwtDecoding.getPayload(), "is not verified with a cryptographic secret or public key."

python/ql/src/experimental/semmle/python/Concepts.qll

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,141 @@ class HeaderDeclaration extends DataFlow::Node {
296296
*/
297297
DataFlow::Node getValueArg() { result = range.getValueArg() }
298298
}
299+
300+
/** Provides classes for modeling JWT encoding-related APIs. */
301+
module JWTEncoding {
302+
/**
303+
* A data-flow node that collects methods encoding a JWT token.
304+
*
305+
* Extend this class to model new APIs. If you want to refine existing API models,
306+
* extend `JWTEncoding` instead.
307+
*/
308+
abstract class Range extends DataFlow::Node {
309+
/**
310+
* Gets the argument containing the encoding payload.
311+
*/
312+
abstract DataFlow::Node getPayload();
313+
314+
/**
315+
* Gets the argument containing the encoding key.
316+
*/
317+
abstract DataFlow::Node getKey();
318+
319+
/**
320+
* Gets the argument for the algorithm used in the encoding.
321+
*/
322+
abstract DataFlow::Node getAlgorithm();
323+
324+
/**
325+
* Gets a string representation of the algorithm used in the encoding.
326+
*/
327+
abstract string getAlgorithmString();
328+
}
329+
}
330+
331+
/**
332+
* A data-flow node that collects methods encoding a JWT token.
333+
*
334+
* Extend this class to refine existing API models. If you want to model new APIs,
335+
* extend `JWTEncoding::Range` instead.
336+
*/
337+
class JWTEncoding extends DataFlow::Node instanceof JWTEncoding::Range {
338+
/**
339+
* Gets the argument containing the payload.
340+
*/
341+
DataFlow::Node getPayload() { result = super.getPayload() }
342+
343+
/**
344+
* Gets the argument containing the encoding key.
345+
*/
346+
DataFlow::Node getKey() { result = super.getKey() }
347+
348+
/**
349+
* Gets the argument for the algorithm used in the encoding.
350+
*/
351+
DataFlow::Node getAlgorithm() { result = super.getAlgorithm() }
352+
353+
/**
354+
* Gets a string representation of the algorithm used in the encoding.
355+
*/
356+
string getAlgorithmString() { result = super.getAlgorithmString() }
357+
}
358+
359+
/** Provides classes for modeling JWT decoding-related APIs. */
360+
module JWTDecoding {
361+
/**
362+
* A data-flow node that collects methods decoding a JWT token.
363+
*
364+
* Extend this class to model new APIs. If you want to refine existing API models,
365+
* extend `JWTDecoding` instead.
366+
*/
367+
abstract class Range extends DataFlow::Node {
368+
/**
369+
* Gets the argument containing the encoding payload.
370+
*/
371+
abstract DataFlow::Node getPayload();
372+
373+
/**
374+
* Gets the argument containing the encoding key.
375+
*/
376+
abstract DataFlow::Node getKey();
377+
378+
/**
379+
* Gets the argument for the algorithm used in the encoding.
380+
*/
381+
abstract DataFlow::Node getAlgorithm();
382+
383+
/**
384+
* Gets a string representation of the algorithm used in the encoding.
385+
*/
386+
abstract string getAlgorithmString();
387+
388+
/**
389+
* Gets the options Node used in the encoding.
390+
*/
391+
abstract DataFlow::Node getOptions();
392+
393+
/**
394+
* Checks if the signature gets verified while decoding.
395+
*/
396+
abstract predicate verifiesSignature();
397+
}
398+
}
399+
400+
/**
401+
* A data-flow node that collects methods encoding a JWT token.
402+
*
403+
* Extend this class to refine existing API models. If you want to model new APIs,
404+
* extend `JWTDecoding::Range` instead.
405+
*/
406+
class JWTDecoding extends DataFlow::Node instanceof JWTDecoding::Range {
407+
/**
408+
* Gets the argument containing the payload.
409+
*/
410+
DataFlow::Node getPayload() { result = super.getPayload() }
411+
412+
/**
413+
* Gets the argument containing the encoding key.
414+
*/
415+
DataFlow::Node getKey() { result = super.getKey() }
416+
417+
/**
418+
* Gets the argument for the algorithm used in the encoding.
419+
*/
420+
DataFlow::Node getAlgorithm() { result = super.getAlgorithm() }
421+
422+
/**
423+
* Gets a string representation of the algorithm used in the encoding.
424+
*/
425+
string getAlgorithmString() { result = super.getAlgorithmString() }
426+
427+
/**
428+
* Gets the options Node used in the encoding.
429+
*/
430+
DataFlow::Node getOptions() { result = super.getOptions() }
431+
432+
/**
433+
* Checks if the signature gets verified while decoding.
434+
*/
435+
predicate verifiesSignature() { super.verifiesSignature() }
436+
}

python/ql/src/experimental/semmle/python/Frameworks.qll

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ private import experimental.semmle.python.frameworks.Werkzeug
99
private import experimental.semmle.python.frameworks.LDAP
1010
private import experimental.semmle.python.frameworks.NoSQL
1111
private import experimental.semmle.python.frameworks.Log
12+
private import experimental.semmle.python.frameworks.JWT
13+
private import experimental.semmle.python.libraries.PyJWT
14+
private import experimental.semmle.python.libraries.Authlib
15+
private import experimental.semmle.python.libraries.PythonJose
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
private import python
2+
private import semmle.python.ApiGraphs
3+
4+
/** Checks if the argument is empty or none. */
5+
predicate isEmptyOrNone(DataFlow::Node arg) { isEmpty(arg) or isNone(arg) }
6+
7+
/** Checks if an empty string `""` flows to `arg` */
8+
predicate isEmpty(DataFlow::Node arg) {
9+
exists(StrConst emptyString |
10+
emptyString.getText() = "" and
11+
DataFlow::exprNode(emptyString).(DataFlow::LocalSourceNode).flowsTo(arg)
12+
)
13+
}
14+
15+
/** Checks if `None` flows to `arg` */
16+
predicate isNone(DataFlow::Node arg) {
17+
DataFlow::exprNode(any(None no)).(DataFlow::LocalSourceNode).flowsTo(arg)
18+
}
19+
20+
/** Checks if `False` flows to `arg` */
21+
predicate isFalse(DataFlow::Node arg) {
22+
DataFlow::exprNode(any(False falseExpr)).(DataFlow::LocalSourceNode).flowsTo(arg)
23+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
private import python
2+
private import experimental.semmle.python.Concepts
3+
private import semmle.python.ApiGraphs
4+
private import experimental.semmle.python.frameworks.JWT
5+
6+
private module Authlib {
7+
/** Gets a reference to `authlib.jose.(jwt|JsonWebToken)` */
8+
private API::Node authlibJWT() {
9+
result in [
10+
API::moduleImport("authlib").getMember("jose").getMember("jwt"),
11+
API::moduleImport("authlib").getMember("jose").getMember("JsonWebToken").getReturn()
12+
]
13+
}
14+
15+
/** Gets a reference to `jwt.encode` */
16+
private API::Node authlibJWTEncode() { result = authlibJWT().getMember("encode") }
17+
18+
/** Gets a reference to `jwt.decode` */
19+
private API::Node authlibJWTDecode() { result = authlibJWT().getMember("decode") }
20+
21+
/**
22+
* Gets a call to `authlib.jose.(jwt|JsonWebToken).encode`.
23+
*
24+
* Given the following example:
25+
*
26+
* ```py
27+
* jwt.encode({"alg": "HS256"}, token, "key")
28+
* ```
29+
*
30+
* * `this` would be `jwt.encode({"alg": "HS256"}, token, "key")`.
31+
* * `getPayload()`'s result would be `token`.
32+
* * `getKey()`'s result would be `"key"`.
33+
* * `getAlgorithm()`'s result would be `"HS256"`.
34+
* * `getAlgorithmstring()`'s result would be `HS256`.
35+
*/
36+
private class AuthlibJWTEncodeCall extends DataFlow::CallCfgNode, JWTEncoding::Range {
37+
AuthlibJWTEncodeCall() { this = authlibJWTEncode().getACall() }
38+
39+
override DataFlow::Node getPayload() { result = this.getArg(1) }
40+
41+
override DataFlow::Node getKey() { result = this.getArg(2) }
42+
43+
override DataFlow::Node getAlgorithm() {
44+
exists(KeyValuePair headerDict |
45+
headerDict = this.getArg(0).asExpr().(Dict).getItem(_) and
46+
headerDict.getKey().(Str_).getS().matches("alg") and
47+
result.asExpr() = headerDict.getValue()
48+
)
49+
}
50+
51+
override string getAlgorithmString() {
52+
exists(StrConst str |
53+
DataFlow::exprNode(str).(DataFlow::LocalSourceNode).flowsTo(getAlgorithm()) and
54+
result = str.getText()
55+
)
56+
}
57+
}
58+
59+
/**
60+
* Gets a call to `authlib.jose.(jwt|JsonWebToken).decode`
61+
*
62+
* Given the following example:
63+
*
64+
* ```py
65+
* jwt.decode(token, key)
66+
* ```
67+
*
68+
* * `this` would be `jwt.decode(token, key)`.
69+
* * `getPayload()`'s result would be `token`.
70+
* * `getKey()`'s result would be `key`.
71+
*/
72+
private class AuthlibJWTDecodeCall extends DataFlow::CallCfgNode, JWTDecoding::Range {
73+
AuthlibJWTDecodeCall() { this = authlibJWTDecode().getACall() }
74+
75+
override DataFlow::Node getPayload() { result = this.getArg(0) }
76+
77+
override DataFlow::Node getKey() { result = this.getArg(1) }
78+
79+
override DataFlow::Node getAlgorithm() { none() }
80+
81+
override string getAlgorithmString() { none() }
82+
83+
override DataFlow::Node getOptions() { none() }
84+
85+
override predicate verifiesSignature() { any() }
86+
}
87+
}

0 commit comments

Comments
 (0)