Skip to content

Commit 25c416e

Browse files
authored
Merge pull request github#14061 from maikypedia/maikypedia/ruby-jwt
Ruby: JWT Security Queries (CWE-347)
2 parents 21bea38 + 22850b2 commit 25c416e

18 files changed

+320
-0
lines changed

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,3 +1281,92 @@ module LdapBind {
12811281
abstract predicate usesSsl();
12821282
}
12831283
}
1284+
1285+
/**
1286+
* A data-flow node that encodes a Jwt token.
1287+
*
1288+
* Extend this class to refine existing API models. If you want to model new APIs,
1289+
* extend `JwtEncoding::Range` instead.
1290+
*/
1291+
class JwtEncoding extends DataFlow::Node instanceof JwtEncoding::Range {
1292+
/** Gets the argument containing the encoding payload. */
1293+
DataFlow::Node getPayload() { result = super.getPayload() }
1294+
1295+
/** Gets the argument containing the encoding algorithm. */
1296+
DataFlow::Node getAlgorithm() { result = super.getAlgorithm() }
1297+
1298+
/** Gets the argument containing the encoding key. */
1299+
DataFlow::Node getKey() { result = super.getKey() }
1300+
1301+
/** Checks if the payloads gets signed while encoding. */
1302+
predicate signsPayload() { super.signsPayload() }
1303+
}
1304+
1305+
/** Provides a class for modeling new Jwt token encoding APIs. */
1306+
module JwtEncoding {
1307+
/**
1308+
* A data-flow node that encodes a Jwt token.
1309+
*
1310+
* Extend this class to model new APIs. If you want to refine existing API models,
1311+
* extend `JwtEncoding` instead.
1312+
*/
1313+
abstract class Range extends DataFlow::Node {
1314+
/** Gets the argument containing the encoding payload. */
1315+
abstract DataFlow::Node getPayload();
1316+
1317+
/** Gets the argument containing the encoding algorithm. */
1318+
abstract DataFlow::Node getAlgorithm();
1319+
1320+
/** Gets the argument containing the encoding key. */
1321+
abstract DataFlow::Node getKey();
1322+
1323+
/** Checks if the payloads gets signed while encoding. */
1324+
abstract predicate signsPayload();
1325+
}
1326+
}
1327+
1328+
/**
1329+
* A data-flow node that decodes a Jwt token.
1330+
*
1331+
* Extend this class to refine existing API models. If you want to model new APIs,
1332+
* extend `JwtDecoding::Range` instead.
1333+
*/
1334+
class JwtDecoding extends DataFlow::Node instanceof JwtDecoding::Range {
1335+
/** Gets the argument containing the encoding payload. */
1336+
DataFlow::Node getPayload() { result = super.getPayload() }
1337+
1338+
/** Gets the argument containing the encoding algorithm. */
1339+
DataFlow::Node getAlgorithm() { result = super.getAlgorithm() }
1340+
1341+
/** Gets the argument containing the encoding key. */
1342+
DataFlow::Node getOptions() { result = super.getOptions() }
1343+
1344+
/** Checks if the signature gets verified while decoding. */
1345+
predicate verifiesSignature() { super.verifiesSignature() }
1346+
}
1347+
1348+
/** Provides a class for modeling new Jwt token encoding APIs. */
1349+
module JwtDecoding {
1350+
/**
1351+
* A data-flow node that encodes a Jwt token.
1352+
*
1353+
* Extend this class to model new APIs. If you want to refine existing API models,
1354+
* extend `JwtDecoding` instead.
1355+
*/
1356+
abstract class Range extends DataFlow::Node {
1357+
/** Gets the argument containing the encoding payload. */
1358+
abstract DataFlow::Node getPayload();
1359+
1360+
/** Gets the argument containing the encoding algorithm. */
1361+
abstract DataFlow::Node getAlgorithm();
1362+
1363+
/** Gets the argument containing the encoding key. */
1364+
abstract DataFlow::Node getKey();
1365+
1366+
/** Gets the argument containing the encoding options. */
1367+
abstract DataFlow::Node getOptions();
1368+
1369+
/** Checks if the signature gets verified while decoding. */
1370+
abstract predicate verifiesSignature();
1371+
}
1372+
}

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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 signsPayload() {
25+
not (
26+
this.getKey().getConstantValue().isStringlikeValue("") or
27+
this.getKey().(DataFlow::ExprNode).getConstantValue().isNil()
28+
)
29+
}
30+
}
31+
32+
/** A call to `JWT.decode`, considered as a JWT decoding. */
33+
private class JwtDecode extends JwtDecoding::Range, DataFlow::CallNode {
34+
JwtDecode() { this = API::getTopLevelMember("JWT").getAMethodCall("decode") }
35+
36+
override DataFlow::Node getPayload() { result = this.getArgument(0) }
37+
38+
override DataFlow::Node getAlgorithm() {
39+
result = this.getArgument(3).(DataFlow::PairNode).getValue() or
40+
result =
41+
this.getArgument(3)
42+
.(DataFlow::HashLiteralNode)
43+
.getElementFromKey(any(Ast::ConstantValue cv | cv.isStringlikeValue("algorithm"))) or
44+
result = this.getArgument(2)
45+
}
46+
47+
override DataFlow::Node getKey() { result = this.getArgument(1) }
48+
49+
override DataFlow::Node getOptions() { result = this.getArgument(3) }
50+
51+
override predicate verifiesSignature() {
52+
not this.getArgument(2).getConstantValue().isBoolean(false) and
53+
not this.getAlgorithm().getConstantValue().isStringlikeValue("none")
54+
or
55+
this.getNumberOfArguments() < 3
56+
}
57+
}
58+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new experimental query, `rb/jwt-empty-secret-or-algorithm`, to detect when application uses an empty secret or weak algorithm.
5+
* Added a new experimental query, `rb/jwt-missing-verification`, to detect when the application does not verify a JWT payload.
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+
JSON Web Tokens should be signed using a strong cryptographic algorithm and non-empty secret.
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.signsPayload()
15+
select jwtEncoding.getPayload(), "This JWT encoding uses an 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.verifiesSignature()
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')

0 commit comments

Comments
 (0)