Skip to content

Commit 17565cd

Browse files
committed
Add JWT Security Queries
1 parent cf53956 commit 17565cd

17 files changed

+312
-0
lines changed

ruby/ql/lib/codeql/ruby/Concepts.qll

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,3 +1250,92 @@ module LdapExecution {
12501250
abstract DataFlow::Node getQuery();
12511251
}
12521252
}
1253+
1254+
/**
1255+
* A data-flow node that encodes a Jwt token.
1256+
*
1257+
* Extend this class to refine existing API models. If you want to model new APIs,
1258+
* extend `JwtEncoding::Range` instead.
1259+
*/
1260+
class JwtEncoding extends DataFlow::Node instanceof JwtEncoding::Range {
1261+
/** Gets the argument containing the encoding payload. */
1262+
DataFlow::Node getPayload() { result = super.getPayload() }
1263+
1264+
/** Gets the argument containing the encoding algorithm. */
1265+
DataFlow::Node getAlgorithm() { result = super.getAlgorithm() }
1266+
1267+
/** Gets the argument containing the encoding key. */
1268+
DataFlow::Node getKey() { result = super.getKey() }
1269+
1270+
/** Checks if the payloads gets signed while encoding. */
1271+
predicate signs() { super.signs() }
1272+
}
1273+
1274+
/** Provides a class for modeling new Jwt token encoding APIs. */
1275+
module JwtEncoding {
1276+
/**
1277+
* A data-flow node that encodes a Jwt token.
1278+
*
1279+
* Extend this class to model new APIs. If you want to refine existing API models,
1280+
* extend `JwtEncoding` instead.
1281+
*/
1282+
abstract class Range extends DataFlow::Node {
1283+
/** Gets the argument containing the encoding payload. */
1284+
abstract DataFlow::Node getPayload();
1285+
1286+
/** Gets the argument containing the encoding algorithm. */
1287+
abstract DataFlow::Node getAlgorithm();
1288+
1289+
/** Gets the argument containing the encoding key. */
1290+
abstract DataFlow::Node getKey();
1291+
1292+
/** Checks if the payloads gets signed while encoding. */
1293+
abstract predicate signs();
1294+
}
1295+
}
1296+
1297+
/**
1298+
* A data-flow node that decodes a Jwt token.
1299+
*
1300+
* Extend this class to refine existing API models. If you want to model new APIs,
1301+
* extend `JwtDecoding::Range` instead.
1302+
*/
1303+
class JwtDecoding extends DataFlow::Node instanceof JwtDecoding::Range {
1304+
/** Gets the argument containing the encoding payload. */
1305+
DataFlow::Node getPayload() { result = super.getPayload() }
1306+
1307+
/** Gets the argument containing the encoding algorithm. */
1308+
DataFlow::Node getAlgorithm() { result = super.getAlgorithm() }
1309+
1310+
/** Gets the argument containing the encoding key. */
1311+
DataFlow::Node getOptions() { result = super.getOptions() }
1312+
1313+
/** Checks if the signature gets verified while decoding. */
1314+
predicate verifies() { super.verifies() }
1315+
}
1316+
1317+
/** Provides a class for modeling new Jwt token encoding APIs. */
1318+
module JwtDecoding {
1319+
/**
1320+
* A data-flow node that encodes a Jwt token.
1321+
*
1322+
* Extend this class to model new APIs. If you want to refine existing API models,
1323+
* extend `JwtDecoding` instead.
1324+
*/
1325+
abstract class Range extends DataFlow::Node {
1326+
/** Gets the argument containing the encoding payload. */
1327+
abstract DataFlow::Node getPayload();
1328+
1329+
/** Gets the argument containing the encoding algorithm. */
1330+
abstract DataFlow::Node getAlgorithm();
1331+
1332+
/** Gets the argument containing the encoding key. */
1333+
abstract DataFlow::Node getKey();
1334+
1335+
/** Gets the argument containing the encoding options. */
1336+
abstract DataFlow::Node getOptions();
1337+
1338+
/** Checks if the signature gets verified while decoding. */
1339+
abstract predicate verifies();
1340+
}
1341+
}

ruby/ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ private import codeql.ruby.frameworks.Pg
3737
private import codeql.ruby.frameworks.Yaml
3838
private import codeql.ruby.frameworks.Sequel
3939
private import codeql.ruby.frameworks.Ldap
40+
private import codeql.ruby.frameworks.Jwt
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Provides creation, verification and decoding JSON Web Tokens (JWT).
3+
*/
4+
5+
private import ruby
6+
private import codeql.ruby.ApiGraphs
7+
private import codeql.ruby.dataflow.FlowSummary
8+
private import codeql.ruby.Concepts
9+
10+
/**
11+
* Provides creation, verification and decoding JSON Web Tokens (JWT).
12+
*/
13+
module Jwt {
14+
/** A call to `JWT.encode`, considered as a JWT encoding. */
15+
private class JwtEncode extends JwtEncoding::Range, DataFlow::CallNode {
16+
JwtEncode() { this = API::getTopLevelMember("JWT").getAMethodCall("encode") }
17+
18+
override DataFlow::Node getPayload() { result = this.getArgument(0) }
19+
20+
override DataFlow::Node getAlgorithm() { result = this.getArgument(2) }
21+
22+
override DataFlow::Node getKey() { result = this.getArgument(1) }
23+
24+
override predicate signs() {
25+
not (this.getKey().getConstantValue().isStringlikeValue("") or this.getKey().(DataFlow::ExprNode).getConstantValue().isNil())
26+
}
27+
}
28+
29+
/** A call to `JWT.decode`, considered as a JWT decoding. */
30+
private class JwtDecode extends JwtDecoding::Range, DataFlow::CallNode {
31+
JwtDecode() { this = API::getTopLevelMember("JWT").getAMethodCall("decode") }
32+
33+
override DataFlow::Node getPayload() { result = this.getArgument(0) }
34+
35+
override DataFlow::Node getAlgorithm() {
36+
result.asExpr().getExpr() = this.getArgument(3).asExpr().getExpr().(Pair).getValue() or
37+
result =
38+
this.getArgument(3)
39+
.(DataFlow::HashLiteralNode)
40+
.getElementFromKey(any(Ast::ConstantValue cv | cv.isStringlikeValue("algorithm"))) or
41+
result = this.getArgument(2)
42+
}
43+
44+
override DataFlow::Node getKey() { result = this.getArgument(1) }
45+
46+
override DataFlow::Node getOptions() { result = this.getArgument(3) }
47+
48+
override predicate verifies() {
49+
not this.getArgument(2).getConstantValue().isBoolean(false) and
50+
not this.getAlgorithm().getConstantValue().isStringlikeValue("none")
51+
or
52+
this.getNumberOfArguments() < 3
53+
}
54+
}
55+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Applications encoding a JSON Web Token (JWT) may be vulnerable when it's not verified or algorithm is <code>none</code>.
9+
</p>
10+
</overview>
11+
12+
<recommendation>
13+
<p>
14+
Use non-empty nor <code>None</code> values while encoding JWT payloads.
15+
</p>
16+
</recommendation>
17+
18+
<example>
19+
<p>
20+
In the example below, the secret used is an empty string and none algorithm is used. This may allow a malicious actor to make changes to a JWT payload.
21+
</p>
22+
23+
<sample src="examples/EmptyJWTSecretBad.rb" />
24+
25+
<p>
26+
The following code fixes the problem by using a non-empty cryptographic secret or key to encode JWT payloads.
27+
</p>
28+
29+
<sample src="examples/EmptyJWTSecretGood.rb" />
30+
</example>
31+
32+
<references>
33+
<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>
34+
</references>
35+
</qhelp>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
* @precision high
7+
* @id rb/jwt-empty-secret-or-algorithm
8+
* @tags security
9+
*/
10+
11+
private import codeql.ruby.Concepts
12+
13+
from JwtEncoding jwtEncoding
14+
where not jwtEncoding.signs()
15+
select jwtEncoding.getPayload(), "This JWT encoding uses empty key or none algorithm."
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Applications decoding a JSON Web Token (JWT) may be vulnerable when the key isn't verified.
9+
</p>
10+
</overview>
11+
12+
<recommendation>
13+
<p>
14+
Calls to <code>verify()</code> functions should use a cryptographic secret or key to decode JWT payloads.</p>
15+
</recommendation>
16+
17+
<example>
18+
<p>
19+
In the example below, false is used to disable the integrity enforcement of a JWT payload and none algorithm is used. This may allow a malicious actor to make changes to a JWT payload.
20+
</p>
21+
22+
<sample src="examples/MissingJWTVerificationBad.rb" />
23+
24+
<p>
25+
The following code fixes the problem by using a cryptographic secret or key to decode JWT payloads.
26+
</p>
27+
28+
<sample src="examples/MissingJWTVerificationGood.rb" />
29+
</example>
30+
31+
<references>
32+
<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>
33+
</references>
34+
</qhelp>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
* @precision high
7+
* @id rb/jwt-missing-verification
8+
* @tags security
9+
* external/cwe/cwe-347
10+
*/
11+
12+
private import codeql.ruby.Concepts
13+
14+
from JwtDecoding jwtDecoding
15+
where not jwtDecoding.verifies()
16+
select jwtDecoding.getPayload(), "is not verified with a cryptographic secret or public key."
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
require 'jwt'
2+
3+
token1 = JWT.encode({ foo: 'bar' }, "secret", 'none')
4+
5+
token2 = JWT.encode({ foo: 'bar' }, nil, 'HS256')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
require 'jwt'
2+
3+
token = JWT.encode({ foo: 'bar' }, "secret", 'HS256')
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require 'jwt'
2+
3+
token = JWT.encode({ foo: 'bar' }, nil, 'none')
4+
5+
decoded1 = JWT.decode(token, nil, false, algorithm: 'HS256')
6+
7+
decoded2 = JWT.decode(token, "secret", true, algorithm: 'none')

0 commit comments

Comments
 (0)