Skip to content

Commit c3f1fba

Browse files
authored
Merge pull request #8598 from hmac/hmac/insecure-dep-resolution
Ruby: Add rb/insecure-dependency query
2 parents 6603f8a + 8f3578c commit c3f1fba

File tree

10 files changed

+277
-0
lines changed

10 files changed

+277
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Provides predicates for reasoning about insecure dependency configurations.
3+
*/
4+
5+
private import ruby
6+
7+
/**
8+
* A method call in a Gemfile.
9+
*/
10+
private class GemfileMethodCall extends MethodCall {
11+
GemfileMethodCall() { this.getLocation().getFile().getBaseName() = "Gemfile" }
12+
}
13+
14+
/**
15+
* Method calls that configure gem dependencies and can specify (possibly insecure) URLs.
16+
*/
17+
abstract private class RelevantGemCall extends GemfileMethodCall {
18+
abstract Expr getAUrlPart();
19+
}
20+
21+
/**
22+
* A call to `source`.
23+
*/
24+
private class SourceCall extends RelevantGemCall {
25+
SourceCall() { this.getMethodName() = "source" }
26+
27+
override Expr getAUrlPart() { result = this.getAnArgument() }
28+
}
29+
30+
/**
31+
* A call to `git_source`.
32+
*/
33+
private class GitSourceCall extends RelevantGemCall {
34+
GitSourceCall() { this.getMethodName() = "git_source" }
35+
36+
override Expr getAUrlPart() { result = this.getBlock().getLastStmt() }
37+
}
38+
39+
/**
40+
* A call to `gem`.
41+
*/
42+
private class GemCall extends RelevantGemCall {
43+
GemCall() { this.getMethodName() = "gem" }
44+
45+
override Expr getAUrlPart() { result = this.getKeywordArgument(["source", "git"]) }
46+
}
47+
48+
/**
49+
* Holds if `s` is a URL with an insecure protocol. `proto` is the protocol.
50+
*/
51+
bindingset[s]
52+
private predicate hasInsecureProtocol(string s, string proto) {
53+
proto = s.regexpCapture("^(http|ftp):.+", 1).toUpperCase()
54+
}
55+
56+
/**
57+
* Holds if `e` is a string containing a URL that uses the insecure protocol `proto`.
58+
*/
59+
private predicate containsInsecureUrl(Expr e, string proto) {
60+
// Handle cases where the string as a whole has no constant value (due to interpolations)
61+
// but has a known prefix. E.g. "http://#{foo}"
62+
exists(StringComponent c | c = e.(StringlikeLiteral).getComponent(0) |
63+
hasInsecureProtocol(c.getConstantValue().getString(), proto)
64+
)
65+
or
66+
hasInsecureProtocol(e.getConstantValue().getString(), proto)
67+
}
68+
69+
/**
70+
* Returns the suggested protocol to use in place of the insecure protocol `proto`.
71+
*/
72+
bindingset[proto]
73+
private string suggestedProtocol(string proto) {
74+
proto = "HTTP" and result = "HTTPS"
75+
or
76+
proto = "FTP" and result = "FTPS or SFTP"
77+
}
78+
79+
/**
80+
* Holds if `url` is a string containing a URL that uses an insecure protocol.
81+
* `msg` is the alert message that will be displayed to the user.
82+
*/
83+
predicate insecureDependencyUrl(Expr url, string msg) {
84+
exists(RelevantGemCall call, string proto |
85+
url = call.getAUrlPart() and
86+
containsInsecureUrl(url, proto) and
87+
msg =
88+
"Dependency source URL uses the unencrypted protocol " + proto + ". Use " +
89+
suggestedProtocol(proto) + " instead."
90+
)
91+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new query, `rb/insecure-dependency`. The query finds cases where Ruby gems may be downloaded over an insecure communication channel.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Using an insecure protocol like HTTP or FTP to download dependencies makes the build process vulnerable to a
9+
man-in-the-middle (MITM) attack.
10+
</p>
11+
<p>
12+
This can allow attackers to inject malicious code into the downloaded dependencies, and thereby
13+
infect the build artifacts and execute arbitrary code on the machine building the artifacts.
14+
</p>
15+
16+
</overview>
17+
<recommendation>
18+
19+
<p>Always use a secure protocol, such as HTTPS or SFTP, when downloading artifacts from a URL.</p>
20+
21+
</recommendation>
22+
23+
<example>
24+
<p>
25+
The below example shows a <code>Gemfile</code> that specifies a gem source using the insecure HTTP protocol.
26+
</p>
27+
<sample src="examples/bad_gemfile.rb" />
28+
<p>
29+
The fix is to change the protocol to HTTPS.
30+
</p>
31+
<sample src="examples/good_gemfile.rb" />
32+
</example>
33+
34+
<references>
35+
<li>
36+
Jonathan Leitschuh:
37+
<a href="https://infosecwriteups.com/want-to-take-over-the-java-ecosystem-all-you-need-is-a-mitm-1fc329d898fb">
38+
Want to take over the Java ecosystem? All you need is a MITM!
39+
</a>
40+
</li>
41+
<li>
42+
Max Veytsman:
43+
<a href="https://max.computer/blog/how-to-take-over-the-computer-of-any-java-or-clojure-or-scala-developer/">
44+
How to take over the computer of any Java (or Clojure or Scala) Developer.
45+
</a>
46+
</li>
47+
<li>
48+
Wikipedia: <a href="https://en.wikipedia.org/wiki/Supply_chain_attack">Supply chain attack.</a>
49+
</li>
50+
<li>
51+
Wikipedia: <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-middle attack.</a>
52+
</li>
53+
</references>
54+
</qhelp>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @name Dependency download using unencrypted communication channel
3+
* @description Using unencrypted protocols to fetch dependencies can leave an application
4+
* open to man-in-the-middle attacks.
5+
* @kind problem
6+
* @problem.severity warning
7+
* @security-severity 8.1
8+
* @precision high
9+
* @id rb/insecure-dependency
10+
* @tags security
11+
* external/cwe/cwe-300
12+
* external/cwe/cwe-319
13+
* external/cwe/cwe-494
14+
* external/cwe/cwe-829
15+
*/
16+
17+
import ruby
18+
import codeql.ruby.security.InsecureDependencyQuery
19+
20+
from Expr url, string msg
21+
where insecureDependencyUrl(url, msg)
22+
select url, msg
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source "http://rubygems.org"
2+
3+
gem "my-gem-a", "1.2.3"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source "https://rubygems.org"
2+
3+
gem "my-gem-a", "1.2.3"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
source "https://rubygems.org" # GOOD
2+
source "http://rubygems.org" # $result=BAD
3+
source "ftp://rubygems.org" # $result=BAD
4+
source "ftps://rubygems.org" # GOOD
5+
source "unknown://rubygems.org" # GOOD
6+
7+
git_source(:a) { "https://github.com" } # GOOD
8+
git_source(:b) { "http://github.com" } # $result=BAD
9+
git_source(:c) { "ftp://github.com" } # $result=BAD
10+
git_source(:d) { "ftps://github.com" } # GOOD
11+
git_source(:e) { "unknown://github.com" } # GOOD
12+
13+
git_source(:f) { |name| "https://github.com/#{name}" } # GOOD
14+
git_source(:g) { |name| "http://github.com/#{name}" } # $result=BAD
15+
git_source(:h) { |name| "ftp://github.com/#{name}" } # $result=BAD
16+
git_source(:i) { |name| "ftps://github.com/#{name}" } # GOOD
17+
git_source(:j) { |name| "unknown://github.com/#{name}" } # GOOD
18+
19+
git_source(:k) do |name|
20+
foo
21+
"https://github.com/#{name}" } # GOOD
22+
end
23+
git_source(:l) do |name|
24+
foo
25+
"http://github.com/#{name}" } # $result=BAD
26+
end
27+
git_source(:m) do |name|
28+
foo
29+
"ftp://github.com/#{name}" } # $result=BAD
30+
end
31+
git_source(:n) do |name|
32+
foo
33+
"ftps://github.com/#{name}" } # GOOD
34+
end
35+
git_source(:o) do |name|
36+
foo
37+
"unknown://github.com/#{name}" } # GOOD
38+
end
39+
40+
gem "jwt", "1.2.3", git: "https://github.com/jwt/ruby-jwt" # GOOD
41+
gem "jwt", "1.2.3", git: "http://github.com/jwt/ruby-jwt" # $result=BAD
42+
gem "jwt", "1.2.3", git: "ftp://github.com/jwt/ruby-jwt" # $result=BAD
43+
gem "jwt", "1.2.3", git: "ftps://github.com/jwt/ruby-jwt" # GOOD
44+
gem "jwt", "1.2.3", git: "unknown://github.com/jwt/ruby-jwt" # GOOD
45+
46+
gem "jwt", "1.2.3", :git => "https://github.com/jwt/ruby-jwt" # GOOD
47+
gem "jwt", "1.2.3", :git => "http://github.com/jwt/ruby-jwt" # $result=BAD
48+
gem "jwt", "1.2.3", :git => "ftp://github.com/jwt/ruby-jwt" # $result=BAD
49+
gem "jwt", "1.2.3", :git => "ftps://github.com/jwt/ruby-jwt" # GOOD
50+
gem "jwt", "1.2.3", :git => "unknown://github.com/jwt/ruby-jwt" # GOOD
51+
52+
gem "jwt", "1.2.3", source: "https://rubygems.org" # GOOD
53+
gem "jwt", "1.2.3", source: "http://rubygems.org" # $result=BAD
54+
gem "jwt", "1.2.3", source: "ftp://rubygems.org" # $result=BAD
55+
gem "jwt", "1.2.3", source: "ftps://rubygems.org" # GOOD
56+
gem "jwt", "1.2.3", source: "unknown://rubygems.org" # GOOD
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
failures
2+
#select
3+
| Gemfile:2:8:2:28 | "http://rubygems.org" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
4+
| Gemfile:3:8:3:27 | "ftp://rubygems.org" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
5+
| Gemfile:8:18:8:36 | "http://github.com" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
6+
| Gemfile:9:18:9:35 | "ftp://github.com" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
7+
| Gemfile:14:25:14:51 | "http://github.com/#{...}" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
8+
| Gemfile:15:25:15:50 | "ftp://github.com/#{...}" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
9+
| Gemfile:25:5:25:31 | "http://github.com/#{...}" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
10+
| Gemfile:29:5:29:30 | "ftp://github.com/#{...}" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
11+
| Gemfile:41:26:41:57 | "http://github.com/jwt/ruby-jwt" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
12+
| Gemfile:42:26:42:56 | "ftp://github.com/jwt/ruby-jwt" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
13+
| Gemfile:47:29:47:60 | "http://github.com/jwt/ruby-jwt" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
14+
| Gemfile:48:29:48:59 | "ftp://github.com/jwt/ruby-jwt" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
15+
| Gemfile:53:29:53:49 | "http://rubygems.org" | Dependency source URL uses the unencrypted protocol HTTP. Use HTTPS instead. |
16+
| Gemfile:54:29:54:48 | "ftp://rubygems.org" | Dependency source URL uses the unencrypted protocol FTP. Use FTPS or SFTP instead. |
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import ruby
2+
import TestUtilities.InlineExpectationsTest
3+
import codeql.ruby.security.InsecureDependencyQuery
4+
5+
class InsecureDependencyTest extends InlineExpectationsTest {
6+
InsecureDependencyTest() { this = "InsecureDependencyTest" }
7+
8+
override string getARelevantTag() { result = "BAD" }
9+
10+
override predicate hasActualResult(Location location, string element, string tag, string value) {
11+
tag = "result" and
12+
value = "BAD" and
13+
exists(Expr e |
14+
insecureDependencyUrl(e, _) and
15+
location = e.getLocation() and
16+
element = e.toString()
17+
)
18+
}
19+
}
20+
21+
from Expr url, string msg
22+
where insecureDependencyUrl(url, msg)
23+
select url, msg
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Calls to `gem` etc. outside of the Gemfile should be ignored, since they may not be configuring dependencies.
2+
3+
gem "foo", git: "http://foo.com"
4+
git_source :a { |x| "http://foo.com" }
5+
source "http://foo.com"

0 commit comments

Comments
 (0)