Skip to content

Commit d910021

Browse files
authored
Merge pull request #79 from spraints/insecure-hash-algorithms
Add check for insecure hash algorithms
2 parents 3ff2f04 + 909d38e commit d910021

File tree

4 files changed

+586
-0
lines changed

4 files changed

+586
-0
lines changed

config/_default_shared.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Bundler/DuplicatedGem:
1010
Bundler/OrderedGems:
1111
Enabled: true
1212

13+
GitHub/InsecureHashAlgorithm:
14+
Enabled: true
15+
1316
Layout/BlockAlignment:
1417
Enabled: true
1518

lib/rubocop/cop/github.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require "rubocop/cop/github/insecure_hash_algorithm"
34
require "rubocop/cop/github/rails_application_record"
45
require "rubocop/cop/github/rails_controller_render_action_symbol"
56
require "rubocop/cop/github/rails_controller_render_literal"
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# frozen_string_literal: true
2+
3+
require "rubocop"
4+
5+
module RuboCop
6+
module Cop
7+
module GitHub
8+
class InsecureHashAlgorithm < Cop
9+
MSG = "This hash function is not allowed"
10+
UUID_V3_MSG = "uuid_v3 uses MD5, which is not allowed"
11+
UUID_V5_MSG = "uuid_v5 uses SHA1, which is not allowed"
12+
13+
# Matches constants like these:
14+
# Digest::MD5
15+
# OpenSSL::Digest::MD5
16+
def_node_matcher :insecure_const?, <<-PATTERN
17+
(const (const _ :Digest) #insecure_algorithm?)
18+
PATTERN
19+
20+
# Matches calls like these:
21+
# Digest.new('md5')
22+
# Digest.hexdigest('md5', 'str')
23+
# OpenSSL::Digest.new('md5')
24+
# OpenSSL::Digest.hexdigest('md5', 'str')
25+
# OpenSSL::Digest::Digest.new('md5')
26+
# OpenSSL::Digest::Digest.hexdigest('md5', 'str')
27+
# OpenSSL::Digest::Digest.new(:MD5)
28+
# OpenSSL::Digest::Digest.hexdigest(:MD5, 'str')
29+
def_node_matcher :insecure_digest?, <<-PATTERN
30+
(send
31+
(const _ {:Digest :HMAC})
32+
#not_just_encoding?
33+
#insecure_algorithm?
34+
...)
35+
PATTERN
36+
37+
# Matches calls like "Digest(:MD5)".
38+
def_node_matcher :insecure_hash_lookup?, <<-PATTERN
39+
(send _ :Digest #insecure_algorithm?)
40+
PATTERN
41+
42+
# Matches calls like "OpenSSL::HMAC.new(secret, hash)"
43+
def_node_matcher :openssl_hmac_new?, <<-PATTERN
44+
(send (const (const _ :OpenSSL) :HMAC) :new ...)
45+
PATTERN
46+
47+
# Matches calls like "OpenSSL::HMAC.new(secret, 'sha1')"
48+
def_node_matcher :openssl_hmac_new_insecure?, <<-PATTERN
49+
(send (const (const _ :OpenSSL) :HMAC) :new _ #insecure_algorithm?)
50+
PATTERN
51+
52+
# Matches Rails's Digest::UUID.
53+
def_node_matcher :digest_uuid?, <<-PATTERN
54+
(const (const _ :Digest) :UUID)
55+
PATTERN
56+
57+
def_node_matcher :uuid_v3?, <<-PATTERN
58+
(send (const _ :UUID) :uuid_v3 ...)
59+
PATTERN
60+
61+
def_node_matcher :uuid_v5?, <<-PATTERN
62+
(send (const _ :UUID) :uuid_v5 ...)
63+
PATTERN
64+
65+
def insecure_algorithm?(val)
66+
return false if val == :Digest # Don't match "Digest::Digest".
67+
case alg_name(val)
68+
when *allowed_hash_functions
69+
false
70+
when Symbol
71+
# can't figure this one out, it's nil or a var or const.
72+
false
73+
else
74+
true
75+
end
76+
end
77+
78+
def not_just_encoding?(val)
79+
!just_encoding?(val)
80+
end
81+
82+
def just_encoding?(val)
83+
val == :hexencode || val == :bubblebabble
84+
end
85+
86+
# Built-in hash functions are listed in these docs:
87+
# https://ruby-doc.org/stdlib-2.7.0/libdoc/digest/rdoc/Digest.html
88+
# https://ruby-doc.org/stdlib-2.7.0/libdoc/openssl/rdoc/OpenSSL/Digest.html
89+
DEFAULT_ALLOWED = %w[
90+
SHA256
91+
SHA384
92+
SHA512
93+
].freeze
94+
95+
def allowed_hash_functions
96+
@allowed_algorithms ||= cop_config.fetch("Allowed", DEFAULT_ALLOWED).map(&:downcase)
97+
end
98+
99+
def alg_name(val)
100+
return :nil if val.nil?
101+
return val.to_s.downcase unless val.is_a?(RuboCop::AST::Node)
102+
case val.type
103+
when :sym, :str
104+
val.children.first.to_s.downcase
105+
else
106+
val.type
107+
end
108+
end
109+
110+
def on_const(const_node)
111+
if insecure_const?(const_node) && !digest_uuid?(const_node)
112+
add_offense(const_node, message: MSG)
113+
end
114+
end
115+
116+
def on_send(send_node)
117+
case
118+
when uuid_v3?(send_node)
119+
unless allowed_hash_functions.include?("md5")
120+
add_offense(send_node, message: UUID_V3_MSG)
121+
end
122+
when uuid_v5?(send_node)
123+
unless allowed_hash_functions.include?("sha1")
124+
add_offense(send_node, message: UUID_V5_MSG)
125+
end
126+
when openssl_hmac_new?(send_node)
127+
if openssl_hmac_new_insecure?(send_node)
128+
add_offense(send_node, message: MSG)
129+
end
130+
when insecure_digest?(send_node)
131+
add_offense(send_node, message: MSG)
132+
when insecure_hash_lookup?(send_node)
133+
add_offense(send_node, message: MSG)
134+
end
135+
end
136+
end
137+
end
138+
end
139+
end

0 commit comments

Comments
 (0)